<?php

/**
 * @file
 * Provides a URL field type that stores external links with optional titles.
 */

/**
 * Implements hook_help().
 */
function url_help($path, $arg) {
  switch ($path) {
    case 'admin/help#url':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The URL module defines a simple link field type for the Field module. Links are external URLs, can have an optional title for each link, and they can be formatted when displayed. See the <a href="@field-help">Field module help page</a> for more information about fields.', array('@field-help' => url('admin/help/field'))) . '</p>';
      return $output;
  }
}

/**
 * Implements hook_field_info().
 */
function url_field_info() {
  $info['url'] = array(
    'label' => t('URL'),
    'description' => t('This field stores URLs with an optional title.'),
    'instance_settings' => array(
      'title_field' => 0,
      'title_fetch' => 0,
    ),
    'default_widget' => 'url_external',
    'default_formatter' => 'url_default',
  );

  return $info;
}

/**
 * Implements hook_field_instance_settings_form().
 */
function url_field_instance_settings_form($field, $instance) {
  $settings = $instance['settings'];

  $form['title_field'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable <em>Title</em> field'),
    '#default_value' => $settings['title_field'],
    '#description' => t('The title attribute is displayed when the link is displayed.'),
  );
  $form['title_fetch'] = array(
    '#type' => 'checkbox',
    '#title' => t('Attempt to fetch the title value from the URL if the <em>Title field</em> is empty.'),
    '#default_value' => $settings['title_fetch'],
  );

  return $form;
}

/**
 * Implements hook_field_load().
 */
function url_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => &$item) {
      if (empty($item['attributes'])) {
        $item['attributes'] = array();
      }
      else {
        $item['attributes'] = unserialize($item['attributes']);
      }
    }
  }
}

/**
 * Implements hook_field_is_empty().
 */
function url_field_is_empty($item, $field) {
  return !isset($item['value']) || $item['value'] === '';
}

/**
 * Implements hook_field_presave().
 */
function url_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
  $settings = $instance['settings'];

  foreach ($items as $delta => &$item) {
    // If the link title is empty, and auto-fetching is enabled, then attempt
    // to extract the title from a request to the URL.
    // Skip this process if $entity is empty (field default value form)
    if (!empty($entity) && !empty($settings['title_fetch']) && !empty($item['value']) && empty($item['title'])) {
      // Parse the URL into a form ready for url().
      $parsed = drupal_parse_url($item['value']);
      $url = url($parsed['path'], array('query' => $parsed['query'], 'fragment' => $parsed['fragment'], 'absolute' => TRUE));
      $item['title'] = url_fetch_title($url);
    }

    // Trim any spaces around the URL title.
    $item['title'] = isset($item['title']) && is_string($item['title']) ? trim($item['title']) : '';

    // Serialize the attributes array.
    $item['attributes'] = !empty($item['attributes']) && is_array($item['attributes']) ? serialize($item['attributes']) : NULL;
  }
}

/**
 * Implements hook_field_widget_info().
 */
function url_field_widget_info() {
  $info['url_external'] = array(
    'label' => t('External URL field'),
    'field types' => array('url'),
    'settings' => array('size' => 60),
  );

  return $info;
}

/**
 * Implements hook_field_widget_settings_form().
 */
function url_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];

  $form['size'] = array(
    '#type' => 'textfield',
    '#title' => t('Size of URL field'),
    '#default_value' => $settings['size'],
    '#required' => TRUE,
    '#element_validate' => array('element_validate_integer_positive'),
    '#weight' => -1,
  );

  return $form;
}

/**
 * Implements hook_field_widget_form().
 */
function url_field_widget_form($form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $settings = $instance['settings'];

  // Display this element in a fieldset if there is only one value.
  if ($field['cardinality'] == 1) {
    $element['#type'] = 'fieldset';
  }

  // URL field.
  $element['value'] = array(
    '#type' => 'urlfield', // Assumes elements module is enabled.
    '#title' => t('URL'),
    '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : '',
    '#size' => $instance['widget']['settings']['size'],
    '#maxlength' => 2048,
    '#required' => !empty($element['#required']),
    '#prefix' => '<div class="field-value field-value-url">',
    '#suffix' => '</div>',
  );

  if (!module_exists('elements')) {
    $element['value']['#type'] = 'textfield';
    $element['value']['#element_validate'] = array('url_validate_url');
  }

  // Title field.
  if (!empty($settings['title_field'])) {
    $element['title'] = array(
      '#type' => 'textfield',
      '#title' => t('Title'),
      '#default_value' => isset($items[$delta]['title']) ? $items[$delta]['title'] : '',
      '#maxlength' => 1024,
      '#weight' => 10,
      '#prefix' => '<div class="field-value field-value-title">',
      '#suffix' => '</div>',
    );

    // Add additional styling to make both fields work together visually.
    $element['#attached']['css'][] = drupal_get_path('module', 'url') . '/url.field.css';
  }
  else {
    $element['title'] = array(
      '#type' => 'value',
      '#default_value' => isset($items[$delta]['title']) ? $items[$delta]['title'] : '',
    );
  }

  // Exposing the attributes array in the widget is left for alternate and more
  // advanced field widgets.
  $element['attributes'] = array(
    '#type' => 'value',
    '#value' => !empty($items[$delta]['attributes']) ? $items[$delta]['attributes'] : array(),
  );

  // If the widget is being used on a default value form, due to a core bug we
  // need to set the attributes default value to be an already serialized
  // value so that it saves properly.
  // @todo Remove when http://drupal.org/node/1899498 is fixed.
  if (empty($element['#entity']) && !isset($items[$delta]['attributes'])) {
    $element['attributes']['#value'] = NULL;
  }

  return $element;
}

/**
 * Implements hook_field_prepare_view().
 */
function url_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => &$item) {
      // Split out the link into the parts required for url(): path and options.
      $parsed = drupal_parse_url($item['value']);
      $item['path'] = $parsed['path'];
      $item['options'] = array(
        'query' => $parsed['query'],
        'fragment' => $parsed['fragment'],
        'attributes' => &$item['attributes'],
      );
    }
  }
}

/**
 * Implements hook_field_formatter_info().
 */
function url_field_formatter_info() {
  $info['url_default'] = array(
    'label' => t('Link'),
    'field types' => array('url'),
    'settings' => array(
      'trim_length' => 80,
      'nofollow' => FALSE,
    ),
  );

  return $info;
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function url_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  $element['trim_length'] = array(
    '#type' => 'textfield',
    '#title' => t('Trim the link text to certain number of characters'),
    '#description' => t('To leave long link text alone, leave this blank.'),
    '#default_value' => $settings['trim_length'],
    '#size' => 10,
    '#element_validate' => array('element_validate_integer_positive'),
  );

  $element['nofollow'] = array(
    '#type' => 'checkbox',
    '#title' => t('Add rel="nofollow" to all links'),
    '#default_value' => $settings['nofollow'],
  );

  return $element;
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function url_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  $summary = array();

  if (!empty($settings['trim_length'])) {
    $summary[] = t('Link text trimmed to @char characters.', array('@char' => $settings['trim_length']));
  }
  else {
    $summary[] = t('Link text not trimmed.');
  }

  if (!empty($settings['nofollow'])) {
    $summary[] = t('Add rel="nofollow"');
  }

  return implode('<br />', $summary);
}

/**
 * Implements hook_field_formatter_prepare_view().
 */
function url_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  foreach ($entities as $id => $entity) {
    $settings = $displays[$id]['settings'];

    foreach ($items[$id] as $delta => &$item) {
      // If the formatter is configured to add rel="nofollow" to links, then
      // add them here.
      if (!empty($settings['nofollow'])) {
        $item['options']['attributes']['rel'] = 'nofollow';
      }
    }
  }
}

/**
 * Implements hook_field_formatter_view().
 */
function url_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
  $intance_settings = $instance['settings'];
  $settings = $display['settings'];

  foreach ($items as $delta => $item) {
    // By default use the full URL as the link title.
    $link_title = $item['value'];

    // If the title field value is available, use it for the link title.
    if (!empty($item['title'])) {
      // Unsanitizied token replacement here because $options['HTML'] is FALSE
      // by default in theme_link().
      $link_title = token_replace($item['title'], array($entity_type => $entity), array('sanitize' => FALSE, 'clear' => TRUE));
    }

    // Trim the link title to the desired length.
    if (!empty($settings['trim_length'])) {
      $link_title = truncate_utf8($link_title, $settings['trim_length'], FALSE, TRUE);
    }

    $element[$delta] = array(
      '#type' => 'link',
      '#title' => $link_title,
      '#href' => $item['path'],
      '#options' => $item['options'],
    );
  }

  return $element;
}

/**
 * Return the HTML title from an URL.
 *
 * @param string $url
 *   An URL to request with drupal_http_request().
 *
 * @return string
 *   The HTML title of the URL if found.
 */
function url_fetch_title($url) {
  $request = drupal_http_request($url);
  if (empty($request->error) && $request->code == 200 && !empty($request->data)) {
    if (preg_match('!<title>(.*?)</title>!iu', $request->data, $matches)) {
      // Title tags should be encoded, but we want the raw value.
      return decode_entities($matches[1]);
    }
  }
}

/**
 * This is a copy of the form_validate_url() function from Drupal 8.
 *
 * Note that #maxlength and #required is validated by _form_validate() already.
 */
function url_validate_url(&$element, &$form_state) {
  $value = trim($element['#value']);
  form_set_value($element, $value, $form_state);

  if ($value !== '' && !valid_url($value, TRUE)) {
    form_error($element, t('The URL %url is not valid.', array('%url' => $value)));
  }
}
