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'), '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 token module 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 strong, em, img, span, etc.'), ); $form['title_value'] = array( '#type' => 'textfield', '#title' => t('Static title'), '#default_value' => isset($instance['settings']['title_value']) ? $instance['settings']['title_value'] : '', '#description' => t('This title will always be used if “Static Title” is selected above.'), ); $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 (…)? 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 rel="nofollow" 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.'), '#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 WCAG 1.0 Guidelines 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; } /** * #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('title_value', t('A default title must be provided if the title is a static 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); } } /** * 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); } } /** * 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 ($error['error_element']['title']) { form_error($element['title'], $error['message']); } elseif ($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. */ function _link_process(&$item, $delta, $field, $entity) { // 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)) { if (!link_validate_url($item['url'])) { 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'])) == FALSE) { $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. */ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) { // 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']) { global $user; // 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_validate_url($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_parts['url']; $item += 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 (isset($item['title'])) { $title = $item['title']; } else { $title = ''; } // Replace title tokens. if ($title && ($instance['settings']['title'] == 'value' || $instance['settings']['enable_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)); $title = filter_xss($title, array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u')); $item['html'] = TRUE; } $item['title'] = empty($title) ? $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')) { // 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_html_class($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 compability 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_absolute' => array( 'variables' => array('element' => NULL, 'field' => NULL), ), 'link_formatter_link_domain' => array( 'variables' => array('element' => NULL, 'display' => 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 .= '