bug report #955214 by alex_b,jcfiala:Fixed problem where a url with a querystring...
[project/link.git] / link.module
CommitLineData
f695ec1c
NH
1<?php
2// $Id$
3
4/**
5 * @file
6 * Defines simple link field types.
7 */
8
8509ca14
NH
9define('LINK_EXTERNAL', 'external');
10define('LINK_INTERNAL', 'internal');
11define('LINK_FRONT', 'front');
12define('LINK_EMAIL', 'email');
23727726
R
13define('LINK_DOMAINS', 'aero|arpa|asia|biz|com|cat|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|mobi|local');
14
15define('LINK_TARGET_DEFAULT', 'default');
16define('LINK_TARGET_NEW_WINDOW', '_blank');
17define('LINK_TARGET_TOP', '_top');
18define('LINK_TARGET_USER', 'user');
8509ca14
NH
19
20/**
3c962bfd
JF
21 * Maximum URLs length.
22 */
23define('LINK_URL_MAX_LENGTH', 2048);
24
25/**
26 * Implements hook_field_info().
f695ec1c
NH
27 */
28function link_field_info() {
29 return array(
3c962bfd 30 'link_field' => array(
23727726
R
31 'label' => t('Link'),
32 'description' => t('Store a title, href, and attributes in the database to assemble a link.'),
ebddcdaa
R
33 'settings' => array(
34 'target' => LINK_TARGET_DEFAULT,
35 'class' => '',
36 'rel' => '',
37 ),
3c962bfd
JF
38 'default_widget' => 'link_field',
39 'default_formatter' => 'default',
23727726 40 ),
f695ec1c
NH
41 );
42}
43
44/**
3c962bfd 45 * Implements hook_field_schema().
ebddcdaa
R
46 */
47function link_field_schema($field) {
48 return array(
49 'columns' => array(
50 'url' => array(
51 'type' => 'varchar',
3c962bfd 52 'length' => LINK_URL_MAX_LENGTH,
ebddcdaa
R
53 'not null' => FALSE,
54 'sortable' => TRUE
55 ),
56 'title' => array(
57 'type' => 'varchar',
58 'length' => 255,
59 'not null' => FALSE,
60 'sortable' => TRUE
61 ),
62 'attributes' => array(
63 'type' => 'text',
64 'size' => 'medium',
65 'not null' => FALSE
66 ),
67 ),
68 );
69}
70
71/**
3c962bfd
JF
72 * Implements hook_field_settings_form().
73 */
74function link_field_settings_form($field, $instance, $has_data) {
75 $form = array(
76 '#element_validate' => array('link_field_settings_form_validate'),
77/* '#theme' => 'link_field_settings', */
78 );
50761cbd 79
3c962bfd
JF
80 $form['url'] = array(
81 '#type' => 'checkbox',
82 '#title' => t('Optional URL'),
83 '#default_value' => isset($field['settings']['url']) ? $field['settings']['url'] : '',
84 '#return_value' => 'optional',
85 '#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.'),
86 );
50761cbd 87
3c962bfd
JF
88 $title_options = array(
89 'optional' => t('Optional Title'),
90 'required' => t('Required Title'),
91 'value' => t('Static Title'),
92 'none' => t('No Title'),
93 );
50761cbd 94
3c962bfd
JF
95 $form['title'] = array(
96 '#type' => 'radios',
97 '#title' => t('Link Title'),
98 '#default_value' => isset($field['settings']['title']) ? $field['settings']['title'] : 'optional',
99 '#options' => $title_options,
100 '#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.'),
101 );
50761cbd 102
3c962bfd
JF
103 $form['title_value'] = array(
104 '#type' => 'textfield',
105 '#title' => t('Static title'),
106 '#default_value' => isset($field['settings']['title_value']) ? $field['settings']['title_value'] : '',
107/* '#size' => '46', */
108 '#description' => t('This title will always be used if &ldquo;Static Title&rdquo; is selected above.'),
109 );
50761cbd 110
3c962bfd
JF
111 // Add token module replacements if available
112 if (module_exists('token')) {
113 $form['tokens'] = array(
114 '#type' => 'fieldset',
115 '#collapsible' => TRUE,
116 '#collapsed' => TRUE,
117 '#title' => t('Placeholder tokens'),
118 '#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."),
119 );
120 $form['tokens']['help'] = array(
121 '#value' => theme('token_help', 'node'),
122 );
50761cbd 123
3c962bfd
JF
124 $form['enable_tokens'] = array(
125 '#type' => 'checkbox',
126 '#title' => t('Allow user-entered tokens'),
127 '#default_value' => isset($field['settings']['enable_tokens']) ? $field['settings']['enable_tokens'] : 1,
128 '#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.'),
129 );
130 }
50761cbd 131
3c962bfd
JF
132 $form['display'] = array(
133 '#tree' => TRUE,
134 );
135 $form['display']['url_cutoff'] = array(
136 '#type' => 'textfield',
137 '#title' => t('URL Display Cutoff'),
138 '#default_value' => isset($field['settings']['display']['url_cutoff']) ? $field['settings']['display']['url_cutoff'] : '80',
139 '#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.'),
140 '#maxlength' => 3,
141 '#size' => 3,
142 );
50761cbd 143
3c962bfd
JF
144 $target_options = array(
145 LINK_TARGET_DEFAULT => t('Default (no target attribute)'),
146 LINK_TARGET_TOP => t('Open link in window root'),
147 LINK_TARGET_NEW_WINDOW => t('Open link in new window'),
148 LINK_TARGET_USER => t('Allow the user to choose'),
149 );
150 $form['attributes'] = array(
151 '#tree' => TRUE,
152 );
153 $form['attributes']['target'] = array(
154 '#type' => 'radios',
155 '#title' => t('Link Target'),
156 '#default_value' => empty($field['settings']['attributes']['target']) ? LINK_TARGET_DEFAULT : $field['settings']['attributes']['target'],
157 '#options' => $target_options,
158 );
159 $form['attributes']['rel'] = array(
160 '#type' => 'textfield',
161 '#title' => t('Rel Attribute'),
162 '#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=&quot;nofollow&quot;</a> which prevents some search engines from spidering entered links.'),
163 '#default_value' => empty($field['settings']['attributes']['rel']) ? '' : $field['settings']['attributes']['rel'],
164 '#field_prefix' => 'rel = "',
165 '#field_suffix' => '"',
166 '#size' => 20,
167 );
168 $form['attributes']['class'] = array(
169 '#type' => 'textfield',
170 '#title' => t('Additional CSS Class'),
171 '#description' => t('When output, this link will have have this class attribute. Multiple classes should be separated by spaces.'),
172 '#default_value' => empty($field['settings']['attributes']['class']) ? '' : $field['settings']['attributes']['class'],
173 );
174 return $form;
175}
176
177/**
178 * Validate the field settings form.
179 */
180function link_field_settings_form_validate($element, &$form_state, $complete_form) {
181 if ($form_state['values']['field']['settings']['title'] === 'value' && empty($form_state['values']['field']['settings']['title_value'])) {
182 form_set_error('title_value', t('A default title must be provided if the title is a static value.'));
183 }
184}
185
186/**
f695ec1c
NH
187 * Implementation of hook_field_settings().
188 */
189function link_field_settings($op, $field) {
190 switch ($op) {
191 case 'form':
8509ca14
NH
192 $form = array(
193 '#theme' => 'link_field_settings',
194 );
f35cf48f 195
8509ca14
NH
196 $form['url'] = array(
197 '#type' => 'checkbox',
198 '#title' => t('Optional URL'),
199 '#default_value' => $field['url'],
200 '#return_value' => 'optional',
23727726 201 '#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.'),
8509ca14
NH
202 );
203
204 $title_options = array(
500b2140
NH
205 'optional' => t('Optional Title'),
206 'required' => t('Required Title'),
8509ca14 207 'value' => t('Static Title: '),
500b2140
NH
208 'none' => t('No Title'),
209 );
f35cf48f 210
500b2140
NH
211 $form['title'] = array(
212 '#type' => 'radios',
213 '#title' => t('Link Title'),
214 '#default_value' => isset($field['title']) ? $field['title'] : 'optional',
8509ca14 215 '#options' => $title_options,
23727726 216 '#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.'),
8509ca14 217 );
f35cf48f 218
8509ca14
NH
219 $form['title_value'] = array(
220 '#type' => 'textfield',
221 '#default_value' => $field['title_value'],
222 '#size' => '46',
223 );
f35cf48f 224
8509ca14
NH
225 // Add token module replacements if available
226 if (module_exists('token')) {
227 $form['tokens'] = array(
228 '#type' => 'fieldset',
229 '#collapsible' => TRUE,
230 '#collapsed' => TRUE,
231 '#title' => t('Placeholder tokens'),
232 '#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."),
233 );
234 $form['tokens']['help'] = array(
235 '#value' => theme('token_help', 'node'),
236 );
f35cf48f 237
8509ca14
NH
238 $form['enable_tokens'] = array(
239 '#type' => 'checkbox',
23727726
R
240 '#title' => t('Allow user-entered tokens'),
241 '#default_value' => isset($field['enable_tokens']) ? $field['enable_tokens'] : 1,
8509ca14
NH
242 '#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.'),
243 );
244 }
f35cf48f 245
8509ca14 246 $form['display'] = array(
23727726 247 '#tree' => TRUE,
8509ca14
NH
248 );
249 $form['display']['url_cutoff'] = array(
250 '#type' => 'textfield',
251 '#title' => t('URL Display Cutoff'),
252 '#default_value' => isset($field['display']['url_cutoff']) ? $field['display']['url_cutoff'] : '80',
253 '#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.'),
254 '#maxlength' => 3,
255 '#size' => 3,
500b2140 256 );
f35cf48f 257
8509ca14 258 $target_options = array(
23727726
R
259 LINK_TARGET_DEFAULT => t('Default (no target attribute)'),
260 LINK_TARGET_TOP => t('Open link in window root'),
261 LINK_TARGET_NEW_WINDOW => t('Open link in new window'),
262 LINK_TARGET_USER => t('Allow the user to choose'),
f695ec1c 263 );
8509ca14 264 $form['attributes'] = array(
23727726 265 '#tree' => TRUE,
f695ec1c
NH
266 );
267 $form['attributes']['target'] = array(
268 '#type' => 'radios',
269 '#title' => t('Link Target'),
23727726 270 '#default_value' => empty($field['attributes']['target']) ? LINK_TARGET_DEFAULT : $field['attributes']['target'],
8509ca14 271 '#options' => $target_options,
f695ec1c 272 );
8f5f8b4f 273 $form['attributes']['rel'] = array(
8509ca14
NH
274 '#type' => 'textfield',
275 '#title' => t('Rel Attribute'),
276 '#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=&quot;nofollow&quot;</a> which prevents some search engines from spidering entered links.'),
23727726
R
277 '#default_value' => empty($field['attributes']['rel']) ? '' : $field['attributes']['rel'],
278 '#field_prefix' => 'rel = "',
279 '#field_suffix' => '"',
280 '#size' => 20,
8509ca14
NH
281 );
282 $form['attributes']['class'] = array(
283 '#type' => 'textfield',
284 '#title' => t('Additional CSS Class'),
23727726
R
285 '#description' => t('When output, this link will have have this class attribute. Multiple classes should be separated by spaces.'),
286 '#default_value' => empty($field['attributes']['class']) ? '' : $field['attributes']['class'],
8f5f8b4f 287 );
f695ec1c
NH
288 return $form;
289
8509ca14
NH
290 case 'validate':
291 if ($field['title'] == 'value' && empty($field['title_value'])) {
292 form_set_error('title_value', t('A default title must be provided if the title is a static value'));
293 }
294 break;
295
f695ec1c 296 case 'save':
8509ca14 297 return array('attributes', 'display', 'url', 'title', 'title_value', 'enable_tokens');
d31c2959
NH
298
299 case 'database columns':
300 return array(
23727726
R
301 'url' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE),
302 'title' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE),
f35cf48f 303 'attributes' => array('type' => 'text', 'size' => 'medium', 'not null' => FALSE),
8509ca14 304 );
d31c2959 305
23727726
R
306 case 'views data':
307 module_load_include('inc', 'link', 'views/link.views');
308 return link_views_content_field_data($field);
f695ec1c
NH
309 }
310}
311
312/**
8509ca14
NH
313 * Theme the settings form for the link field.
314 */
3c962bfd 315/*
8509ca14
NH
316function theme_link_field_settings($form) {
317 $title_value = drupal_render($form['title_value']);
318 $title_checkbox = drupal_render($form['title']['value']);
f35cf48f 319
8509ca14
NH
320 // Set Static Title radio option to include the title_value textfield.
321 $form['title']['value'] = array('#value' => '<div class="container-inline">'. $title_checkbox . $title_value .'</div>');
f35cf48f 322
8509ca14
NH
323 // Reprint the title radio options with the included textfield.
324 return drupal_render($form);
325}
326
327/**
ebddcdaa 328 * Implement hook_field_is_empty().
f35cf48f 329 */
ebddcdaa 330function link_field_is_empty($item, $field) {
3c962bfd
JF
331 return empty($item['title']) && empty($item['url']);
332}
333
334function link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
335 foreach ($entities as $id => $entity) {
336 foreach ($items[$id] as $delta => $item) {
50761cbd 337 $items[$id][$delta]['attributes'] = _link_load($field, $item);
3c962bfd 338 }
f35cf48f 339 }
f35cf48f
NH
340}
341
342/**
3c962bfd 343 * Implements hook_field_validate().
f695ec1c 344 */
3c962bfd
JF
345function link_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
346 $optional_field_found = FALSE;
347 foreach ($items as $delta => $value) {
50761cbd 348 _link_validate($items[$delta], $delta, $field, $entity, $instance, $optional_field_found);
3c962bfd
JF
349 }
350}
f35cf48f 351
3c962bfd
JF
352/**
353 * Implements hook_field_presave().
354 */
355function link_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
356 foreach ($items as $delta => $value) {
50761cbd 357 _link_sanitize($items[$delta], $delta, $field, $entity);
3c962bfd 358 _link_process($items[$delta], $delta, $field, $entity);
f695ec1c
NH
359 }
360}
361
362/**
3c962bfd 363 * Implements hook_field_widget_info().
f695ec1c 364 */
3c962bfd 365function link_field_widget_info() {
f695ec1c 366 return array(
3c962bfd 367 'link_field' => array(
23727726 368 'label' => 'Link',
3c962bfd
JF
369 'field types' => array('link_field'),
370 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
f695ec1c
NH
371 ),
372 );
373}
374
375/**
3c962bfd 376 * Implements hook_field_widget_form().
f695ec1c 377 */
3c962bfd
JF
378function link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
379 $element += array(
380 '#type' => $instance['widget']['type'],
f35cf48f 381 '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
0a10b9ca 382 );
f35cf48f 383 return $element;
0a10b9ca
NH
384}
385
3c962bfd
JF
386/**
387 * Implements hook_field_formatter_view().
388 */
389/*function link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
390 $element = array();
391 if ($display['type'] === 'link_default') {
392 foreach ($items as $delta => $item) {
393 $element[$delta] = array(
394 '#markup' => theme('link_field', $item),
395 );
396 }
23727726 397 }
3c962bfd
JF
398 return $element;
399}*/
400
401function _link_load($field, &$item) {
50761cbd 402 return $item['attributes'] = isset($item['attributes']) ? unserialize($item['attributes']) : $field['settings']['attributes'];
8509ca14
NH
403}
404
3c962bfd 405function _link_process(&$item, $delta = 0, $field, $entity) {
8509ca14 406 // Trim whitespace from URL.
f35cf48f 407 $item['url'] = trim($item['url']);
23727726
R
408
409 // if no attributes are set then make sure $item['attributes'] is an empty array - this lets $field['attributes'] override it.
410 if (empty($item['attributes'])) {
ebddcdaa 411 $item['attributes'] = array();
23727726
R
412 }
413
8509ca14 414 // Serialize the attributes array.
f35cf48f
NH
415 $item['attributes'] = serialize($item['attributes']);
416
417 // Don't save an invalid default value (e.g. 'http://').
e5534e6b
JF
418 if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url'])
419 && is_object($node)) {
f35cf48f
NH
420 if (!link_validate_url($item['url'])) {
421 unset($item['url']);
8509ca14
NH
422 }
423 }
424}
425
50761cbd 426function _link_validate(&$item, $delta, $field, $node, $instance, &$optional_field_found) {
3c962bfd 427 if ($item['url'] && !(isset($field['widget']['default_value'][$delta]['url']) && $item['url'] === $field['widget']['default_value'][$delta]['url'] && !$item['required'])) {
f35cf48f
NH
428 // Validate the link.
429 if (link_validate_url(trim($item['url'])) == FALSE) {
23727726 430 form_set_error($field['field_name'] .']['. $delta .'][url', t('Not a valid URL.'));
f35cf48f
NH
431 }
432 // Require a title for the link if necessary.
3c962bfd 433 if ($field['settings']['title'] == 'required' && strlen(trim($item['title'])) == 0) {
23727726 434 form_set_error($field['field_name'] .']['. $delta .'][title', t('Titles are required for all links.'));
f35cf48f 435 }
8509ca14 436 }
f35cf48f 437 // Require a link if we have a title.
50761cbd 438 if ($field['settings']['url'] !== 'optional' && strlen(isset($item['title']) ? $item['title'] : NULL) > 0 && strlen(trim($item['url'])) == 0) {
23727726 439 form_set_error($field['field_name'] .']['. $delta .'][url', t('You cannot enter a title without a link url.'));
f35cf48f
NH
440 }
441 // In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link.
3c962bfd 442 if ($field['settings']['url'] === 'optional' && $field['settings']['title'] === 'optional' && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
f35cf48f 443 $optional_field_found = TRUE;
8509ca14 444 }
50761cbd
JF
445 // Require entire field
446 if ($field['settings']['url'] === 'optional'
447 && $field['settings']['title'] === 'optional'
448 && $instance['required'] == 1 && !$optional_field_found && isset($instance['id'])) {
449 form_set_error($instance['field_name'] .'][0][title',
450 t('At least one title or URL must be entered.'));
451 }
8509ca14
NH
452}
453
454/**
f35cf48f 455 * Cleanup user-entered values for a link field according to field settings.
23727726 456 *
f35cf48f
NH
457 * @param $item
458 * A single link item, usually containing url, title, and attributes.
459 * @param $delta
460 * The delta value if this field is one of multiple fields.
461 * @param $field
462 * The CCK field definition.
463 * @param $node
464 * The node containing this link.
8509ca14 465 */
f35cf48f 466function _link_sanitize(&$item, $delta, &$field, &$node) {
23727726
R
467 // Don't try to process empty links.
468 if (empty($item['url']) && empty($item['title'])) {
469 return;
470 }
471
f35cf48f
NH
472 // Replace URL tokens.
473 if (module_exists('token') && $field['enable_tokens']) {
23727726
R
474 // Load the node if necessary for nodes in views.
475 $token_node = isset($node->nid) ? node_load($node->nid) : $node;
476 $item['url'] = token_replace($item['url'], 'node', $token_node);
f35cf48f
NH
477 }
478
479 $type = link_validate_url($item['url']);
480 $url = link_cleanup_url($item['url']);
481
23727726 482 // Separate out the anchor if any.
f35cf48f
NH
483 if (strpos($url, '#') !== FALSE) {
484 $item['fragment'] = substr($url, strpos($url, '#') + 1);
485 $url = substr($url, 0, strpos($url, '#'));
8509ca14 486 }
23727726 487 // Separate out the query string if any.
f35cf48f 488 if (strpos($url, '?') !== FALSE) {
e5534e6b
JF
489 $query = substr($url, strpos($url, '?') + 1);
490 parse_str($query, $query_array);
491 $item['query'] = $query_array;
f35cf48f
NH
492 $url = substr($url, 0, strpos($url, '?'));
493 }
f35cf48f
NH
494
495 // Create a shortened URL for display.
23727726 496 $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));
50761cbd
JF
497 if ($field['settings']['display']['url_cutoff'] && strlen($display_url) > $field['settings']['display']['url_cutoff']) {
498 $display_url = substr($display_url, 0, $field['settings']['display']['url_cutoff']) ."...";
f35cf48f
NH
499 }
500 $item['display_url'] = $display_url;
501
502 // Use the title defined at the field level.
50761cbd 503 if ($field['settings']['title'] == 'value' && strlen(trim($field['settings']['title_value']))) {
e5534e6b 504 $title = $field['settings']['title_value'];
f35cf48f
NH
505 }
506 // Use the title defined by the user at the widget level.
e5534e6b 507 else if (isset($item['title'])) {
f35cf48f
NH
508 $title = $item['title'];
509 }
e5534e6b 510
f35cf48f
NH
511 // Replace tokens.
512 if (module_exists('token') && ($field['title'] == 'value' || $field['enable_tokens'])) {
23727726
R
513 // Load the node if necessary for nodes in views.
514 $token_node = isset($node->nid) ? node_load($node->nid) : $node;
515 $title = filter_xss(token_replace($title, 'node', $token_node), array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
516 $item['html'] = TRUE;
f35cf48f 517 }
e5534e6b 518 $item['title'] = empty($title) ? $item['display_url'] : $title;
f35cf48f 519
23727726
R
520 if (!isset($item['attributes'])) {
521 $item['attributes'] = array();
522 }
523
524 // Unserialize attributtes array if it has not been unserialized yet.
525 if (!is_array($item['attributes'])) {
526 $item['attributes'] = (array)unserialize($item['attributes']);
527 }
528
529 // Add default attributes.
50761cbd 530 $field['settings']['attributes'] += _link_default_attributes();
23727726
R
531
532 // Merge item attributes with attributes defined at the field level.
50761cbd 533 $item['attributes'] += $field['settings']['attributes'];
23727726
R
534
535 // If user is not allowed to choose target attribute, use default defined at
536 // field level.
50761cbd
JF
537 if ($field['settings']['attributes']['target'] != LINK_TARGET_USER) {
538 $item['attributes']['target'] = $field['settings']['attributes']['target'];
23727726
R
539 }
540
541 // Remove the target attribute if the default (no target) is selected.
542 if (empty($item['attributes']) || $item['attributes']['target'] == LINK_TARGET_DEFAULT) {
543 unset($item['attributes']['target']);
544 }
545
546 // Remove the rel=nofollow for internal links.
547 if ($type != LINK_EXTERNAL && strpos($item['attributes']['rel'], 'nofollow') !== FALSE) {
548 $item['attributes']['rel'] = str_replace('nofollow', '', $item['attributes']);
8509ca14 549 }
23727726
R
550
551 // Remove empty attributes.
552 $item['attributes'] = array_filter($item['attributes']);
f35cf48f 553
50761cbd 554 // Sets title to trimmed url if one exists
e5534e6b 555 // @TODO: Do we need this? It seems not.
50761cbd
JF
556 if(!empty($item['display_url']) && empty($item['title'])) {
557 $item['title'] = $item['display_url'];
558 }
559 elseif(!isset($item['title'])) {
560 $item['title'] = $item['url'];
561 }
e5534e6b 562
8509ca14
NH
563}
564
565/**
3c962bfd 566 * Implements hook_theme().
8509ca14 567 */
f35cf48f
NH
568function link_theme() {
569 return array(
3c962bfd
JF
570 /*'link_field_settings' => array(
571 'variables' => array('element' => NULL),
572 ),*/
f35cf48f 573 'link_formatter_default' => array(
3c962bfd 574 'variables' => array('element' => NULL),
f35cf48f
NH
575 ),
576 'link_formatter_plain' => array(
3c962bfd 577 'variables' => array('element' => NULL),
f35cf48f 578 ),
23727726 579 'link_formatter_url' => array(
3c962bfd 580 'variables' => array('element' => NULL),
23727726 581 ),
f35cf48f 582 'link_formatter_short' => array(
3c962bfd 583 'variables' => array('element' => NULL),
f35cf48f
NH
584 ),
585 'link_formatter_label' => array(
3c962bfd 586 'variables' => array('element' => NULL),
f35cf48f 587 ),
23727726 588 'link_formatter_separate' => array(
3c962bfd 589 'variables' => array('element' => NULL),
23727726 590 ),
3c962bfd 591 'link_field' => array(
93b7f6b4 592 'render element' => 'element',
f35cf48f
NH
593 ),
594 );
595}
596
597/**
598 * FAPI theme for an individual text elements.
f35cf48f 599 */
93b7f6b4 600function theme_link_field($vars) {
f35cf48f
NH
601 drupal_add_css(drupal_get_path('module', 'link') .'/link.css');
602
93b7f6b4 603 $element = $vars['element'];
23727726
R
604 // Prefix single value link fields with the name of the field.
605 if (empty($element['#field']['multiple'])) {
93b7f6b4
JF
606 if (isset($element['url']) && !isset($element['title'])) {
607 unset($element['url']['#title']);
23727726 608 }
f35cf48f
NH
609 }
610
8509ca14 611 $output = '';
93b7f6b4 612 $output .= '<div class="link-field-subrow clearfix">';
23727726 613 if (isset($element['title'])) {
93b7f6b4 614 $output .= '<div class="link-field-title link-field-column">'. drupal_render($element['title']) .'</div>';
8509ca14 615 }
93b7f6b4 616 $output .= '<div class="link-field-url'. (isset($element['title']) ? ' link-field-column' : '') .'">'. drupal_render($element['url']) .'</div>';
8509ca14 617 $output .= '</div>';
23727726 618 if (!empty($element['attributes']['target'])) {
93b7f6b4 619 $output .= '<div class="link-attributes">'. drupal_render($element['attributes']['target']) .'</div>';
8509ca14 620 }
8509ca14 621 return $output;
f9252715
NH
622}
623
23727726 624/**
3c962bfd 625 * Implements hook_element_info().
f35cf48f 626 */
3c962bfd 627function link_element_info() {
f35cf48f 628 $elements = array();
3c962bfd 629 $elements['link_field'] = array(
f35cf48f 630 '#input' => TRUE,
3c962bfd 631 '#process' => array('link_field_process'),
93b7f6b4
JF
632 '#theme' => 'link_field',
633 '#theme_wrappers' => array('form_element'),
f35cf48f
NH
634 );
635 return $elements;
636}
637
23727726
R
638function _link_default_attributes() {
639 return array(
640 'target' => LINK_TARGET_DEFAULT,
641 'class' => '',
642 'rel' => '',
643 );
644}
645
f35cf48f
NH
646/**
647 * Process the link type element before displaying the field.
648 *
649 * Build the form element. When creating a form using FAPI #process,
650 * note that $element['#value'] is already set.
651 *
652 * The $fields array is in $form['#field_info'][$element['#field_name']].
653 */
3c962bfd
JF
654function link_field_process($element, $form_state, $form) {
655 $settings = &$form_state['field'][$element['#field_name']][$element['#language']]['field']['settings'];
656 $element['url'] = array(
657 '#type' => 'textfield',
658 '#maxlength' => LINK_URL_MAX_LENGTH,
659 '#title' => t('URL'),
3c962bfd
JF
660 '#required' => ($element['#delta'] == 0 && $settings['url'] !== 'optional') ? $element['#required'] : FALSE,
661 '#default_value' => isset($element['#value']['url']) ? $element['#value']['url'] : NULL,
662 );
663 if ($settings['title'] !== 'none' && $settings['title'] !== 'value') {
664 $element['title'] = array(
665 '#type' => 'textfield',
666 '#maxlength' => '255',
667 '#title' => t('Title'),
50761cbd 668 '#required' => ($settings['title'] == 'required' && !empty($element['#value']['url'])) ? TRUE : FALSE,
3c962bfd
JF
669 '#default_value' => isset($element['#value']['title']) ? $element['#value']['title'] : NULL,
670 );
671 }
672
673 // Initialize field attributes as an array if it is not an array yet.
674 if (!is_array($settings['attributes'])) {
675 $settings['attributes'] = array();
676 }
677 // Add default atrributes.
678 $settings['attributes'] += _link_default_attributes();
679 $attributes = isset($element['#value']['attributes']) ? $element['#value']['attributes'] : $settings['attributes'];
680 if (!empty($settings['attributes']['target']) && $settings['attributes']['target'] == LINK_TARGET_USER) {
681 $element['attributes']['target'] = array(
682 '#type' => 'checkbox',
683 '#title' => t('Open URL in a New Window'),
684 '#return_value' => LINK_TARGET_NEW_WINDOW,
50761cbd 685 '#default_value' => isset($attributes['target']) ? $attributes['target'] : FALSE,
3c962bfd
JF
686 );
687 }
688 return $element;
f35cf48f
NH
689}
690
0a10b9ca 691/**
d31c2959
NH
692 * Implementation of hook_field_formatter_info().
693 */
694function link_field_formatter_info() {
695 return array(
696 'default' => array(
23727726 697 'label' => t('Title, as link (default)'),
3c962bfd
JF
698 'field types' => array('link_field'),
699 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
23727726
R
700 ),
701 'url' => array(
702 'label' => t('URL, as link'),
3c962bfd
JF
703 'field types' => array('link_field'),
704 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
d31c2959
NH
705 ),
706 'plain' => array(
23727726 707 'label' => t('URL, as plain text'),
3c962bfd
JF
708 'field types' => array('link_field'),
709 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
8509ca14
NH
710 ),
711 'short' => array(
712 'label' => t('Short, as link with title "Link"'),
3c962bfd
JF
713 'field types' => array('link_field'),
714 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
8509ca14
NH
715 ),
716 'label' => array(
717 'label' => t('Label, as link with label as title'),
3c962bfd
JF
718 'field types' => array('link_field'),
719 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
d31c2959 720 ),
23727726
R
721 'separate' => array(
722 'label' => t('Separate title and URL'),
3c962bfd
JF
723 'field types' => array('link_field'),
724 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
23727726 725 ),
d31c2959
NH
726 );
727}
728
3c962bfd
JF
729function link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
730 $elements = array();
731 foreach ($items as $delta => $item) {
732 $elements[$delta] = array(
50761cbd 733 '#markup' => theme('link_formatter_'. $display['type'], array('element' => $item, 'field' => $instance)),
3c962bfd
JF
734 );
735 }
736 return $elements;
737}
738
d31c2959 739/**
f35cf48f 740 * Theme function for 'default' text field formatter.
f695ec1c 741 */
f35cf48f 742function theme_link_formatter_default($element) {
50761cbd 743 //drupal_set_message('<pre>'. print_r($element['element'], TRUE) .'</pre>');
f35cf48f 744 // Display a normal link if both title and URL are available.
50761cbd
JF
745 if (!empty($element['element']['title']) && !empty($element['element']['url'])) {
746 return l($element['element']['title'], $element['element']['url'], array('attributes' => $element['element']['attributes']));
f695ec1c 747 }
f35cf48f 748 // If only a title, display the title.
50761cbd
JF
749 elseif (!empty($element['element']['title'])) {
750 return check_plain($element['element']['title']);
751 }
752 elseif (!empty($element['element']['url'])) {
753 return l($element['element']['title'], $element['element']['url'], array('attributes' => $element['element']['attributes']));
8509ca14 754 }
f35cf48f 755}
8509ca14 756
f35cf48f
NH
757/**
758 * Theme function for 'plain' text field formatter.
759 */
760function theme_link_formatter_plain($element) {
50761cbd 761 return empty($element['element']['url']) ? check_plain($element['element']['title']) : url($element['element']['url'], array('attributes' => $element['element']['attributes']));
23727726
R
762}
763
764/**
765 * Theme function for 'url' text field formatter.
766 */
3c962bfd 767/*
23727726 768function theme_link_formatter_url($element) {
50761cbd 769 return $element['element']['url'] ? l($element['element']['display_url'], $element['element']['url'], array('attributes' => $element['element']['attributes']) : '';
f35cf48f 770}
3c962bfd 771*/
8509ca14 772
f35cf48f
NH
773/**
774 * Theme function for 'short' text field formatter.
775 */
776function theme_link_formatter_short($element) {
50761cbd 777 return $element['element']['url'] ? l(t('Link'), $element['element']['url'], array('attributes' => $element['element']['attributes'])) : '';
f695ec1c
NH
778}
779
780/**
23727726 781 * Theme function for 'label' text field formatter.
f35cf48f
NH
782 */
783function theme_link_formatter_label($element) {
50761cbd 784 return $element['element']['url'] ? l($element['field']['label'], $element['element']['url'], array('attributes' => $element['element']['attributes'])) : '';
f35cf48f 785}
23727726 786
f35cf48f 787/**
23727726 788 * Theme function for 'separate' text field formatter.
8509ca14 789 */
50761cbd 790
23727726 791function theme_link_formatter_separate($element) {
50761cbd
JF
792 $class = empty($element['element']['attributes']['class']) ? '' : ' '. $element['element']['attributes']['class'];
793 unset($element['element']['attributes']['class']);
794 $title = empty($element['element']['title']) ? '' : check_plain($element['element']['title']);
f35cf48f 795
23727726
R
796 $output = '';
797 $output .= '<div class="link-item '. $class .'">';
798 if (!empty($title)) {
799 $output .= '<div class="link-title">'. $title .'</div>';
8509ca14 800 }
50761cbd 801 $output .= '<div class="link-url">'. l($element['element']['url'], $element['element']['url'], array('attributes' => $element['element']['attributes'])) .'</div>';
23727726
R
802 $output .= '</div>';
803 return $output;
804}
50761cbd 805
f35cf48f 806
23727726 807function link_token_list($type = 'all') {
3c962bfd 808 if ($type === 'field' || $type === 'all') {
23727726 809 $tokens = array();
f35cf48f 810
23727726
R
811 $tokens['link']['url'] = t("Link URL");
812 $tokens['link']['title'] = t("Link title");
813 $tokens['link']['view'] = t("Formatted html link");
8509ca14 814
23727726
R
815 return $tokens;
816 }
817}
8509ca14 818
23727726 819function link_token_values($type, $object = NULL) {
3c962bfd 820 if ($type === 'field') {
23727726 821 $item = $object[0];
8509ca14 822
23727726
R
823 $tokens['url'] = $item['url'];
824 $tokens['title'] = $item['title'];
825 $tokens['view'] = isset($item['view']) ? $item['view'] : '';
8509ca14 826
23727726 827 return $tokens;
8509ca14
NH
828 }
829}
830
831/**
3c962bfd 832 * Implements hook_views_api().
8509ca14 833 */
23727726
R
834function link_views_api() {
835 return array(
836 'api' => 2,
837 'path' => drupal_get_path('module', 'link') .'/views',
838 );
8509ca14
NH
839}
840
841/**
f9252715
NH
842 * Forms a valid URL if possible from an entered address.
843 * Trims whitespace and automatically adds an http:// to addresses without a protocol specified
844 *
1051a8ba
NH
845 * @param string $url
846 * @param string $protocol The protocol to be prepended to the url if one is not specified
f9252715 847 */
8509ca14 848function link_cleanup_url($url, $protocol = "http") {
f9252715 849 $url = trim($url);
8509ca14 850 $type = link_validate_url($url);
f35cf48f 851
3c962bfd 852 if ($type === LINK_EXTERNAL) {
8509ca14 853 // Check if there is no protocol specified.
23727726 854 $protocol_match = preg_match("/^([a-z0-9][a-z0-9\.\-_]*:\/\/)/i", $url);
8509ca14
NH
855 if (empty($protocol_match)) {
856 // But should there be? Add an automatic http:// if it starts with a domain name.
23727726 857 $domain_match = preg_match('/^(([a-z0-9]([a-z0-9\-_]*\.)+)('. LINK_DOMAINS .'|[a-z]{2}))/i', $url);
8509ca14 858 if (!empty($domain_match)) {
23727726 859 $url = $protocol ."://". $url;
8509ca14 860 }
f9252715
NH
861 }
862 }
f35cf48f 863
8509ca14 864 return $url;
f9252715
NH
865}
866
867/**
8509ca14
NH
868 * A lenient verification for URLs. Accepts all URLs following RFC 1738 standard
869 * for URL formation and all email addresses following the RFC 2368 standard for
870 * mailto address formation.
f9252715
NH
871 *
872 * @param string $text
873 * @return mixed Returns boolean FALSE if the URL is not valid. On success, returns an object with
874 * the following attributes: protocol, hostname, ip, and port.
875 */
8509ca14 876function link_validate_url($text) {
f35cf48f 877
8509ca14 878 $allowed_protocols = variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'));
f35cf48f 879
23727726 880 $protocol = '(('. implode("|", $allowed_protocols) .'):\/\/)';
8509ca14 881 $authentication = '([a-z0-9]+(:[a-z0-9]+)?@)';
23727726
R
882 $domain = '(([a-z0-9]([a-z0-9\-_\[\]])*)(\.(([a-z0-9\-_\[\]])+\.)*('. LINK_DOMAINS .'|[a-z]{2}))?)';
883 $ipv4 = '([0-9]{1,3}(\.[0-9]{1,3}){3})';
884 $ipv6 = '([0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})';
8509ca14 885 $port = '(:([0-9]{1,5}))';
f35cf48f 886
8509ca14 887 // Pattern specific to eternal links.
23727726 888 $external_pattern = '/^'. $protocol .'?'. $authentication .'?('. $domain .'|'. $ipv4 .'|'. $ipv6 .' |localhost)'. $port .'?';
8509ca14
NH
889
890 // Pattern specific to internal links.
891 $internal_pattern = "/^([a-z0-9_\-+\[\]]+)";
892
23727726
R
893 $directories = "(\/[a-z0-9_\-\.~+%=&,$'!():;*@\[\]]*)*";
894 // Yes, four backslashes == a single backslash.
895 $query = "(\/?\?([?a-z0-9+_|\-\.\/\\\\%=&,$'():;*@\[\]]*))";
8509ca14
NH
896 $anchor = "(#[a-z0-9_\-\.~+%=&,$'():;*@\[\]]*)";
897
898 // The rest of the path for a standard URL.
23727726 899 $end = $directories .'?'. $query .'?'. $anchor .'?'.'$/i';
8509ca14
NH
900
901 $user = '[a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'\[\]]+';
23727726 902 $email_pattern = '/^mailto:'. $user .'@'.'('. $domain .'|'. $ipv4 .'|'. $ipv6 .'|localhost)'. $query .'?$/';
8509ca14 903
23727726
R
904 if (strpos($text, '<front>') === 0) {
905 return LINK_FRONT;
8509ca14 906 }
23727726 907 if (in_array('mailto', $allowed_protocols) && preg_match($email_pattern, $text)) {
8509ca14
NH
908 return LINK_EMAIL;
909 }
23727726
R
910 if (preg_match($internal_pattern . $end, $text)) {
911 return LINK_INTERNAL;
912 }
913 if (preg_match($external_pattern . $end, $text)) {
914 return LINK_EXTERNAL;
f695ec1c 915 }
23727726 916
8509ca14 917 return FALSE;
6f21634b 918}