6 * Defines simple link field types.
9 define('LINK_EXTERNAL', 'external');
10 define('LINK_INTERNAL', 'internal');
11 define('LINK_FRONT', 'front');
12 define('LINK_EMAIL', 'email');
13 define('LINK_DOMAINS', 'aero|arpa|biz|com|cat|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|mobi');
16 * Implementation of hook_field_info().
18 function link_field_info() {
22 'description' => t('Store a title, href, and attributes in the database to assemble a link.'),
28 * Implementation of hook_field_settings().
30 function link_field_settings($op, $field) {
34 '#theme' => 'link_field_settings',
38 '#type' => 'checkbox',
39 '#title' => t('Optional URL'),
40 '#default_value' => $field['url'],
41 '#return_value' => 'optional',
42 '#description' => t('If checked, the URL field is optional and submitting a title alone will be acceptable. If the URL is ommitted, the title will be displayed as plain text.'),
45 $title_options = array(
46 'optional' => t('Optional Title'),
47 'required' => t('Required Title'),
48 'value' => t('Static Title: '),
49 'none' => t('No Title'),
52 $form['title'] = array(
54 '#title' => t('Link Title'),
55 '#default_value' => isset($field['title']) ?
$field['title'] : 'optional',
56 '#options' => $title_options,
57 '#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 node 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.'),
60 $form['title_value'] = array(
61 '#type' => 'textfield',
62 '#default_value' => $field['title_value'],
66 // Add token module replacements if available
67 if (module_exists('token')) {
68 $form['tokens'] = array(
69 '#type' => 'fieldset',
70 '#collapsible' => TRUE
,
72 '#title' => t('Placeholder tokens'),
73 '#description' => t("The following placeholder tokens can be used in both paths and titles. When used in a path or title, they will be replaced with the appropriate values."),
75 $form['tokens']['help'] = array(
76 '#value' => theme('token_help', 'node'),
79 $form['enable_tokens'] = array(
80 '#type' => 'checkbox',
81 '#title' => t('Allow user-entered tokens'),
82 '#default_value' => isset($field['enable_tokens']) ?
$field['enable_tokens'] : 1,
83 '#description' => t('Checking will allow users to enter tokens in URLs and Titles on the node edit form. This does not affect the field settings on this page.'),
87 $form['display'] = array(
90 $form['display']['url_cutoff'] = array(
91 '#type' => 'textfield',
92 '#title' => t('URL Display Cutoff'),
93 '#default_value' => isset($field['display']['url_cutoff']) ?
$field['display']['url_cutoff'] : '80',
94 '#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.'),
99 $target_options = array(
100 'default' => t('Default (no target attribute)'),
101 '_top' => t('Open link in window root'),
102 '_blank' => t('Open link in new window'),
103 'user' => t('Allow the user to choose'),
105 $form['attributes'] = array(
108 $form['attributes']['target'] = array(
110 '#title' => t('Link Target'),
111 '#default_value' => $field['attributes']['target'] ?
$field['attributes']['target'] : 'default',
112 '#options' => $target_options,
114 $form['attributes']['rel'] = array(
115 '#type' => 'textfield',
116 '#title' => t('Rel Attribute'),
117 '#description' => t('When output, this link will have this rel attribute. The most common usage is <a href="http://en.wikipedia.org/wiki/Nofollow">rel="nofollow"</a> which prevents some search engines from spidering entered links.'),
118 '#default_value' => $field['attributes']['rel'] ?
$field['attributes']['rel'] : '',
119 '#field_prefix' => 'rel = "',
120 '#field_suffix' => '"',
123 $form['attributes']['class'] = array(
124 '#type' => 'textfield',
125 '#title' => t('Additional CSS Class'),
126 '#description' => t('When output, this link will have have this class attribute. Multiple classes should be seperated by spaces.'),
127 '#default_value' => isset($field['attributes']['class']) ?
$field['attributes']['class'] : '',
132 if ($field['title'] == 'value' && empty($field['title_value'])) {
133 form_set_error('title_value', t('A default title must be provided if the title is a static value'));
138 return array('attributes', 'display', 'url', 'title', 'title_value', 'enable_tokens');
140 case
'database columns':
142 'url' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE
, 'sortable' => TRUE
),
143 'title' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE
, 'sortable' => TRUE
),
144 'attributes' => array('type' => 'text', 'size' => 'medium', 'not null' => FALSE
),
148 if (!empty($field)) {
149 $data = link_views_data($field);
151 return isset($data) ?
$data : NULL
;
156 * Theme the settings form for the link field.
158 function theme_link_field_settings($form) {
159 $title_value = drupal_render($form['title_value']);
160 $title_checkbox = drupal_render($form['title']['value']);
162 // Set Static Title radio option to include the title_value textfield.
163 $form['title']['value'] = array('#value' => '<div class="container-inline">'.
$title_checkbox .
$title_value .
'</div>');
165 // Reprint the title radio options with the included textfield.
166 return drupal_render($form);
170 * Implementation of hook_content_is_empty().
172 function link_content_is_empty($item, $field) {
173 if (empty($item['title']) && empty($item['url'])) {
180 * Implementation of hook_field().
182 function link_field($op, &$node, $field, &$items, $teaser, $page) {
185 foreach ($items as
$delta => $item) {
186 _link_load($items[$delta], $delta);
192 $optional_field_found = FALSE
;
193 foreach($items as
$delta => $value) {
194 _link_validate($items[$delta],$delta, $field, $node, $optional_field_found);
197 if ($field['url'] == 'optional' && $field['title'] == 'optional' && $field['required'] && !$optional_field_found) {
198 form_set_error($field['field_name'] .
'][0][title', t('At least one title or URL must be entered.'));
202 case
'process form values':
203 foreach($items as
$delta => $value) {
204 _link_process($items[$delta],$delta, $field, $node);
209 foreach ($items as
$delta => $value) {
210 _link_sanitize($items[$delta], $delta, $field, $node);
217 * Implementation of hook_widget_info().
219 function link_widget_info() {
222 'label' => 'Text Fields for Title and URL',
223 'field types' => array('link'),
224 'multiple values' => CONTENT_HANDLE_CORE
,
230 * Implementation of hook_widget().
232 function link_widget(&$form, &$form_state, $field, $items, $delta = 0) {
234 '#type' => $field['widget']['type'],
235 '#default_value' => isset($items[$delta]) ?
$items[$delta] : '',
236 '#title' => $field['widget']['label'],
237 '#weight' => $field['widget']['weight'],
238 '#description' => $field['widget']['description'],
239 '#required' => $field['required'],
245 function _link_load(&$item, $delta = 0) {
246 // Unserialize the attributes array.
247 $item['attributes'] = unserialize($item['attributes']);
250 function _link_process(&$item, $delta = 0, $field, $node) {
251 // Remove the target attribute if not selected.
252 if (!$item['attributes']['target'] || $item['attributes']['target'] == "default") {
253 unset($item['attributes']['target']);
255 // Trim whitespace from URL.
256 $item['url'] = trim($item['url']);
257 // Serialize the attributes array.
258 $item['attributes'] = serialize($item['attributes']);
260 // Don't save an invalid default value (e.g. 'http://').
261 if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url']) && is_object($node)) {
262 if (!link_validate_url($item['url'])) {
268 function _link_validate(&$item, $delta, $field, $node, &$optional_field_found) {
269 if ($item['url'] && !(isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url'] && !$field['required'])) {
270 // Validate the link.
271 if (link_validate_url(trim($item['url'])) == FALSE
) {
272 form_set_error($field['field_name'] .
']['.
$delta.
'][url', t('Not a valid URL.'));
274 // Require a title for the link if necessary.
275 if ($field['title'] == 'required' && strlen(trim($item['title'])) == 0) {
276 form_set_error($field['field_name'] .
']['.
$delta.
'][title', t('Titles are required for all links.'));
279 // Require a link if we have a title.
280 if ($field['url'] !== 'optional' && strlen($item['title']) > 0 && strlen(trim($item['url'])) == 0) {
281 form_set_error($field['field_name'] .
']['.
$delta.
'][url', t('You cannot enter a title without a link url.'));
283 // In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link.
284 if ($field['url'] == 'optional' && $field['title'] == 'optional' && (strlen(trim($item['url'])) != 0 || strlen(trim($item['title'])) != 0)) {
285 $optional_field_found = TRUE
;
290 * Cleanup user-entered values for a link field according to field settings.
293 * A single link item, usually containing url, title, and attributes.
295 * The delta value if this field is one of multiple fields.
297 * The CCK field definition.
299 * The node containing this link.
301 function _link_sanitize(&$item, $delta, &$field, &$node) {
302 // Don't try to process empty links.
303 if (empty($item['url']) && empty($item['title'])) {
307 // Replace URL tokens.
308 if (module_exists('token') && $field['enable_tokens']) {
309 $token_node = node_load($node->nid
); // Necessary for nodes in views.
310 $item['url'] = token_replace($item['url'], 'node', $token_node);
313 $type = link_validate_url($item['url']);
314 $url = link_cleanup_url($item['url']);
316 // Seperate out the anchor if any.
317 if (strpos($url, '#') !== FALSE
) {
318 $item['fragment'] = substr($url, strpos($url, '#') + 1);
319 $url = substr($url, 0, strpos($url, '#'));
321 // Seperate out the query string if any.
322 if (strpos($url, '?') !== FALSE
) {
323 $item['query'] = substr($url, strpos($url, '?') + 1);
324 $url = substr($url, 0, strpos($url, '?'));
326 // Save the new URL without the anchor or query.
329 // Create a shortened URL for display.
330 $display_url = $type == LINK_EMAIL ?
str_replace('mailto:', '', $url) : url($url, array('query' => isset($item['query']) ?
$item['query'] : NULL
, 'fragment' => isset($item['fragment']) ?
$item['fragment'] : NULL
, 'absolute' => TRUE
));
331 if ($field['display']['url_cutoff'] && strlen($display_url) > $field['display']['url_cutoff']) {
332 $display_url = substr($display_url, 0, $field['display']['url_cutoff']) .
"...";
334 $item['display_url'] = $display_url;
336 // Use the title defined at the field level.
337 if ($field['title'] == 'value' && strlen(trim($field['title_value']))) {
338 $title = $field['title_value'];
340 // Use the title defined by the user at the widget level.
342 $title = $item['title'];
345 if (module_exists('token') && ($field['title'] == 'value' || $field['enable_tokens'])) {
346 $token_node = node_load($node->nid
); // Necessary for nodes in views.
347 $title = filter_xss(token_replace($title, 'node', $token_node), array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
348 $item['html'] = TRUE
;
350 $item['display_title'] = empty($title) ?
$item['display_url'] : $title;
352 // Add attributes defined at the widget level
353 $attributes = array();
354 if (is_array($item['attributes'])) {
355 foreach($item['attributes'] as
$attribute => $attbvalue) {
356 if (isset($item['attributes'][$attribute]) && $field['attributes'][$attribute] == 'user') {
357 $attributes[$attribute] = $attbvalue;
361 // Add attributes defined at the field level
362 if (is_array($field['attributes'])) {
363 foreach($field['attributes'] as
$attribute => $attbvalue) {
364 if (!empty($attbvalue) && $attbvalue != 'default' && $attbvalue != 'user') {
365 $attributes[$attribute] = $attbvalue;
369 // Remove the rel=nofollow for internal links.
370 if ($type != LINK_EXTERNAL
&& isset($attributes['rel']) && strpos($attributes['rel'], 'nofollow') !== FALSE
) {
371 $attributes['rel'] = str_replace('nofollow', '', $attributes['rel']);
372 if (empty($attributes['rel'])) {
373 unset($attributes['rel']);
376 $item['attributes'] = $attributes;
378 // Add the widget label.
379 $item['label'] = $field['widget']['label'];
383 * Implementation of hook_theme().
385 function link_theme() {
387 'link_field_settings' => array(
388 'arguments' => array('element' => NULL
),
390 'link_formatter_default' => array(
391 'arguments' => array('element' => NULL
),
393 'link_formatter_plain' => array(
394 'arguments' => array('element' => NULL
),
396 'link_formatter_url' => array(
397 'arguments' => array('element' => NULL
),
399 'link_formatter_short' => array(
400 'arguments' => array('element' => NULL
),
402 'link_formatter_label' => array(
403 'arguments' => array('element' => NULL
),
405 'link_formatter_separate' => array(
406 'arguments' => array('element' => NULL
),
409 'arguments' => array('element' => NULL
),
415 * FAPI theme for an individual text elements.
417 function theme_link($element) {
418 drupal_add_css(drupal_get_path('module', 'link') .
'/link.css');
420 // Prefix single value link fields with the name of the field.
421 if (empty($element['#field']['multiple'])) {
422 if (isset($element['url']) && isset($element['title'])) {
423 $element['url']['#title'] = $element['#title'] .
' '.
$element['url']['#title'];
424 $element['title']['#title'] = $element['#title'] .
' '.
$element['title']['#title'];
426 elseif($element['url']) {
427 $element['url']['#title'] = $element['#title'];
432 $output .
= '<div class="link-field-subrow clear-block">';
433 if (isset($element['title'])) {
434 $output .
= '<div class="link-field-title link-field-column">' .
theme('textfield', $element['title']) .
'</div>';
436 $output .
= '<div class="link-field-url' .
(isset($element['title']) ?
' link-field-column' : '') .
'">' .
theme('textfield', $element['url']) .
'</div>';
438 if (!empty($element['attributes'])) {
439 $output .
= '<div class="link-attributes">' .
theme('form_element', $element['attributes'], $element['attributes']['#value']) .
'</div>';
445 * Implementation of hook_elements().
447 function link_elements() {
449 $elements['link'] = array(
451 '#columns' => array('url', 'title'),
452 '#process' => array('link_process'),
458 * Process the link type element before displaying the field.
460 * Build the form element. When creating a form using FAPI #process,
461 * note that $element['#value'] is already set.
463 * The $fields array is in $form['#field_info'][$element['#field_name']].
465 function link_process($element, $edit, $form_state, $form) {
466 $field = $form['#field_info'][$element['#field_name']];
467 $delta = $element['#delta'];
468 $element['url'] = array(
469 '#type' => 'textfield',
470 '#maxlength' => '255',
471 '#title' => t('URL'),
472 '#description' => $element['#description'],
473 '#required' => ($delta == 0 && $field['url'] !== 'optional') ?
$element['#required'] : FALSE
,
474 '#default_value' => isset($element['#value']['url']) ?
$element['#value']['url'] : NULL
,
476 if ($field['title'] != 'none' && $field['title'] != 'value') {
477 $element['title'] = array(
478 '#type' => 'textfield',
479 '#maxlength' => '255',
480 '#title' => t('Title'),
481 '#required' => ($delta == 0 && $field['title'] == 'required') ?
$field['required'] : FALSE
,
482 '#default_value' => isset($element['#value']['title']) ?
$element['#value']['title'] : NULL
,
485 if ($field['attributes']['target'] == 'user') {
486 $element['attributes']['target'] = array(
487 '#type' => 'checkbox',
488 '#title' => t('Open URL in a New Window'),
489 '#return_value' => "_blank",
496 * Implementation of hook_field_formatter_info().
498 function link_field_formatter_info() {
501 'label' => t('Title, as link (default)'),
502 'field types' => array('link'),
503 'multiple values' => CONTENT_HANDLE_CORE
,
506 'label' => t('URL, as link'),
507 'field types' => array('link'),
508 'multiple values' => CONTENT_HANDLE_CORE
,
511 'label' => t('URL, as plain text'),
512 'field types' => array('link'),
513 'multiple values' => CONTENT_HANDLE_CORE
,
516 'label' => t('Short, as link with title "Link"'),
517 'field types' => array('link'),
518 'multiple values' => CONTENT_HANDLE_CORE
,
521 'label' => t('Label, as link with label as title'),
522 'field types' => array('link'),
523 'multiple values' => CONTENT_HANDLE_CORE
,
526 'label' => t('Separate title and URL'),
527 'field types' => array('link'),
528 'multiple values' => CONTENT_HANDLE_CORE
,
534 * Theme function for 'default' text field formatter.
536 function theme_link_formatter_default($element) {
537 // Display a normal link if both title and URL are available.
538 if (!empty($element['#item']['display_title']) && !empty($element['#item']['url'])) {
539 return l($element['#item']['display_title'], $element['#item']['url'], $element['#item']);
541 // If only a title, display the title.
542 elseif (!empty($element['#item']['display_title'])) {
543 return check_plain($element['#item']['display_title']);
548 * Theme function for 'plain' text field formatter.
550 function theme_link_formatter_plain($element) {
551 return empty($element['#item']['url']) ?
check_plain($element['#item']['title']) : check_plain($element['#item']['url']);
555 * Theme function for 'url' text field formatter.
557 function theme_link_formatter_url($element) {
558 return $element['#item']['url'] ?
l($element['#item']['display_url'], $element['#item']['url'], $element['#item']) : '';
562 * Theme function for 'short' text field formatter.
564 function theme_link_formatter_short($element) {
565 return $element['#item']['url'] ?
l(t('Link'), $element['#item']['url'], $element['#item']) : '';
569 * Theme function for 'label' text field formatter.
571 function theme_link_formatter_label($element) {
572 return $element['#item']['url'] ?
l($element['#item']['label'], $element['#item']['url'], $element['#item']) : '';
576 * Theme function for 'separate' text field formatter.
578 function theme_link_formatter_separate($element) {
580 $output .
= '<div class="link-item">';
581 $output .
= '<div class="link-title">'.
$element['#item']['display_title'] .
'</div>';
582 $output .
= '<div class="link-url">'.
l($element['#item']['display_url'], $element['#item']['url'], $element['#item']) .
'</div>';
587 function link_token_list($type = 'all') {
588 if ($type == 'field' || $type == 'all') {
591 $tokens['link']['url'] = t("Link URL");
592 $tokens['link']['title'] = t("Link title");
593 $tokens['link']['view'] = t("Formatted html link");
599 function link_token_values($type, $object = NULL
) {
600 if ($type == 'field') {
603 $tokens['url'] = $item['url'];
604 $tokens['title'] = $item['title'];
605 $tokens['view'] = $item['view'];
612 * Forms a valid URL if possible from an entered address.
613 * Trims whitespace and automatically adds an http:// to addresses without a protocol specified
616 * @param string $protocol The protocol to be prepended to the url if one is not specified
618 function link_cleanup_url($url, $protocol = "http") {
620 $type = link_validate_url($url);
622 if ($type == LINK_EXTERNAL
) {
623 // Check if there is no protocol specified.
624 $protocol_match = preg_match("/^([a-z0-9][a-z0-9\.\-_]*:\/\/)/i",$url);
625 if (empty($protocol_match)) {
626 // But should there be? Add an automatic http:// if it starts with a domain name.
627 $domain_match = preg_match('/^(([a-z0-9]([a-z0-9\-_]*\.)+)('. LINK_DOMAINS .
'|[a-z]{2}))/i',$url);
628 if (!empty($domain_match)) {
629 $url = $protocol.
"://".
$url;
638 * A lenient verification for URLs. Accepts all URLs following RFC 1738 standard
639 * for URL formation and all email addresses following the RFC 2368 standard for
640 * mailto address formation.
642 * @param string $text
643 * @return mixed Returns boolean FALSE if the URL is not valid. On success, returns an object with
644 * the following attributes: protocol, hostname, ip, and port.
646 function link_validate_url($text) {
648 $allowed_protocols = variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'));
650 $protocol = '((' .
implode("|", $allowed_protocols) .
'):\/\/)';
651 $authentication = '([a-z0-9]+(:[a-z0-9]+)?@)';
652 $domain = '((([a-z0-9]([a-z0-9\-_\[\]]*\.))+)('. LINK_DOMAINS .
'|[a-z]{2}))';
653 $ipv4 = '([0-9]{1,3}(\.[0-9]{1,3}){3})';
654 $ipv6 = '([0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})';
655 $port = '(:([0-9]{1,5}))';
657 // Pattern specific to eternal links.
658 $external_pattern = '/^' .
$protocol .
'?'.
$authentication .
'?' .
'(' .
$domain .
'|' .
$ipv4 .
'|' .
$ipv6 .
' |localhost)' .
$port .
'?';
660 // Pattern specific to internal links.
661 $internal_pattern = "/^([a-z0-9_\-+\[\]]+)";
663 $directories = "(\/[a-z0-9_\-\.~+%=&,$'():;*@\[\]]*)*";
664 $query = "(\/?\?([?a-z0-9+_\-\.\/%=&,$'():;*@\[\]]*))";
665 $anchor = "(#[a-z0-9_\-\.~+%=&,$'():;*@\[\]]*)";
667 // The rest of the path for a standard URL.
668 $end = $directories .
'?' .
$query .
'?' .
$anchor .
'?' .
'$/i';
670 $user = '[a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'\[\]]+';
671 $email_pattern = '/^mailto:' .
$user .
'@' .
'(' .
$domain .
'|' .
$ipv4 .
'|'.
$ipv6 .
'|localhost)' .
$query .
'$/';
673 if (preg_match($external_pattern .
$end, $text)) {
674 return LINK_EXTERNAL
;
676 elseif (preg_match($internal_pattern .
$end, $text)) {
677 return LINK_INTERNAL
;
679 elseif (in_array('mailto', $allowed_protocols) && preg_match($email_pattern, $text)) {
682 elseif (strpos($text, '<front>') === 0) {