<?php

/**
 * @file
 * Defines simple link field types.
 */

define('LINK_EXTERNAL', 'external');
define('LINK_INTERNAL', 'internal');
define('LINK_FRONT', 'front');
define('LINK_EMAIL', 'email');
define('LINK_NEWS', 'news');
define('LINK_TARGET_DEFAULT', 'default');
define('LINK_TARGET_NEW_WINDOW', '_blank');
define('LINK_TARGET_TOP', '_top');
define('LINK_TARGET_USER', 'user');

/**
 * Maximum URLs length - needs to match value in link.install.
 */
define('LINK_URL_MAX_LENGTH', 2048);

/**
 * Implements hook_help().
 */
function link_help($path, $arg) {
  switch ($path) {
    case 'admin/help#link':
      $output = '<p><strong>About</strong></p>';
      $output .= '<p>' . 'The link provides a standard custom content field for links. Links can be easily added to any content types and profiles and include advanced validating and different ways of storing internal or external links and URLs. It also supports additional link text title, site wide tokens for titles and title attributes, target attributes, css class attribution, static repeating values, input conversion, and many more.' . '</p>';
      $output .= '<p>' . '<strong>Requirements / Dependencies</strong>' . '</p>';
      $output .= '<p>' . 'Fields API is provided already by core [no dependencies].' . '</p>';
      $output .= '<p><strong>Configuration</strong></p>';
      $output .= '<p>' . 'Configuration is only slightly more complicated than a text field. Link text titles for URLs can be made required, set as instead of URL, optional (default), or left out entirely. If no link text title is provided, the trimmed version of the complete URL will be displayed. The target attribute should be set to "_blank", "top", or left out completely (checkboxes provide info). The rel=nofollow attribute prevents the link from being followed by certain search engines.' . '</p>';
      return $output;
  }
}

/**
 * Implements hook_field_info().
 */
function link_field_info() {
  return array(
    'link_field' => array(
      'label' => t('Link'),
      'description' => t('Store a title, href, and attributes in the database to assemble a link.'),
      'settings' => array(
        'attributes' => _link_default_attributes(),
        'url' => 0,
        'title' => 'optional',
        'title_value' => '',
        'title_maxlength' => 128,
        'enable_tokens' => 1,
        'display' => array(
          'url_cutoff' => 80,
        ),
      ),
      'instance_settings' => array(
        'attributes' => _link_default_attributes(),
        'url' => 0,
        'title' => 'optional',
        'title_value' => '',
        'title_label_use_field_label' => FALSE,
        'title_maxlength' => 128,
        'enable_tokens' => 1,
        'display' => array(
          'url_cutoff' => 80,
        ),
        'validate_url' => 1,
        'absolute_url' => 1,
      ),
      'default_widget' => 'link_field',
      'default_formatter' => 'link_default',
      // Support hook_entity_property_info() from contrib "Entity API".
      'property_type' => 'field_item_link',
      'property_callbacks' => array('link_field_property_info_callback'),
    ),
  );
}

/**
 * Implements hook_field_instance_settings_form().
 */
function link_field_instance_settings_form($field, $instance) {
  $form = array(
    '#element_validate' => array('link_field_settings_form_validate'),
  );

  $form['absolute_url'] = array(
    '#type' => 'checkbox',
    '#title' => t('Absolute URL'),
    '#default_value' => isset($instance['settings']['absolute_url']) && ($instance['settings']['absolute_url'] !== '') ? $instance['settings']['absolute_url'] : TRUE,
    '#description' => t('If checked, the URL will always render as an absolute URL.'),
  );

  $form['validate_url'] = array(
    '#type' => 'checkbox',
    '#title' => t('Validate URL'),
    '#default_value' => isset($instance['settings']['validate_url']) && ($instance['settings']['validate_url'] !== '') ? $instance['settings']['validate_url'] : TRUE,
    '#description' => t('If checked, the URL field will be verified as a valid URL during validation.'),
  );

  $form['url'] = array(
    '#type' => 'checkbox',
    '#title' => t('Optional URL'),
    '#default_value' => isset($instance['settings']['url']) ? $instance['settings']['url'] : '',
    '#return_value' => 'optional',
    '#description' => t('If checked, the URL field is optional and submitting a title alone will be acceptable. If the URL is omitted, the title will be displayed as plain text.'),
  );

  $title_options = array(
    'optional' => t('Optional Title'),
    'required' => t('Required Title'),
    'value' => t('Static Title'),
    'select' => t('Selected Title'),
    'none' => t('No Title'),
  );

  $form['title'] = array(
    '#type' => 'radios',
    '#title' => t('Link Title'),
    '#default_value' => isset($instance['settings']['title']) ? $instance['settings']['title'] : 'optional',
    '#options' => $title_options,
    '#description' => t('If the link title is optional or required, a field will be displayed to the end user. If the link title is static, the link will always use the same title. If <a href="http://drupal.org/project/token">token module</a> is installed, the static title value may use any other entity field as its value. Static and token-based titles may include most inline XHTML tags such as <em>strong</em>, <em>em</em>, <em>img</em>, <em>span</em>, etc.'),
  );

  $form['title_value'] = array(
    '#type' => 'textfield',
    '#title' => t('Static or default title'),
    '#default_value' => isset($instance['settings']['title_value']) ? $instance['settings']['title_value'] : '',
    '#description' => t('This title will 1) always be used if "Static Title" is selected above, or 2) used if "Optional title" is selected above and no title is entered when creating content.'),
    '#states' => array(
      'visible' => array(
        ':input[name="instance[settings][title]"]' => array('value' => 'value'),
      ),
    ),
  );

  $form['title_allowed_values'] = array(
    '#type' => 'textarea',
    '#title' => t('Title allowed values'),
    '#default_value' => isset($instance['settings']['title_allowed_values']) ? $instance['settings']['title_allowed_values'] : '',
    '#description' => t('When using "Selected Title", you can allow users to select the title from a limited set of values (eg. Home, Office, Other). Enter here all possible values that title can take, one value per line.'),
    '#states' => array(
      'visible' => array(
        ':input[name="instance[settings][title]"]' => array('value' => 'select'),
      ),
    ),
  );

  $form['title_label_use_field_label'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use field label as the label for the title field'),
    '#default_value' => isset($instance['settings']['title_label_use_field_label']) ? $instance['settings']['title_label_use_field_label'] : FALSE,
    '#description' => t('If this is checked the field label will be hidden.'),
  );

  $form['title_maxlength'] = array(
    '#type' => 'textfield',
    '#title' => t('Max length of title field'),
    '#default_value' => isset($instance['settings']['title_maxlength']) ? $instance['settings']['title_maxlength'] : '128',
    '#description' => t('Set a maximum length on the title field (applies only if Link Title is optional or required).  The maximum limit is 255 characters.'),
    '#maxlength' => 3,
    '#size' => 3,
  );

  if (module_exists('token')) {
    // Add token module replacements fields.
    $form['enable_tokens'] = array(
      '#type' => 'checkbox',
      '#title' => t('Allow user-entered tokens'),
      '#default_value' => isset($instance['settings']['enable_tokens']) ? $instance['settings']['enable_tokens'] : 1,
      '#description' => t('Checking will allow users to enter tokens in URLs and Titles on the entity edit form. This does not affect the field settings on this page.'),
    );

    $entity_info = entity_get_info($instance['entity_type']);
    $form['tokens_help'] = array(
      '#theme' => 'token_tree',
      '#token_types' => array($entity_info['token type']),
      '#global_types' => TRUE,
      '#click_insert' => TRUE,
      '#dialog' => TRUE,
    );
  }

  $form['display'] = array(
    '#tree' => TRUE,
  );
  $form['display']['url_cutoff'] = array(
    '#type' => 'textfield',
    '#title' => t('URL Display Cutoff'),
    '#default_value' => isset($instance['settings']['display']['url_cutoff']) ? $instance['settings']['display']['url_cutoff'] : '80',
    '#description' => t('If the user does not include a title for this link, the URL will be used as the title. When should the link title be trimmed and finished with an elipsis (&hellip;)? Leave blank for no limit.'),
    '#maxlength' => 3,
    '#size' => 3,
  );

  $target_options = array(
    LINK_TARGET_DEFAULT => t('Default (no target attribute)'),
    LINK_TARGET_TOP => t('Open link in window root'),
    LINK_TARGET_NEW_WINDOW => t('Open link in new window'),
    LINK_TARGET_USER => t('Allow the user to choose'),
  );
  $form['attributes'] = array(
    '#tree' => TRUE,
  );
  $form['attributes']['target'] = array(
    '#type' => 'radios',
    '#title' => t('Link Target'),
    '#default_value' => empty($instance['settings']['attributes']['target']) ? LINK_TARGET_DEFAULT : $instance['settings']['attributes']['target'],
    '#options' => $target_options,
  );
  $form['attributes']['rel'] = array(
    '#type' => 'textfield',
    '#title' => t('Rel Attribute'),
    '#description' => t('When output, this link will have this rel attribute. The most common usage is <a href="http://en.wikipedia.org/wiki/Nofollow" target="blank">rel=&quot;nofollow&quot;</a> which prevents some search engines from spidering entered links.'),
    '#default_value' => empty($instance['settings']['attributes']['rel']) ? '' : $instance['settings']['attributes']['rel'],
    '#field_prefix' => 'rel = "',
    '#field_suffix' => '"',
    '#size' => 20,
  );
  $rel_remove_options = array(
    'default' => t('Keep rel as set up above (untouched/default)'),
    'rel_remove_external' => t('Remove rel if given link is external'),
    'rel_remove_internal' => t('Remove rel if given link is internal'),
  );
  $form['rel_remove'] = array(
    '#type' => 'radios',
    '#title' => t('Remove rel attribute automatically'),
    '#default_value' => !isset($instance['settings']['rel_remove']) ? 'default' : $instance['settings']['rel_remove'],
    '#description' => t('Turn on/off if rel attribute should be removed automatically, if user given link is internal/external'),
    '#options' => $rel_remove_options,
  );
  $form['attributes']['configurable_class'] = array(
    '#title' => t("Allow the user to enter a custom link class per link"),
    '#type' => 'checkbox',
    '#default_value' => empty($instance['settings']['attributes']['configurable_class']) ? '' : $instance['settings']['attributes']['configurable_class'],
  );
  $form['attributes']['class'] = array(
    '#type' => 'textfield',
    '#title' => t('Additional CSS Class'),
    '#description' => t('When output, this link will have this class attribute. Multiple classes should be separated by spaces. Only alphanumeric characters and hyphens are allowed'),
    '#default_value' => empty($instance['settings']['attributes']['class']) ? '' : $instance['settings']['attributes']['class'],
  );
  $form['attributes']['configurable_title'] = array(
    '#title' => t("Allow the user to enter a link 'title' attribute"),
    '#type' => 'checkbox',
    '#default_value' => empty($instance['settings']['attributes']['configurable_title']) ? '' : $instance['settings']['attributes']['configurable_title'],
  );
  $form['attributes']['title'] = array(
    '#title' => t("Default link 'title' Attribute"),
    '#type' => 'textfield',
    '#description' => t('When output, links will use this "title" attribute if the user does not provide one and when different from the link text. Read <a href="http://www.w3.org/TR/WCAG10-HTML-TECHS/#links" target="blank">WCAG 1.0 Guidelines</a> for links comformances. Tokens values will be evaluated.'),
    '#default_value' => empty($instance['settings']['attributes']['title']) ? '' : $instance['settings']['attributes']['title'],
    '#field_prefix' => 'title = "',
    '#field_suffix' => '"',
    '#size' => 20,
  );
  return $form;
}

/**
 * Form validate.
 *
 * #element_validate handler for link_field_instance_settings_form().
 */
function link_field_settings_form_validate($element, &$form_state, $complete_form) {
  if ($form_state['values']['instance']['settings']['title'] === 'value' && empty($form_state['values']['instance']['settings']['title_value'])) {
    form_set_error('instance][settings][title_value', t('A default title must be provided if the title is a static value.'));
  }
  if ($form_state['values']['instance']['settings']['title'] === 'select'
    && empty($form_state['values']['instance']['settings']['title_allowed_values'])) {
    form_set_error('instance][settings][title_allowed_values', t('You must enter one or more allowed values for link Title, the title is a selected value.'));
  }
  if (!empty($form_state['values']['instance']['settings']['display']['url_cutoff']) && !is_numeric($form_state['values']['instance']['settings']['display']['url_cutoff'])) {
    form_set_error('display', t('URL Display Cutoff value must be numeric.'));
  }
  if (empty($form_state['values']['instance']['settings']['title_maxlength'])) {
    form_set_value($element['title_maxlength'], '128', $form_state);
  }
  elseif (!is_numeric($form_state['values']['instance']['settings']['title_maxlength'])) {
    form_set_error('title_maxlength', t('The max length of the link title must be numeric.'));
  }
  elseif ($form_state['values']['instance']['settings']['title_maxlength'] > 255) {
    form_set_error('title_maxlength', t('The max length of the link title cannot be greater than 255 characters.'));
  }
}

/**
 * Implements hook_field_is_empty().
 */
function link_field_is_empty($item, $field) {
  return empty($item['title']) && empty($item['url']);
}

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

/**
 * Implements hook_field_validate().
 */
function link_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  $optional_field_found = FALSE;
  if ($instance['settings']['validate_url'] !== 0 || is_null($instance['settings']['validate_url']) || !isset($instance['settings']['validate_url'])) {
    foreach ($items as $delta => $value) {
      _link_validate($items[$delta], $delta, $field, $entity, $instance, $langcode, $optional_field_found, $errors);
    }
  }

  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] && !$optional_field_found) {
    $errors[$field['field_name']][$langcode][0][] = array(
      'error' => 'link_required',
      'message' => t('At least one title or URL must be entered.'),
      'error_element' => array('url' => FALSE, 'title' => TRUE),
    );
  }
}

/**
 * Implements hook_field_insert().
 */
function link_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $delta => $value) {
    _link_process($items[$delta], $delta, $field, $entity, $instance);
  }
}

/**
 * Implements hook_field_update().
 */
function link_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $delta => $value) {
    _link_process($items[$delta], $delta, $field, $entity, $instance);
  }
}

/**
 * Implements hook_field_prepare_view().
 */
function link_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  foreach ($items as $entity_id => $entity_items) {
    foreach ($entity_items as $delta => $value) {
      _link_sanitize($items[$entity_id][$delta], $delta, $field, $instances[$entity_id], $entities[$entity_id]);
    }
  }
}

/**
 * Implements hook_field_widget_info().
 */
function link_field_widget_info() {
  return array(
    'link_field' => array(
      'label' => 'Link',
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $element += array(
    '#type' => $instance['widget']['type'],
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
  );
  return $element;
}

/**
 * Implements hook_field_widget_error().
 */
function link_field_widget_error($element, $error, $form, &$form_state) {
  if (!empty($error['error_element']['title'])) {
    form_error($element['title'], $error['message']);
  }
  elseif (!empty($error['error_element']['url'])) {
    form_error($element['url'], $error['message']);
  }
}

/**
 * Unpacks the item attributes for use.
 */
function _link_load($field, $item, $instance) {
  if (isset($item['attributes'])) {
    if (!is_array($item['attributes'])) {
      $item['attributes'] = unserialize($item['attributes']);
    }
    return $item['attributes'];
  }
  elseif (isset($instance['settings']['attributes'])) {
    return $instance['settings']['attributes'];
  }
  else {
    return $field['settings']['attributes'];
  }
}

/**
 * Prepares the item attributes and url for storage.
 *
 * @param array $item
 *   Link field values.
 * @param array $delta
 *   The sequence number for current values.
 * @param array $field
 *   The field structure array.
 * @param object $entity
 *   Entity object.
 * @param array $instance
 *   The instance structure for $field on $entity's bundle.
 *
 * @codingStandardsIgnoreStart
 */
function _link_process(&$item, $delta, $field, $entity, $instance) {
  // @codingStandardsIgnoreEnd
  // Trim whitespace from URL.
  if (!empty($item['url'])) {
    $item['url'] = trim($item['url']);
  }

  // If no attributes are set then make sure $item['attributes'] is an empty
  // array, so $field['attributes'] can override it.
  if (empty($item['attributes'])) {
    $item['attributes'] = array();
  }

  // Serialize the attributes array.
  if (!is_string($item['attributes'])) {
    $item['attributes'] = serialize($item['attributes']);
  }

  // Don't save an invalid default value (e.g. 'http://').
  if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url']) && is_object($entity)) {
    $langcode = !empty($entity) ? field_language($instance['entity_type'], $entity, $instance['field_name']) : LANGUAGE_NONE;
    if (!link_validate_url($item['url'], $langcode)) {
      unset($item['url']);
    }
  }
}

/**
 * Validates that the link field has been entered properly.
 */
function _link_validate(&$item, $delta, $field, $entity, $instance, $langcode, &$optional_field_found, &$errors) {
  if ($item['url'] && !(isset($instance['default_value'][$delta]['url']) && $item['url'] === $instance['default_value'][$delta]['url'] && !$instance['required'])) {
    // Validate the link.
    if (!link_validate_url(trim($item['url']), $langcode)) {
      $errors[$field['field_name']][$langcode][$delta][] = array(
        'error' => 'link_required',
        'message' => t('The value %value provided for %field is not a valid URL.', array(
          '%value' => trim($item['url']),
          '%field' => $instance['label'],
        )),
        'error_element' => array('url' => TRUE, 'title' => FALSE),
      );
    }
    // Require a title for the link if necessary.
    if ($instance['settings']['title'] == 'required' && strlen(trim($item['title'])) == 0) {
      $errors[$field['field_name']][$langcode][$delta][] = array(
        'error' => 'link_required',
        'message' => t('Titles are required for all links.'),
        'error_element' => array('url' => FALSE, 'title' => TRUE),
      );
    }
  }
  // Require a link if we have a title.
  if ($instance['settings']['url'] !== 'optional' && strlen(isset($item['title']) ? $item['title'] : NULL) > 0 && strlen(trim($item['url'])) == 0) {
    $errors[$field['field_name']][$langcode][$delta][] = array(
      'error' => 'link_required',
      'message' => t('You cannot enter a title without a link url.'),
      'error_element' => array('url' => TRUE, 'title' => FALSE),
    );
  }
  // In a totally bizzaro case, where URLs and titles are optional but the field
  // is required, ensure there is at least one link.
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional'
    && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
    $optional_field_found = TRUE;
  }
  // Require entire field.
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] == 1 && !$optional_field_found && isset($instance['id'])) {
    $errors[$field['field_name']][$langcode][$delta][] = array(
      'error' => 'link_required',
      'message' => t('At least one title or URL must be entered.'),
      'error_element' => array('url' => FALSE, 'title' => TRUE),
    );
  }
}

/**
 * Clean up user-entered values for a link field according to field settings.
 *
 * @param array $item
 *   A single link item, usually containing url, title, and attributes.
 * @param int $delta
 *   The delta value if this field is one of multiple fields.
 * @param array $field
 *   The CCK field definition.
 * @param object $entity
 *   The entity containing this link.
 *
 * @codingStandardsIgnoreStart
 */
function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
  // @codingStandardsIgnoreEnd
  // Don't try to process empty links.
  if (empty($item['url']) && empty($item['title'])) {
    return;
  }
  if (empty($item['html'])) {
    $item['html'] = FALSE;
  }

  // Replace URL tokens.
  $entity_type = $instance['entity_type'];
  $entity_info = entity_get_info($entity_type);
  $property_id = $entity_info['entity keys']['id'];
  $entity_token_type = isset($entity_info['token type']) ? $entity_info['token type'] : (
  $entity_type == 'taxonomy_term' || $entity_type == 'taxonomy_vocabulary' ? str_replace('taxonomy_', '', $entity_type) : $entity_type
  );
  if (isset($instance['settings']['enable_tokens']) && $instance['settings']['enable_tokens']) {
    $text_tokens = token_scan($item['url']);
    if (!empty($text_tokens)) {
      // Load the entity if necessary for entities in views.
      if (isset($entity->{$property_id})) {
        $entity_loaded = entity_load($entity_type, array($entity->{$property_id}));
        $entity_loaded = array_pop($entity_loaded);
      }
      else {
        $entity_loaded = $entity;
      }
      $item['url'] = token_replace($item['url'], array($entity_token_type => $entity_loaded));
    }
  }

  $type = link_url_type($item['url']);
  // If the type of the URL cannot be determined and URL validation is disabled,
  // then assume LINK_EXTERNAL for later processing.
  if ($type == FALSE && $instance['settings']['validate_url'] === 0) {
    $type = LINK_EXTERNAL;
  }
  $url = link_cleanup_url($item['url']);
  $url_parts = _link_parse_url($url);

  if (!empty($url_parts['url'])) {
    $item['url'] = url($url_parts['url'],
      array(
        'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
        'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
        'absolute' => !empty($instance['settings']['absolute_url']),
        'html' => TRUE,
      )
    );
  }

  // Create a shortened URL for display.
  if ($type == LINK_EMAIL) {
    $display_url = str_replace('mailto:', '', $url);
  }
  else {
    $display_url = url($url_parts['url'],
      array(
        'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
        'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
        'absolute' => !empty($instance['settings']['absolute_url']),
      )
    );
  }
  if ($instance['settings']['display']['url_cutoff'] && strlen($display_url) > $instance['settings']['display']['url_cutoff']) {
    $display_url = substr($display_url, 0, $instance['settings']['display']['url_cutoff']) . "...";
  }
  $item['display_url'] = $display_url;

  // Use the title defined at the instance level.
  if ($instance['settings']['title'] == 'value' && strlen(trim($instance['settings']['title_value']))) {
    $title = $instance['settings']['title_value'];
    if (function_exists('i18n_string_translate')) {
      $i18n_string_name = "field:{$instance['field_name']}:{$instance['bundle']}:title_value";
      $title = i18n_string_translate($i18n_string_name, $title);
    }
  }
  // Use the title defined by the user at the widget level.
  elseif (drupal_strlen(trim($item['title']))) {
    $title = $item['title'];
  }
  // Use the static title if a user-defined title is optional and a static title
  // has been defined.
  elseif ($instance['settings']['title'] == 'optional' && drupal_strlen(trim($instance['settings']['title_value']))) {
    $title = $instance['settings']['title_value'];
  }
  else {
    $title = '';
  }

  // Replace title tokens.
  if ($title && $instance['settings']['enable_tokens']) {
    $text_tokens = token_scan($title);
    if (!empty($text_tokens)) {
      // Load the entity if necessary for entities in views.
      if (isset($entity->{$property_id})) {
        $entity_loaded = entity_load($entity_type, array($entity->{$property_id}));
        $entity_loaded = array_pop($entity_loaded);
      }
      else {
        $entity_loaded = $entity;
      }
      $title = token_replace($title, array($entity_token_type => $entity_loaded));
    }
  }
  if ($title && ($instance['settings']['title'] == 'value' || $instance['settings']['enable_tokens'])) {
    $title = filter_xss($title, array(
      'b',
      'br',
      'code',
      'em',
      'i',
      'img',
      'span',
      'strong',
      'sub',
      'sup',
      'tt',
      'u',
    ));
    $item['html'] = TRUE;
  }
  $item['title'] = empty($title) && $title !== '0' ? $item['display_url'] : $title;

  if (!isset($item['attributes'])) {
    $item['attributes'] = array();
  }

  // Unserialize attributtes array if it has not been unserialized yet.
  if (!is_array($item['attributes'])) {
    $item['attributes'] = (array) unserialize($item['attributes']);
  }

  // Add default attributes.
  if (!is_array($instance['settings']['attributes'])) {
    $instance['settings']['attributes'] = _link_default_attributes();
  }
  else {
    $instance['settings']['attributes'] += _link_default_attributes();
  }

  // Merge item attributes with attributes defined at the field level.
  $item['attributes'] += $instance['settings']['attributes'];

  // If user is not allowed to choose target attribute, use default defined at
  // field level.
  if ($instance['settings']['attributes']['target'] != LINK_TARGET_USER) {
    $item['attributes']['target'] = $instance['settings']['attributes']['target'];
  }
  elseif ($item['attributes']['target'] == LINK_TARGET_USER) {
    $item['attributes']['target'] = LINK_TARGET_DEFAULT;
  }

  // Remove the target attribute if the default (no target) is selected.
  if (empty($item['attributes']) || (isset($item['attributes']['target']) && $item['attributes']['target'] == LINK_TARGET_DEFAULT)) {
    unset($item['attributes']['target']);
  }

  // Remove rel attribute for internal or external links if selected.
  if (isset($item['attributes']['rel']) && isset($instance['settings']['rel_remove']) && $instance['settings']['rel_remove'] != 'default') {
    if (($instance['settings']['rel_remove'] != 'rel_remove_internal' && $type != LINK_INTERNAL) ||
      ($instance['settings']['rel_remove'] != 'rel_remove_external' && $type != LINK_EXTERNAL)) {
      unset($item['attributes']['rel']);
    }
  }

  // Handle "title" link attribute.
  if (!empty($item['attributes']['title']) && module_exists('token')) {
    $text_tokens = token_scan($item['attributes']['title']);
    if (!empty($text_tokens)) {
      // Load the entity (necessary for entities in views).
      if (isset($entity->{$property_id})) {
        $entity_loaded = entity_load($entity_type, array($entity->{$property_id}));
        $entity_loaded = array_pop($entity_loaded);
      }
      else {
        $entity_loaded = $entity;
      }
      $item['attributes']['title'] = token_replace($item['attributes']['title'], array($entity_token_type => $entity_loaded));
    }
    $item['attributes']['title'] = filter_xss($item['attributes']['title'], array(
      'b',
      'br',
      'code',
      'em',
      'i',
      'img',
      'span',
      'strong',
      'sub',
      'sup',
      'tt',
      'u',
    ));
  }
  // Handle attribute classes.
  if (!empty($item['attributes']['class'])) {
    $classes = explode(' ', $item['attributes']['class']);
    foreach ($classes as &$class) {
      $class = drupal_clean_css_identifier($class);
    }
    $item['attributes']['class'] = implode(' ', $classes);
  }
  unset($item['attributes']['configurable_class']);

  // Remove title attribute if it's equal to link text.
  if (isset($item['attributes']['title']) && $item['attributes']['title'] == $item['title']) {
    unset($item['attributes']['title']);
  }
  unset($item['attributes']['configurable_title']);

  // Remove empty attributes.
  $item['attributes'] = array_filter($item['attributes']);
}

/**
 * Because parse_url doesn't work with relative urls.
 *
 * @param string $url
 *   URL to parse.
 *
 * @return array
 *   Array of url pieces - only 'url', 'query', and 'fragment'.
 */
function _link_parse_url($url) {
  $url_parts = array();
  // Separate out the anchor, if any.
  if (strpos($url, '#') !== FALSE) {
    $url_parts['fragment'] = substr($url, strpos($url, '#') + 1);
    $url = substr($url, 0, strpos($url, '#'));
  }
  // Separate out the query string, if any.
  if (strpos($url, '?') !== FALSE) {
    $query = substr($url, strpos($url, '?') + 1);
    $url_parts['query'] = _link_parse_str($query);
    $url = substr($url, 0, strpos($url, '?'));
  }
  $url_parts['url'] = $url;
  return $url_parts;
}

/**
 * Replaces the PHP parse_str() function.
 *
 * Because parse_str replaces the following characters in query parameters name
 * in order to maintain compatibility with deprecated register_globals
 * directive:
 *
 *   - chr(32) ( ) (space)
 *   - chr(46) (.) (dot)
 *   - chr(91) ([) (open square bracket)
 *   - chr(128) - chr(159) (various)
 *
 * @param string $query
 *   Query to parse.
 *
 * @return array
 *   Array of query parameters.
 *
 * @see http://php.net/manual/en/language.variables.external.php#81080
 */
function _link_parse_str($query) {
  $query_array = array();

  $pairs = explode('&', $query);
  foreach ($pairs as $pair) {
    $name_value = explode('=', $pair, 2);
    $name = urldecode($name_value[0]);
    $value = isset($name_value[1]) ? urldecode($name_value[1]) : NULL;
    $query_array[$name] = $value;
  }

  return $query_array;
}

/**
 * Implements hook_theme().
 */
function link_theme() {
  return array(
    'link_formatter_link_default' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_plain' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_host' => array(
      'variables' => array('element' => NULL),
    ),
    'link_formatter_link_absolute' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_domain' => array(
      'variables' => array(
        'element' => NULL,
        'display' => NULL,
        'field' => NULL,
      ),
    ),
    'link_formatter_link_no_protocol' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_title_plain' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_url' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_short' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_label' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_formatter_link_separate' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
    'link_field' => array(
      'render element' => 'element',
    ),
  );
}

/**
 * Formats a link field widget.
 */
function theme_link_field($vars) {
  drupal_add_css(drupal_get_path('module', 'link') . '/link.css');
  $element = $vars['element'];
  // Prefix single value link fields with the name of the field.
  if (empty($element['#field']['multiple'])) {
    if (isset($element['url']) && !isset($element['title'])) {
      $element['url']['#title_display'] = 'invisible';
    }
  }

  $output = '';
  $output .= '<div class="link-field-subrow clearfix">';
  if (isset($element['title'])) {
    $output .= '<div class="link-field-title link-field-column">' . drupal_render($element['title']) . '</div>';
  }
  $output .= '<div class="link-field-url' . (isset($element['title']) ? ' link-field-column' : '') . '">' . drupal_render($element['url']) . '</div>';
  $output .= '</div>';
  if (!empty($element['attributes']['target'])) {
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['target']) . '</div>';
  }
  if (!empty($element['attributes']['title'])) {
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['title']) . '</div>';
  }
  if (!empty($element['attributes']['class'])) {
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['class']) . '</div>';
  }
  $output .= drupal_render_children($element);
  return $output;
}

/**
 * Implements hook_element_info().
 */
function link_element_info() {
  $elements = array();
  $elements['link_field'] = array(
    '#input' => TRUE,
    '#process' => array('link_field_process'),
    '#theme' => 'link_field',
    '#theme_wrappers' => array('form_element'),
  );
  return $elements;
}

/**
 * Returns the default attributes and their values.
 */
function _link_default_attributes() {
  return array(
    'target' => LINK_TARGET_DEFAULT,
    'class' => '',
    'rel' => '',
  );
}

/**
 * Processes the link type element before displaying the field.
 *
 * Build the form element. When creating a form using FAPI #process,
 * note that $element['#value'] is already set.
 *
 * The $fields array is in
 * $complete_form['#field_info'][$element['#field_name']].
 */
function link_field_process($element, $form_state, $complete_form) {
  $instance = field_widget_instance($element, $form_state);
  $settings = $instance['settings'];
  $element['url'] = array(
    '#type' => 'textfield',
    '#maxlength' => LINK_URL_MAX_LENGTH,
    '#title' => t('URL'),
    '#required' => ($element['#delta'] == 0 && $settings['url'] !== 'optional') ? $element['#required'] : FALSE,
    '#default_value' => isset($element['#value']['url']) ? $element['#value']['url'] : NULL,
  );
  if (in_array($settings['title'], array('optional', 'required'))) {
    // Figure out the label of the title field.
    if (!empty($settings['title_label_use_field_label'])) {
      // Use the element label as the title field label.
      $title_label = $element['#title'];
      // Hide the field label because there is no need for the duplicate labels.
      $element['#title_display'] = 'invisible';
    }
    else {
      $title_label = t('Title');
    }

    // Default value.
    $title_maxlength = 128;
    if (!empty($settings['title_maxlength'])) {
      $title_maxlength = $settings['title_maxlength'];
    }

    $element['title'] = array(
      '#type' => 'textfield',
      '#maxlength' => $title_maxlength,
      '#title' => $title_label,
      '#description' => t('The link title is limited to @maxlength characters maximum.', array('@maxlength' => $title_maxlength)),
      '#required' => ($settings['title'] == 'required' && (($element['#delta'] == 0 && $element['#required']) || !empty($element['#value']['url']))) ? TRUE : FALSE,
      '#default_value' => isset($element['#value']['title']) ? $element['#value']['title'] : NULL,
    );
  }
  elseif ($settings['title'] == 'select') {
    $options = drupal_map_assoc(array_filter(explode("\n", str_replace("\r", "\n", trim($settings['title_allowed_values'])))));
    $element['title'] = array(
      '#type' => 'select',
      '#title' => t('Title'),
      '#description' => t('Select the a title for this link.'),
      '#default_value' => isset($element['#value']['title']) ? $element['#value']['title'] : NULL,
      '#options' => $options,
    );
  }

  // Initialize field attributes as an array if it is not an array yet.
  if (!is_array($settings['attributes'])) {
    $settings['attributes'] = array();
  }
  // Add default attributes.
  $settings['attributes'] += _link_default_attributes();
  $attributes = isset($element['#value']['attributes']) ? $element['#value']['attributes'] : $settings['attributes'];
  if (!empty($settings['attributes']['target']) && $settings['attributes']['target'] == LINK_TARGET_USER) {
    $element['attributes']['target'] = array(
      '#type' => 'checkbox',
      '#title' => t('Open URL in a New Window'),
      '#return_value' => LINK_TARGET_NEW_WINDOW,
      '#default_value' => isset($attributes['target']) ? $attributes['target'] : FALSE,
    );
  }
  if (!empty($settings['attributes']['configurable_title']) && $settings['attributes']['configurable_title'] == 1) {
    $element['attributes']['title'] = array(
      '#type' => 'textfield',
      '#title' => t('Link "title" attribute'),
      '#default_value' => isset($attributes['title']) ? $attributes['title'] : '',
      '#field_prefix' => 'title = "',
      '#field_suffix' => '"',
    );
  }
  if (!empty($settings['attributes']['configurable_class']) && $settings['attributes']['configurable_class'] == 1) {
    $element['attributes']['class'] = array(
      '#type' => 'textfield',
      '#title' => t('Custom link class'),
      '#default_value' => isset($attributes['class']) ? $attributes['class'] : '',
      '#field_prefix' => 'class = "',
      '#field_suffix' => '"',
    );
  }

  // If the title field is available or there are field accepts multiple values
  // then allow the individual field items display the required asterisk if
  // needed.
  if (isset($element['title']) || isset($element['_weight'])) {
    // To prevent an extra required indicator, disable the required flag on the
    // base element since all the sub-fields are already required if desired.
    $element['#required'] = FALSE;
  }

  return $element;
}

/**
 * Implements hook_field_formatter_info().
 */
function link_field_formatter_info() {
  return array(
    'link_default' => array(
      'label' => t('Title, as link (default)'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_title_plain' => array(
      'label' => t('Title, as plain text'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_host' => array(
      'label' => t('Host, as plain text'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_url' => array(
      'label' => t('URL, as link'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_plain' => array(
      'label' => t('URL, as plain text'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_absolute' => array(
      'label' => t('URL, absolute'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_domain' => array(
      'label' => t('Domain, as link'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
      'settings' => array(
        'strip_www' => FALSE,
      ),
    ),
    'link_no_protocol' => array(
      'label' => t('URL with the protocol removed'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_short' => array(
      'label' => t('Short, as link with title "Link"'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_label' => array(
      'label' => t('Label, as link with label as title'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
    'link_separate' => array(
      'label' => t('Separate title and URL'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function link_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();
  if ($display['type'] == 'link_domain') {
    $element['strip_www'] = array(
      '#title' => t('Strip www. from domain'),
      '#type' => 'checkbox',
      '#default_value' => $settings['strip_www'],
    );
  }
  return $element;
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function link_field_formatter_settings_summary($field, $instance, $view_mode) {

  $display = $instance['display'][$view_mode];

  if ($display['type'] == 'link_domain') {
    if ($display['settings']['strip_www']) {
      return t('Strip www. from domain');
    }
    else {
      return t('Leave www. in domain');
    }
  }
  return '';
}

/**
 * Implements hook_field_formatter_view().
 */
function link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $elements = array();
  foreach ($items as $delta => $item) {
    $elements[$delta] = array(
      '#theme' => 'link_formatter_' . $display['type'],
      '#element' => $item,
      '#field' => $instance,
      '#display' => $display,
    );
  }
  return $elements;
}

/**
 * Formats a link.
 */
function theme_link_formatter_link_default($vars) {
  $link_options = $vars['element'];
  unset($link_options['title']);
  unset($link_options['url']);

  if (isset($link_options['attributes']['class'])) {
    $link_options['attributes']['class'] = array($link_options['attributes']['class']);
  }
  // Display a normal link if both title and URL are available.
  if (!empty($vars['element']['title']) && !empty($vars['element']['url'])) {
    return l($vars['element']['title'], $vars['element']['url'], $link_options);
  }
  // If only a title, display the title.
  elseif (!empty($vars['element']['title'])) {
    return !empty($link_options['html']) ? $vars['element']['title'] : check_plain($vars['element']['title']);
  }
  elseif (!empty($vars['element']['url'])) {
    return l($vars['element']['title'], $vars['element']['url'], $link_options);
  }
}

/**
 * Formats a link (or its title) as plain text.
 */
function theme_link_formatter_link_plain($vars) {
  $link_options = $vars['element'];
  if (isset($link_options['title'])) {
    unset($link_options['title']);
  }
  else {
    $vars['element']['title'] = '';
  }
  unset($link_options['url']);
  return empty($vars['element']['url']) ? check_plain($vars['element']['title']) : url($vars['element']['url'], $link_options);
}

/**
 * Theme function for 'host' text field formatter.
 */
function theme_link_formatter_link_host($vars) {
  $host = @parse_url($vars['element']['url']);
  return isset($host['host']) ? check_plain($host['host']) : '';
}

/**
 * Formats a link as an absolute URL.
 */
function theme_link_formatter_link_absolute($vars) {
  $absolute = array('absolute' => TRUE);
  return empty($vars['element']['url']) ? '' : url($vars['element']['url'], $absolute + $vars['element']);
}

/**
 * Formats a link using the URL's domain for it's link text.
 */
function theme_link_formatter_link_domain($vars) {
  $link_options = $vars['element'];
  unset($link_options['title']);
  unset($link_options['url']);
  $domain = parse_url($vars['element']['display_url'], PHP_URL_HOST);
  if (!empty($vars['display']['settings']['strip_www'])) {
    $domain = str_replace('www.', '', $domain);
  }
  return $vars['element']['url'] ? l($domain, $vars['element']['url'], $link_options) : '';
}

/**
 * Formats a link without the http:// or https://.
 */
function theme_link_formatter_link_no_protocol($vars) {
  $link_options = $vars['element'];
  unset($link_options['title']);
  unset($link_options['url']);
  // We drop any scheme of the url.
  $scheme = parse_url($vars['element']['url']);
  $search = '/' . preg_quote($scheme['scheme'] . '://', '/') . '/';
  $replace = '';
  $display_url = preg_replace($search, $replace, $vars['element']['url'], 1);

  return $vars['element']['url'] ? l($display_url, $vars['element']['url'], $link_options) : '';
}

/**
 * Formats a link's title as plain text.
 */
function theme_link_formatter_link_title_plain($vars) {
  return empty($vars['element']['title']) ? '' : check_plain(decode_entities($vars['element']['title']));
}

/**
 * Formats a link using an alternate display URL for its link text.
 */
function theme_link_formatter_link_url($vars) {
  $link_options = $vars['element'];
  unset($link_options['title']);
  unset($link_options['url']);
  return $vars['element']['url'] ? l($vars['element']['display_url'], $vars['element']['url'], $link_options) : '';
}

/**
 * Formats a link using "Link" as the link text.
 */
function theme_link_formatter_link_short($vars) {
  $link_options = $vars['element'];
  unset($link_options['title']);
  unset($link_options['url']);
  return $vars['element']['url'] ? l(t('Link'), $vars['element']['url'], $link_options) : '';
}

/**
 * Formats a link using the field's label as link text.
 */
function theme_link_formatter_link_label($vars) {
  $link_options = $vars['element'];
  unset($link_options['title']);
  unset($link_options['url']);
  return $vars['element']['url'] ? l($vars['field']['label'], $vars['element']['url'], $link_options) : '';
}

/**
 * Formats a link as separate title and URL elements.
 */
function theme_link_formatter_link_separate($vars) {
  $class = empty($vars['element']['attributes']['class']) ? '' : ' ' . $vars['element']['attributes']['class'];
  unset($vars['element']['attributes']['class']);
  $link_options = $vars['element'];
  unset($link_options['title']);
  unset($link_options['url']);
  $title = empty($vars['element']['title']) ? '' : check_plain($vars['element']['title']);

  // @TODO static html markup looks not very elegant
  // needs smarter output solution and an optional title/url seperator
  $url_parts = _link_parse_url($vars['element']['url']);
  $output = '';
  $output .= '<div class="link-item ' . $class . '">';
  if (!empty($title)) {
    $output .= '<div class="link-title">' . $title . '</div>';
  }
  $output .= '<div class="link-url">' . l($url_parts['url'], $vars['element']['url'], $link_options) . '</div>';
  $output .= '</div>';
  return $output;
}

/**
 * Implements hook_token_list().
 *
 * @TODO: hook_token_list no longer exists - this should change to
 *   hook_token_info().
 */
function link_token_list($type = 'all') {
  if ($type === 'field' || $type === 'all') {
    $tokens = array();
    $tokens['link']['url'] = t("Link URL");
    $tokens['link']['title'] = t("Link title");
    $tokens['link']['view'] = t("Formatted html link");
    return $tokens;
  }
}

/**
 * Implements hook_token_values().
 *
 * @TODO: hook_token_values no longer exists - this should change to
 *   hook_tokens().
 */
function link_token_values($type, $object = NULL) {
  if ($type === 'field') {
    $item = $object[0];

    $tokens['url'] = $item['url'];
    $tokens['title'] = $item['title'];
    $tokens['view'] = isset($item['view']) ? $item['view'] : '';

    return $tokens;
  }
}

/**
 * Implements hook_views_api().
 */
function link_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'link') . '/views',
  );
}

/**
 * Forms a valid URL if possible from an entered address.
 *
 * Trims whitespace and automatically adds an http:// to addresses without a
 * protocol specified.
 *
 * @param string $url
 *   The url entered by the user.
 * @param string $protocol
 *   The protocol to be prepended to the url if one is not specified.
 */
function link_cleanup_url($url, $protocol = 'http') {
  $url = trim($url);
  $type = link_url_type($url);

  if ($type === LINK_EXTERNAL) {
    // Check if there is no protocol specified.
    $protocol_match = preg_match("/^([a-z0-9][a-z0-9\.\-_]*:\/\/)/i", $url);
    if (empty($protocol_match)) {
      // But should there be? Add an automatic http:// if it starts with a
      // domain name.
      $link_domains = _link_domains();
      $domain_match = preg_match('/^(([a-z0-9]([a-z0-9\-_]*\.)+)(' . $link_domains . '|[a-z]{2}))/i', $url);
      if (!empty($domain_match)) {
        $url = $protocol . "://" . $url;
      }
    }
  }

  return $url;
}

/**
 * Validates a URL.
 *
 * @param string $text
 *   Url to be validated.
 * @param string $langcode
 *   An optional language code to look up the path in.
 *
 * @return bool
 *   True if a valid link, FALSE otherwise.
 */
function link_validate_url($text, $langcode = NULL) {

  $text = _link_clean_relative($text);
  $text = link_cleanup_url($text);
  $type = link_url_type($text);

  if ($type && ($type == LINK_INTERNAL || $type == LINK_EXTERNAL)) {
    $flag = valid_url($text, TRUE);
    if (!$flag) {
      $normal_path = drupal_get_normal_path($text, $langcode);
      $parsed_link = parse_url($normal_path, PHP_URL_PATH);
      if ($normal_path != $parsed_link) {
        $normal_path = $parsed_link;
      }
      $flag = drupal_valid_path($normal_path);
    }
    if (!$flag) {
      $flag = file_exists($normal_path);
    }
    if (!$flag) {
      $uri = file_build_uri($normal_path);
      $flag = file_exists($uri);
    }
  }
  else {
    $flag = (bool) $type;
  }

  return $flag;
}

/**
 * Cleaner of relatives urls.
 *
 * @param string $url
 *   The url to clean up the relative protocol.
 */
function _link_clean_relative($url) {
  $check = substr($url, 0, 2);
  if (isset($_SERVER['HTTPS']) &&
    ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) ||
    isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
    $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
    $protocol = 'https://';
  }
  else {
    $protocol = 'http://';
  }

  if ($check == '//') {
    $url = str_replace('//', $protocol, $url);
  }

  return $url;
}

/**
 * Type check a URL.
 *
 * Accepts all URLs following RFC 1738 standard for URL formation and all e-mail
 * addresses following the RFC 2368 standard for mailto address formation.
 *
 * @param string $text
 *   Url to be checked.
 *
 * @return mixed
 *   Returns boolean FALSE if the URL is not valid. On success, returns one of
 *   the LINK_(linktype) constants.
 */
function link_url_type($text) {
  // @TODO Complete letters.
  // @codingStandardsIgnoreStart
  $link_ichars_domain = (string) html_entity_decode(implode("", array(
    "&#x00BF;", // ¿
    "&#x00C0;", // À
    "&#x00C1;", // Á
    "&#x00C2;", // Â
    "&#x00C3;", // Ã
    "&#x00C4;", // Ä
    "&#x00C5;", // Å
    "&#x00C6;", // Æ
    "&#x00C7;", // Ç
    "&#x00C8;", // È
    "&#x00C9;", // É
    "&#x00CA;", // Ê
    "&#x00CB;", // Ë
    "&#x00CC;", // Ì
    "&#x00CD;", // Í
    "&#x00CE;", // Î
    "&#x00CF;", // Ï
    "&#x00D0;", // Ð
    "&#x00D1;", // Ñ
    "&#x00D2;", // Ò
    "&#x00D3;", // Ó
    "&#x00D4;", // Ô
    "&#x00D5;", // Õ
    "&#x00D6;", // Ö
    // ×
    "&#x00D8;", // Ø
    "&#x00D9;", // Ù
    "&#x00DA;", // Ú
    "&#x00DB;", // Û
    "&#x00DC;", // Ü
    "&#x00DD;", // Ý
    "&#x00DE;", // Þ
    // ß (see LINK_ICHARS)
    "&#x00E0;", // à
    "&#x00E1;", // á
    "&#x00E2;", // â
    "&#x00E3;", // ã
    "&#x00E4;", // ä
    "&#x00E5;", // å
    "&#x00E6;", // æ
    "&#x00E7;", // ç
    "&#x00E8;", // è
    "&#x00E9;", // é
    "&#x00EA;", // ê
    "&#x00EB;", // ë
    "&#x00EC;", // ì
    "&#x00ED;", // í
    "&#x00EE;", // î
    "&#x00EF;", // ï
    "&#x00F0;", // ð
    "&#x00F1;", // ñ
    "&#x00F2;", // ò
    "&#x00F3;", // ó
    "&#x00F4;", // ô
    "&#x00F5;", // õ
    "&#x00F6;", // ö
    // ÷
    "&#x00F8;", // ø
    "&#x00F9;", // ù
    "&#x00FA;", // ú
    "&#x00FB;", // û
    "&#x00FC;", // ü
    "&#x00FD;", // ý
    "&#x00FE;", // þ
    "&#x00FF;", // ÿ
    "&#x0152;", // Œ
    "&#x0153;", // œ
    "&#x0178;", // Ÿ
  )), ENT_QUOTES, 'UTF-8');
  // @codingStandardsIgnoreEnd

  $link_ichars = $link_ichars_domain . (string) html_entity_decode(implode("", array(
      // ß.
      "&#x00DF;",
    )), ENT_QUOTES, 'UTF-8');
  $allowed_protocols = variable_get('filter_allowed_protocols', array(
    'http',
    'https',
    'ftp',
    'news',
    'nntp',
    'telnet',
    'mailto',
    'irc',
    'ssh',
    'sftp',
    'webcal',
  ));
  $link_domains = _link_domains();

  // Starting a parenthesis group with (?: means that it is grouped, but is not
  // captured.
  $protocol = '((?:' . implode("|", $allowed_protocols) . '):\/\/)';
  $authentication = "(?:(?:(?:[\w\.\-\+!$&'\(\)*\+,;=" . $link_ichars . "]|%[0-9a-f]{2})+(?::(?:[\w" . $link_ichars . "\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})*)?)?@)";
  $domain = '(?:(?:[a-z0-9' . $link_ichars_domain . ']([a-z0-9' . $link_ichars_domain . '\-_\[\]])*)(\.(([a-z0-9' . $link_ichars_domain . '\-_\[\]])+\.)*(' . $link_domains . '|[a-z]{2}))?)';
  $ipv4 = '(?:[0-9]{1,3}(\.[0-9]{1,3}){3})';
  $ipv6 = '(?:[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})';
  $port = '(?::([0-9]{1,5}))';
  // Pattern specific to external links.
  $external_pattern = '/^' . $protocol . '?' . $authentication . '?(' . $domain . '|' . $ipv4 . '|' . $ipv6 . ' |localhost)' . $port . '?';

  // Pattern specific to internal links.
  $internal_pattern = "/^(?:[a-z0-9" . $link_ichars . "_\-+\[\] ]+)";
  $internal_pattern_file = "/^(?:[a-z0-9" . $link_ichars . "_\-+\[\]\. \/\(\)][a-z0-9" . $link_ichars . "_\-+\[\]\. \(\)][a-z0-9" . $link_ichars . "_\-+\[\]\. \/\(\)]+)$/i";

  $directories = "(?:\/[a-z0-9" . $link_ichars . "_\-\.~+%=&,$'#!():;*@\[\]]*)*";
  // Yes, four backslashes == a single backslash.
  $query = "(?:\/?\?([?a-z0-9" . $link_ichars . "+_|\-\.~\/\\\\%=&,$'!():;*@\[\]{} ]*))";
  $anchor = "(?:#[a-z0-9" . $link_ichars . "_\-\.~+%=&,$'():;*@\[\]\/\?]*)";

  // The rest of the path for a standard URL.
  // @codingStandardsIgnoreLine
  $end = $directories . '?' . $query . '?' . $anchor . '?' . '$/i';

  $message_id = '[^@].*@' . $domain;
  $newsgroup_name = '(?:[0-9a-z+-]*\.)*[0-9a-z+-]*';
  $news_pattern = '/^news:(' . $newsgroup_name . '|' . $message_id . ')$/i';

  $user = '[a-zA-Z0-9' . $link_ichars . '_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'\[\]]+';
  $email_pattern = '/^mailto:' . $user . '@' . '(?:' . $domain . '|' . $ipv4 . '|' . $ipv6 . '|localhost)' . $query . '?$/';

  if (strpos($text, '<front>') === 0) {
    return LINK_FRONT;
  }
  if (in_array('mailto', $allowed_protocols) && preg_match($email_pattern, $text)) {
    return LINK_EMAIL;
  }
  if (in_array('news', $allowed_protocols) && preg_match($news_pattern, $text)) {
    return LINK_NEWS;
  }
  if (preg_match($internal_pattern . $end, $text)) {
    return LINK_INTERNAL;
  }
  if (drupal_valid_path($text) && url_is_external($text) == FALSE) {
    return LINK_INTERNAL;
  }
  if (preg_match($external_pattern . $end, $text)) {
    return LINK_EXTERNAL;
  }
  if (preg_match($internal_pattern_file, $text)) {
    return LINK_INTERNAL;
  }

  return FALSE;
}

/**
 * Returns the list of allowed domains.
 *
 * If the variable link_allowed_domains is set, restrict allowed domains to the
 * strings in that array. If the variable link_allowed_domains is not set, allow
 * all domains between 2 and 63 characters in length.
 * See https://tools.ietf.org/html/rfc1034.
 */
function _link_domains() {
  $link_allowed_domains = variable_get('link_allowed_domains', array());
  return empty($link_allowed_domains) ? '[a-z][a-z0-9-]{1,62}' : implode('|', $link_allowed_domains);
}

/**
 * Implements hook_migrate_field_alter().
 */
function link_content_migrate_field_alter(&$field_value, $instance_value) {
  if ($field_value['type'] == 'link') {
    // Adjust the field type.
    $field_value['type'] = 'link_field';
    // Remove settings that are now on the instance.
    foreach (array(
               'attributes',
               'display',
               'url',
               'title',
               'title_value',
               'enable_tokens',
               'validate_url',
             ) as $setting) {
      unset($field_value['settings'][$setting]);
    }
  }
}

/**
 * Implements hook_migrate_instance_alter().
 *
 * Widget type also changed to link_field.
 */
function link_content_migrate_instance_alter(&$instance_value, $field_value) {
  if ($field_value['type'] == 'link') {
    // Grab settings that were previously on the field.
    foreach (array(
               'attributes',
               'display',
               'url',
               'title',
               'title_value',
               'enable_tokens',
               'validate_url',
             ) as $setting) {
      if (isset($field_value['settings'][$setting])) {
        $instance_value['settings'][$setting] = $field_value['settings'][$setting];
      }
    }
    // Adjust widget type.
    if ($instance_value['widget']['type'] == 'link') {
      $instance_value['widget']['type'] = 'link_field';
    }
    // Adjust formatter types.
    foreach ($instance_value['display'] as $context => $settings) {
      if (in_array($settings['type'], array(
        'default',
        'title_plain',
        'url',
        'plain',
        'short',
        'label',
        'separate',
      ))) {
        $instance_value['display'][$context]['type'] = 'link_' . $settings['type'];
      }
    }
  }
}

/**
 * Implements hook_field_settings_form().
 */
function link_field_settings_form() {
  return array();
}

/**
 * Additional callback to adapt the property info of link fields.
 *
 * @see entity_metadata_field_entity_property_info()
 */
function link_field_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  // Define a data structure so it's possible to deal with both the link title
  // and URL.
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';

  // Auto-create the field item as soon as a property is set.
  $property['auto creation'] = 'link_field_item_create';

  $property['property info'] = link_field_item_property_info();
  $property['property info']['url']['required'] = !$instance['settings']['url'];
  $property['property info']['title']['required'] = ($instance['settings']['title'] == 'required');
  if ($instance['settings']['title'] == 'none') {
    unset($property['property info']['title']);
  }
  unset($property['query callback']);
}

/**
 * Callback for creating a new, empty link field item.
 *
 * @see link_field_property_info_callback()
 */
function link_field_item_create() {
  return array('title' => NULL, 'url' => NULL, 'display_url' => NULL);
}

/**
 * Defines info for the properties of the link-field item data structure.
 */
function link_field_item_property_info() {
  $properties['title'] = array(
    'type' => 'text',
    'label' => t('The title of the link.'),
    'setter callback' => 'entity_property_verbatim_set',
  );
  $properties['url'] = array(
    'type' => 'text',
    'label' => t('The URL of the link.'),
    'setter callback' => 'entity_property_verbatim_set',
  );
  $properties['attributes'] = array(
    'type' => 'struct',
    'label' => t('The attributes of the link.'),
    'setter callback' => 'entity_property_verbatim_set',
    'getter callback' => 'link_attribute_property_get',
  );
  $properties['display_url'] = array(
    'type' => 'uri',
    'label' => t('The full URL of the link.'),
    'setter callback' => 'entity_property_verbatim_set',
  );
  return $properties;
}

/**
 * Entity property info getter callback for link attributes.
 */
function link_attribute_property_get($data, array $options, $name, $type, $info) {
  return isset($data[$name]) ? array_filter($data[$name]) : array();
}

/**
 * Implements hook_field_update_instance().
 */
function link_field_update_instance($instance, $prior_instance) {
  if (function_exists('i18n_string_update') && isset($instance['widget']) && $instance['widget']['type'] == 'link_field' && $prior_instance['settings']['title_value'] != $instance['settings']['title_value']) {
    $i18n_string_name = "field:{$instance['field_name']}:{$instance['bundle']}:title_value";
    i18n_string_update($i18n_string_name, $instance['settings']['title_value']);
  }
}

/**
 * Implements hook_i18n_string_list_TEXTGROUP_alter().
 */
function link_i18n_string_list_field_alter(&$strings, $type = NULL, $object = NULL) {
  if ($type != 'field_instance' || !is_array($object) || !isset($object['widget']['type'])) {
    return;
  }
  if ($object['widget']['type'] == 'link_field' && isset($object['settings']['title_value'])) {
    $strings['field'][$object['field_name']][$object['bundle']]['title_value']['string'] = $object['settings']['title_value'];
  }
}
