security by dropbox: Fixes security vulnerability in Separate Title and URL formatter
[project/link.git] / link.module
1 <?php
2 // $Id$
3
4 /**
5 * @file
6 * Defines simple link field types.
7 */
8
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|local');
14
15 /**
16 * Implementation of hook_menu().
17 */
18 function link_menu($may_cache) {
19 $items = array();
20 if ($may_cache) {
21 $items[] = array(
22 'path' => 'link/widget/js',
23 'callback' => 'link_widget_js',
24 'access' => user_access('access content'),
25 'type' => MENU_CALLBACK,
26 );
27 }
28 return $items;
29 }
30
31 /**
32 * Implementation of hook_field_info().
33 */
34 function link_field_info() {
35 return array(
36 'link' => array('label' => 'Link'),
37 );
38 }
39
40 /**
41 * Implementation of hook_field_settings().
42 */
43 function link_field_settings($op, $field) {
44 switch ($op) {
45 case 'form':
46 $form = array(
47 '#theme' => 'link_field_settings',
48 );
49
50 $form['url'] = array(
51 '#type' => 'checkbox',
52 '#title' => t('Optional URL'),
53 '#default_value' => $field['url'],
54 '#return_value' => 'optional',
55 '#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.'),
56 );
57
58 $title_options = array(
59 'optional' => t('Optional Title'),
60 'required' => t('Required Title'),
61 'value' => t('Static Title: '),
62 'none' => t('No Title'),
63 );
64
65 $form['title'] = array(
66 '#type' => 'radios',
67 '#title' => t('Link Title'),
68 '#default_value' => isset($field['title']) ? $field['title'] : 'optional',
69 '#options' => $title_options,
70 '#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.'),
71 );
72
73 $form['title_value'] = array(
74 '#type' => 'textfield',
75 '#default_value' => $field['title_value'],
76 '#size' => '46',
77 );
78
79 // Add token module replacements if available
80 if (module_exists('token')) {
81 $form['tokens'] = array(
82 '#type' => 'fieldset',
83 '#collapsible' => TRUE,
84 '#collapsed' => TRUE,
85 '#title' => t('Placeholder tokens'),
86 '#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."),
87 );
88 $form['tokens']['help'] = array(
89 '#value' => theme('token_help', 'node'),
90 );
91
92 $form['enable_tokens'] = array(
93 '#type' => 'checkbox',
94 '#title' => t('Allow user-entered tokens'),
95 '#default_value' => isset($field['enable_tokens']) ? $field['enable_tokens'] : 1,
96 '#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.'),
97 );
98 }
99
100 $form['display'] = array(
101 '#tree' => true,
102 );
103 $form['display']['url_cutoff'] = array(
104 '#type' => 'textfield',
105 '#title' => t('URL Display Cutoff'),
106 '#default_value' => isset($field['display']['url_cutoff']) ? $field['display']['url_cutoff'] : '80',
107 '#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.'),
108 '#maxlength' => 3,
109 '#size' => 3,
110 );
111
112 $target_options = array(
113 'default' => t('Default (no target attribute)'),
114 '_top' => t('Open link in window root'),
115 '_blank' => t('Open link in new window'),
116 'user' => t('Allow the user to choose'),
117 );
118 $form['attributes'] = array(
119 '#tree' => true,
120 );
121 $form['attributes']['target'] = array(
122 '#type' => 'radios',
123 '#title' => t('Link Target'),
124 '#default_value' => empty($field['attributes']['target']) ? 'default' : $field['attributes']['target'],
125 '#options' => $target_options,
126 );
127 $form['attributes']['rel'] = array(
128 '#type' => 'textfield',
129 '#title' => t('Rel Attribute'),
130 '#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.'),
131 '#default_value' => empty($field['attributes']['rel']) ? '' : $field['attributes']['rel'],
132 '#field_prefix' => 'rel = "',
133 '#field_suffix' => '"',
134 '#size' => 20,
135 );
136 $form['attributes']['class'] = array(
137 '#type' => 'textfield',
138 '#title' => t('Additional CSS Class'),
139 '#description' => t('When output, this link will have have this class attribute. Multiple classes should be separated by spaces.'),
140 '#default_value' => empty($field['attributes']['class']) ? '' : $field['attributes']['class'],
141 );
142 return $form;
143
144 case 'validate':
145 if ($field['title'] == 'value' && empty($field['title_value'])) {
146 form_set_error('title_value', t('A default title must be provided if the title is a static value'));
147 }
148 break;
149
150 case 'save':
151 return array('attributes', 'display', 'url', 'title', 'title_value', 'enable_tokens');
152
153 case 'database columns':
154 return array(
155 'url' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => "''", 'sortable' => TRUE),
156 'title' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => "''", 'sortable' => TRUE),
157 'attributes' => array('type' => 'mediumtext', 'not null' => FALSE),
158 );
159
160 case 'filters':
161 return array(
162 'default' => array(
163 'name' => t('URL'),
164 'operator' => 'views_handler_operator_like',
165 'handler' => 'views_handler_operator_like',
166 ),
167 'title' => array(
168 'name' => t('Title'),
169 'operator' => 'views_handler_operator_like',
170 'handler' => 'views_handler_operator_like',
171 ),
172 'protocol' => array(
173 'name' => t('Protocol'),
174 'list' => drupal_map_assoc(variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'))),
175 'operator' => 'views_handler_operator_or',
176 'handler' => 'link_views_protocol_filter_handler',
177 ),
178 );
179
180 case 'arguments':
181 return array(
182 'content: '. $field['field_name'] .'_title' => array(
183 'name' => t('Link Title') .': '. t($field['widget']['label']) .' ('. $field['field_name'] .')',
184 'handler' => 'link_views_argument_handler',
185 ),
186 'content: '. $field['field_name'] .'_target' => array(
187 'name' => t('Link Target') .': '. t($field['widget']['label']) .' ('. $field['field_name'] .')',
188 'handler' => 'link_views_argument_handler',
189 ),
190 );
191
192 }
193 }
194
195 /**
196 * Theme the settings form for the link field.
197 */
198 function theme_link_field_settings($form) {
199 $title_value = drupal_render($form['title_value']);
200 $title_checkbox = drupal_render($form['title']['value']);
201
202 // Set Static Title radio option to include the title_value textfield.
203 $form['title']['value'] = array('#value' => '<div class="container-inline">'. $title_checkbox . $title_value .'</div>');
204
205 // Reprint the title radio options with the included textfield.
206 return drupal_render($form);
207 }
208
209 /**
210 * Implementation of hook_field().
211 */
212 function link_field($op, &$node, $field, &$items, $teaser, $page) {
213 switch ($op) {
214 case 'load':
215 foreach ($items as $delta => $item) {
216 $items[$delta]['attributes'] = unserialize($item['attributes']);
217 }
218 break;
219 case 'view':
220 foreach ($items as $delta => $item) {
221 $items[$delta]['view'] = content_format($field, $items[$delta], 'default', $node);
222 }
223 return theme('field', $node, $field, $items, $teaser, $page);
224 break;
225 }
226 }
227
228 /**
229 * Implementation of hook_widget_info().
230 */
231 function link_widget_info() {
232 return array(
233 'link' => array(
234 'label' => 'Text Fields for Title and URL',
235 'field types' => array('link'),
236 ),
237 );
238 }
239
240 /**
241 * Implementation of hook_widget().
242 */
243 function link_widget($op, &$node, $field, &$items) {
244 switch ($op) {
245 case 'prepare form values':
246 foreach($items as $delta => $value) {
247 if (is_numeric($delta)) {
248 _link_widget_prepare($items[$delta], $delta);
249 }
250 }
251 if ($_POST[$field['field_name']]) {
252 $items = $_POST[$field['field_name']];
253 unset($items['count'], $items['more-url'], $items['more']);
254 }
255 return;
256 case 'form':
257 $form = array();
258 $form[$field['field_name']] = array(
259 '#tree' => TRUE,
260 '#theme' => 'link_widget_form',
261 '#type' => $field['multiple'] ? 'fieldset' : 'markup',
262 '#collapsible' => TRUE,
263 '#collapsed' => FALSE,
264 '#title' => t($field['widget']['label']),
265 '#description' => $field['widget']['description'],
266 );
267
268 // Add token module replacements if available.
269 if (module_exists('token') && $field['enable_tokens']) {
270 $tokens_form = array(
271 '#type' => 'fieldset',
272 '#collapsible' => TRUE,
273 '#collapsed' => TRUE,
274 '#title' => t('Placeholder tokens'),
275 '#description' => t("The following placeholder tokens can be used in both titles and URLs. When used in a URL or title, they will be replaced with the appropriate values."),
276 '#weight' => 2,
277 );
278 $tokens_form['help'] = array(
279 '#value' => theme('token_help', 'node'),
280 );
281 }
282
283 if ($field['multiple']) {
284 drupal_add_js(drupal_get_path('module', 'link') .'/link.js');
285
286 $delta = 0;
287 // Render link fields for all the entered values.
288 foreach ($items as $data) {
289 if (is_array($data) && ($data['url'] || $data['title'])) {
290 _link_widget_form($form[$field['field_name']][$delta], $field, $data, $delta);
291 $delta++;
292 }
293 }
294 // Render two additional new link fields.
295 foreach (range($delta, $delta + 1) as $delta) {
296 _link_widget_form($form[$field['field_name']][$delta], $field, array(), $delta);
297 }
298
299 // Create a wrapper for additional fields.
300 $form[$field['field_name']]['wrapper'] = array(
301 '#type' => 'markup',
302 '#value' => '<div id="' . str_replace('_', '-', $field['field_name']) . '-wrapper" class="clear-block"></div>',
303 );
304
305 // Add 'More' Javascript Callback.
306 $form[$field['field_name']]['more-url'] = array(
307 '#type' => 'hidden',
308 '#value' => url('link/widget/js/' . $field['type_name'] . '/' . $field['field_name'], NULL, NULL, TRUE),
309 '#attributes' => array('class' => 'more-links'),
310 '#id' => str_replace('_', '-', $field['field_name']) . '-more-url',
311 );
312
313 // Add Current Field Count.
314 $form[$field['field_name']]['count'] = array(
315 '#type' => 'hidden',
316 '#value' => $delta,
317 '#id' => str_replace('_', '-', $field['field_name']) . '-count',
318 );
319
320 // Add More Button.
321 $form[$field['field_name']]['more'] = array(
322 '#type' => 'button',
323 '#value' => t('More Links'),
324 '#name' => 'more',
325 '#id' => str_replace('_', '-', $field['field_name']) . '-more',
326 '#weight' => 1,
327 );
328 if (isset($tokens_form)) {
329 $form[$field['field_name']]['tokens'] = $tokens_form;
330 }
331 } // end if multiple.
332 else {
333 _link_widget_form($form[$field['field_name']][0], $field, $items[0]);
334 if (isset($tokens_form)) {
335 $form[$field['field_name']][0]['tokens'] = $tokens_form;
336 }
337 }
338 return $form;
339
340 case 'validate':
341 if (!is_object($node)) return;
342 unset($items['count']);
343 unset($items['more-url']);
344 unset($items['more']);
345 $optional_field_found = FALSE;
346 foreach($items as $delta => $value) {
347 _link_widget_validate($items[$delta],$delta, $field, $node, $optional_field_found);
348 }
349
350 if ($field['url'] == 'optional' && $field['title'] == 'optional' && $field['required'] && !$optional_field_found) {
351 form_set_error($field['field_name'] .'][0][title', t('At least one title or URL must be entered.'));
352 }
353 return;
354
355 case 'process form values':
356 // Remove the JS helper fields.
357 unset($items['more-url']);
358 unset($items['count']);
359 unset($items['more']);
360 foreach($items as $delta => $value) {
361 if (is_numeric($delta)) {
362 _link_widget_process($items[$delta],$delta, $field, $node);
363 }
364 }
365 return;
366
367 case 'submit':
368 // Don't save empty fields (beyond the first one).
369 $save_field = array();
370 unset($items['more-url']);
371 unset($items['count']);
372 unset($items['more']);
373 foreach ($items as $delta => $value) {
374 if ($value['url'] !== 'optional' || $delta == 0) {
375 $save_items[] = $items[$delta];
376 }
377 }
378 $items = $save_items;
379 return;
380 }
381 }
382
383 /**
384 * Helper function renders the link widget in both single and multiple value cases.
385 */
386
387 function _link_widget_form(&$form_item, $field, $item, $delta = 0) {
388 $form_item = array(
389 '#tree' => TRUE,
390 '#theme' => 'link_widget_form_row',
391 );
392
393 $default_url = "";
394 if (isset($field['widget']['default_value'][$delta]['url'])) {
395 $default_url = $field['widget']['default_value'][$delta]['url'];
396 }
397
398 $form_item['url'] = array(
399 '#type' => 'textfield',
400 '#maxlength' => '255',
401 '#title' => $delta == 0 ? t('URL') : NULL,
402 '#default_value' => ($item['url']) ? $item['url'] : $default_url,
403 '#required' => ($delta == 0) ? ($field['required'] && empty($field['url'])) : FALSE,
404 );
405 if ($field['title'] != 'value' && $field['title'] != 'none') {
406 $default_title = "";
407 if (isset($field['widget']['default_value'][$delta]['title'])) {
408 $default_title = $field['widget']['default_value'][$delta]['title'];
409 }
410 $form_item['title'] = array(
411 '#type' => 'textfield',
412 '#maxlength' => '255',
413 '#title' => $delta == 0 ? t('Title') : NULL,
414 '#default_value' => ($item['title']) ? $item['title'] : $default_title,
415 '#required' => ($delta == 0 && $field['title'] == 'required') ? $field['required'] : FALSE,
416 );
417 }
418 if (!empty($field['attributes']['target']) && $field['attributes']['target'] == 'user') {
419 $form_item['attributes']['target'] = array(
420 '#type' => 'checkbox',
421 '#title' => t('Open URL in a New Window'),
422 '#default_value' => $item['attributes']['target'],
423 '#return_value' => "_blank",
424 );
425 }
426 }
427
428 function _link_widget_prepare(&$item, $delta = 0) {
429 // Unserialize the attributes array.
430 $item['attributes'] = unserialize($item['attributes']);
431 }
432
433 function _link_widget_process(&$item, $delta = 0, $field, $node) {
434 // Remove the target attribute if not selected.
435 if (!$item['attributes']['target'] || $item['attributes']['target'] == "default") {
436 unset($item['attributes']['target']);
437 }
438 // Trim whitespace from URL.
439 $item['url'] = trim($item['url']);
440 // Serialize the attributes array.
441 $item['attributes'] = serialize($item['attributes']);
442
443 // Don't save an invalid default value (e.g. 'http://').
444 if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url']) && is_object($node)) {
445 if (!link_validate_url($item['url'])) {
446 unset($item['url']);
447 }
448 }
449 }
450
451 function _link_widget_validate(&$item, $delta, $field, $node, &$optional_field_found) {
452 if ($item['url'] && !(isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url'] && !$field['required'])) {
453 // Validate the link.
454 if (link_validate_url(trim($item['url'])) == FALSE) {
455 form_set_error($field['field_name'] .']['. $delta. '][url', t('Not a valid URL.'));
456 }
457 // Require a title for the link if necessary.
458 if ($field['title'] == 'required' && strlen(trim($item['title'])) == 0) {
459 form_set_error($field['field_name'] .']['. $delta. '][title', t('Titles are required for all links.'));
460 }
461 }
462 // Require a link if we have a title.
463 if ($field['url'] !== 'optional' && strlen($item['title']) > 0 && strlen(trim($item['url'])) == 0) {
464 form_set_error($field['field_name'] .']['. $delta. '][url', t('You cannot enter a title without a link url.'));
465 }
466 // In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link.
467 if ($field['url'] == 'optional' && $field['title'] == 'optional' && (strlen(trim($item['url'])) != 0 || strlen(trim($item['title'])) != 0)) {
468 $optional_field_found = TRUE;
469 }
470 }
471
472 function link_widget_js($type_name, $field_name) {
473 $field = content_fields($field_name, $type_name);
474 $type = content_types($type);
475 $delta = $_POST[$field_name]['count'];
476 $form = array();
477 $node_field = array();
478
479 _link_widget_form($form, $field, $node_field, $delta);
480
481 // Assign parents matching the original form.
482 foreach (element_children($form) as $key) {
483 $form[$key]['#parents'] = array($field_name, $delta, $key);
484 }
485
486 // Add names, ids, and other form properties.
487 foreach (module_implements('form_alter') as $module) {
488 $function = $module .'_form_alter';
489 $function('link_widget_js', $form);
490 }
491 $form = form_builder('link_widget_js', $form);
492
493 $output = drupal_render($form);
494
495 print drupal_to_js(array('status' => TRUE, 'data' => $output));
496 exit;
497 }
498
499 /**
500 * Theme the display of the entire link set
501 */
502 function theme_link_widget_form($element) {
503 drupal_add_css(drupal_get_path('module', 'link') .'/link.css');
504 // Check for multiple (output normally).
505 if (isset($element[1])) {
506 $output = drupal_render($element);
507 }
508 // Add the field label to the 'Title' and 'URL' labels.
509 else {
510 if (isset($element[0]['title'])) {
511 $element[0]['title']['#title'] = $element['#title'] . ' ' . $element[0]['title']['#title'];
512 $element[0]['title']['#description'] = $element['#description'];
513 $element[0]['url']['#title'] = $element['#title'] . ' ' . $element[0]['url']['#title'];
514 }
515 else {
516 $element[0]['url']['#title'] = $element['#title'];
517 $element[0]['url']['#description'] = $element['#description'];
518 }
519 $output = drupal_render($element);
520 }
521
522 return $output;
523 }
524
525 /**
526 * Theme the display of a single form row
527 */
528 function theme_link_widget_form_row($element) {
529 $output = '';
530 $output .= '<div class="link-field-row clear-block"><div class="link-field-subrow clear-block">';
531 if ($element['title']) {
532 $output .= '<div class="link-field-title link-field-column">' . drupal_render($element['title']) . '</div>';
533 }
534 $output .= '<div class="link-field-url' . ($element['title'] ? ' link-field-column' : '') . '">' . drupal_render($element['url']) . '</div>';
535 $output .= '</div>';
536 if ($element['attributes']) {
537 $output .= '<div class="link-attributes">' . drupal_render($element['attributes']) . '</div>';
538 }
539 $output .= drupal_render($element);
540 $output .= '</div>';
541 return $output;
542 }
543
544 /**
545 * Implementation of hook_field_formatter_info().
546 */
547 function link_field_formatter_info() {
548 return array(
549 'default' => array(
550 'label' => t('Title, as link (default)'),
551 'field types' => array('link'),
552 ),
553 'url' => array(
554 'label' => t('URL, as link'),
555 'field types' => array('link'),
556 ),
557 'plain' => array(
558 'label' => t('URL, plain text'),
559 'field types' => array('link'),
560 ),
561 'short' => array(
562 'label' => t('Short, as link with title "Link"'),
563 'field types' => array('link'),
564 ),
565 'label' => array(
566 'label' => t('Label, as link with label as title'),
567 'field types' => array('link'),
568 ),
569 'separate' => array(
570 'label' => t('Separate title and URL'),
571 'field types' => array('link'),
572 ),
573 );
574 }
575
576 /**
577 * Implementation of hook_field_formatter().
578 */
579 function link_field_formatter($field, $item, $formatter, $node) {
580 if (empty($item['url']) && ($field['url'] != 'optional' || empty($item['title']))) {
581 return '';
582 }
583
584 if ($formatter == 'plain') {
585 return empty($item['url']) ? check_plain($item['title']) : check_plain(link_cleanup_url($item['url']));
586 }
587
588 // Replace URL tokens.
589 if (module_exists('token') && $field['enable_tokens']) {
590 // Load the node if necessary for nodes in views.
591 $token_node = isset($node->nid) ? node_load($node->nid) : $node;
592 $item['url'] = token_replace($item['url'], 'node', $token_node);
593 }
594
595 $type = link_validate_url($item['url']);
596 $url = link_cleanup_url($item['url']);
597
598 // Separate out the anchor if any.
599 if (strpos($url, '#') !== FALSE) {
600 $fragment = substr($url, strpos($url, '#') + 1);
601 $url = substr($url, 0, strpos($url, '#'));
602 }
603 // Separate out the query string if any.
604 if (strpos($url, '?') !== FALSE) {
605 $query = substr($url, strpos($url, '?') + 1);
606 $url = substr($url, 0, strpos($url, '?'));
607 }
608
609 // Create a display URL.
610 $display_url = $type == LINK_EMAIL ? str_replace('mailto:', '', $url) : url($url, $query, $fragment, TRUE);
611 if ($field['display']['url_cutoff'] && strlen($display_url) > $field['display']['url_cutoff']) {
612 $display_url = substr($display_url, 0, $field['display']['url_cutoff']) . "...";
613 }
614
615 // Build a list of attributes.
616 $attributes = array();
617 $item['attributes'] = unserialize($item['attributes']);
618 // Add attributes defined at the widget level.
619 if (!empty($item['attributes']) && is_array($item['attributes'])) {
620 foreach($item['attributes'] as $attribute => $attbvalue) {
621 if (isset($item['attributes'][$attribute]) && $field['attributes'][$attribute] == 'user') {
622 $attributes[$attribute] = $attbvalue;
623 }
624 }
625 }
626 // Add attributes defined at the field level.
627 if (is_array($field['attributes'])) {
628 foreach($field['attributes'] as $attribute => $attbvalue) {
629 if (!empty($attbvalue) && $attbvalue != 'default' && $attbvalue != 'user') {
630 $attributes[$attribute] = $attbvalue;
631 }
632 }
633 }
634 // Remove the rel=nofollow for internal links.
635 if ($type != LINK_EXTERNAL && isset($attributes['rel']) && strpos($attributes['rel'], 'nofollow') !== FALSE) {
636 $attributes['rel'] = str_replace('nofollow', '', $attributes['rel']);
637 if (empty($attributes['rel'])) {
638 unset($attributes['rel']);
639 }
640 }
641
642 // Give the link the title 'Link'.
643 if ($formatter == 'short') {
644 $output = l(t('Link'), $url, $attributes, $query, $fragment);
645 }
646 // Build the link using the widget label.
647 elseif ($formatter == 'label') {
648 $output = l(t($field['widget']['label']), $url, $attributes, $query, $fragment);
649 }
650 // Build the link using the URL as title
651 elseif ($formatter == 'url') {
652 $output = l($display_url, $url, $attributes, $query, $fragment);
653 }
654 // Build the link using the widget label as separate title.
655 elseif ($formatter == 'separate') {
656 $title = check_plain(_link_field_formatter_title($field, $item, $node));
657 $class = empty($field['attributes']['class']) ? '' : ' '. $field['attributes']['class'];
658 unset($field['attributes']['class']);
659
660 $output = '';
661 $output .= '<div class="link-item'. $class .'">';
662 $output .= '<div class="link-title">'. $title .'</div>';
663 $output .= '<div class="link-url">'. l($display_url, $url, $attributes, $query, $fragment, FALSE, $item['html']) .'</div>';
664 $output .= '</div>';
665 }
666 // Build the link with a title.
667 elseif (strlen(trim($item['title'])) || ($field['title'] == 'value' && strlen(trim($field['title_value'])))) {
668 $title = _link_field_formatter_title($field, $item, $node);
669 if (empty($url) && !empty($title)) {
670 $output = check_plain($title);
671 }
672 else {
673 $output = l($title, $url, $attributes, $query, $fragment, FALSE, $item['html']);
674 }
675 }
676 // Build the link with the URL or email address as the title (max 80 characters).
677 else {
678 $output = l($display_url, $url, $attributes, $query, $fragment);
679 }
680 return $output;
681 }
682
683 /**
684 * Helper function for link_field_formatter().
685 */
686 function _link_field_formatter_title(&$field, &$item, &$node) {
687 // Use the title defined at the field level.
688 if ($field['title'] == 'value' && trim($field['title_value'])) {
689 $title = $field['title_value'];
690 }
691 // Use the title defined by the user at the widget level.
692 else {
693 $title = $item['title'];
694 }
695 // Replace tokens.
696 $item['html'] = FALSE;
697 if (module_exists('token') && ($field['title'] == 'value' || $field['enable_tokens'])) {
698 // Load the node if necessary for nodes in views.
699 $token_node = isset($node->nid) ? node_load($node->nid) : $node;
700 $title = filter_xss(token_replace($title, 'node', $token_node), array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
701 $item['html'] = TRUE;
702 }
703 return $title;
704 }
705
706 /**
707 * Views module argument handler for link fields
708 */
709 function link_views_argument_handler($op, &$query, $argtype, $arg = '') {
710 if ($op == 'filter') {
711 $field_name = substr($argtype['type'], 9, strrpos($argtype['type'], '_') - 9);
712 $column = substr($argtype['type'], strrpos($argtype['type'], '_') + 1);
713 }
714 else {
715 $field_name = substr($argtype, 9, strrpos($argtype, '_') - 9);
716 $column = substr($argtype, strrpos($argtype, '_') + 1);
717 }
718
719 // Right now the only attribute we support in views in 'target', but
720 // other attributes of the href tag could be added later.
721 if ($column == 'target') {
722 $attribute = $column;
723 $column = 'attributes';
724 }
725
726 $field = content_fields($field_name);
727 $db_info = content_database_info($field);
728 $main_column = $db_info['columns'][$column];
729
730 // The table name used here is the Views alias for the table, not the actual
731 // table name.
732 $table = 'node_data_'. $field['field_name'];
733
734 switch ($op) {
735 case 'summary':
736 $query->ensure_table($table);
737 $query->add_field($main_column['column'], $table);
738 return array('field' => $table .'.'. $main_column['column']);
739 break;
740
741 case 'filter':
742 $query->ensure_table($table);
743 if ($column == 'attributes') {
744 // Because attributes are stored serialized, our only option is to also
745 // serialize the data we're searching for and use LIKE to find similar data.
746 $query->add_where($table .'.'. $main_column['column'] ." LIKE '%%%s%'", serialize($attribute) . serialize($arg));
747 }
748 else {
749 $query->add_where($table .'.'. $main_column['column'] ." = '%s'", $arg);
750 }
751 break;
752
753 case 'link':
754 $item = array();
755 foreach ($db_info['columns'] as $column => $attributes) {
756 $view_column_name = $attributes['column'];
757 $item[$column] = $query->$view_column_name;
758 }
759
760 return l(content_format($field, $item, 'plain'), $arg .'/'. $query->$main_column['column'], array(), NULL, NULL, FALSE, TRUE);
761
762 case 'sort':
763 break;
764
765 case 'title':
766 $item = array(key($db_info['columns']) => $query);
767 return content_format($field, $item);
768 break;
769 }
770 }
771
772 /**
773 * Views modules filter handler for link protocol filtering
774 */
775 function link_views_protocol_filter_handler($op, $filter, $filterinfo, &$query) {
776 global $db_type;
777
778 $protocols = $filter['value'];
779 $field = $filterinfo['field'];
780 // $table is not the real table name but the views alias.
781 $table = 'node_data_'. $filterinfo['content_field']['field_name'];
782
783 foreach ($protocols as $protocol) {
784 // Simple case, the URL begins with the specified protocol.
785 $condition = $table .'.'. $field .' LIKE \''. $protocol .'%\'';
786
787 // More complex case, no protocol specified but is automatically cleaned up
788 // by link_cleanup_url(). RegEx is required for this search operation.
789 if ($protocol == 'http') {
790 if ($db_type == 'pgsql') {
791 // PostGreSQL code has NOT been tested. Please report any problems to the link issue queue.
792 // pgSQL requires all slashes to be double escaped in regular expressions.
793 // See http://www.postgresql.org/docs/8.1/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
794 $condition .= ' OR '. $table .'.'. $field .' ~* \''. '^(([a-z0-9]([a-z0-9\\-_]*\\.)+)('. LINK_DOMAINS .'|[a-z][a-z]))' .'\'';
795 }
796 else {
797 // mySQL requires backslashes to be double (triple?) escaped within character classes.
798 // See http://dev.mysql.com/doc/refman/5.0/en/string-comparison-functions.html#operator_regexp
799 $condition .= ' OR '. $table .'.'. $field .' REGEXP \''. '^(([a-z0-9]([a-z0-9\\\-_]*\.)+)('. LINK_DOMAINS .'|[a-z][a-z]))' .'\'';
800 }
801 }
802
803 $where_conditions[] = $condition;
804 }
805
806 $query->ensure_table($table);
807 $query->add_where(implode(' '. $filter['operator'] .' ', $where_conditions));
808 }
809
810 /**
811 * Forms a valid URL if possible from an entered address.
812 * Trims whitespace and automatically adds an http:// to addresses without a protocol specified
813 *
814 * @param string $url
815 * @param string $protocol The protocol to be prepended to the url if one is not specified
816 */
817 function link_cleanup_url($url, $protocol = "http") {
818 $url = trim($url);
819 $type = link_validate_url($url);
820
821 if ($type == LINK_EXTERNAL) {
822 // Check if there is no protocol specified.
823 $protocol_match = preg_match("/^([a-z0-9][a-z0-9\.\-_]*:\/\/)/i",$url);
824 if (empty($protocol_match)) {
825 // But should there be? Add an automatic http:// if it starts with a domain name.
826 $domain_match = preg_match('/^(([a-z0-9]([a-z0-9\-_]*\.)+)('. LINK_DOMAINS .'|[a-z]{2}))/i',$url);
827 if (!empty($domain_match)) {
828 $url = $protocol."://".$url;
829 }
830 }
831 }
832
833 return $url;
834 }
835
836 /**
837 * A lenient verification for URLs. Accepts all URLs following RFC 1738 standard
838 * for URL formation and all email addresses following the RFC 2368 standard for
839 * mailto address formation.
840 *
841 * @param string $text
842 * @return mixed Returns boolean FALSE if the URL is not valid. On success, returns an object with
843 * the following attributes: protocol, hostname, ip, and port.
844 */
845 function link_validate_url($text) {
846
847 $allowed_protocols = variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'));
848
849 $protocol = '((' . implode("|", $allowed_protocols) . '):\/\/)';
850 $authentication = '([a-z0-9]+(:[a-z0-9]+)?@)';
851 $domain = '(([a-z0-9]([a-z0-9\-_\[\]])*)(\.(([a-z0-9\-_\[\]])+\.)*('. LINK_DOMAINS .'|[a-z]{2}))?)';
852 $ipv4 = '([0-9]{1,3}(\.[0-9]{1,3}){3})';
853 $ipv6 = '([0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})';
854 $port = '(:([0-9]{1,5}))';
855
856 // Pattern specific to eternal links.
857 $external_pattern = '/^' . $protocol . '?' . $authentication . '?' . '(' . $domain . '|' . $ipv4 . '|' . $ipv6 . ' |localhost)' . $port . '?';
858
859 // Pattern specific to internal links.
860 $internal_pattern = "/^([a-z0-9_\-+\[\]]+)";
861
862 $directories = "(\/[a-z0-9_\-\.~+%=&,$'():;*@\[\]]*)*";
863 // Yes, four backslashes == a single backslash.
864 $query = "(\/?\?([?a-z0-9+_|\-\.\/\\\\%=&,$'():;*@\[\]]*))";
865 $anchor = "(#[a-z0-9_\-\.~+%=&,$'():;*@\[\]]*)";
866
867 // The rest of the path for a standard URL.
868 $end = $directories . '?' . $query . '?' . $anchor . '?' . '$/i';
869
870 $user = '[a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'\[\]]+';
871 $email_pattern = '/^mailto:' . $user . '@' . '(' . $domain . '|' . $ipv4 .'|'. $ipv6 . '|localhost)' . $query . '?$/';
872
873 if (strpos($text, '<front>') === 0) {
874 return LINK_FRONT;
875 }
876 if (in_array('mailto', $allowed_protocols) && preg_match($email_pattern, $text)) {
877 return LINK_EMAIL;
878 }
879 if (preg_match($internal_pattern . $end, $text)) {
880 return LINK_INTERNAL;
881 }
882 if (preg_match($external_pattern . $end, $text)) {
883 return LINK_EXTERNAL;
884 }
885
886 return FALSE;
887 }