link.module

Tracking 5.x-1.x branch
  1. drupal
    1. 5 contributions/link/link.module
    2. 6 contributions/link/link.module
    3. 7 contributions/link/link.module
    4. 8 drupal/core/modules/link/link.module

Defines simple link field types.

Functions & methods

NameDescription
link_cleanup_urlForms a valid URL if possible from an entered address. Trims whitespace and automatically adds an http:// to addresses without a protocol specified
link_fieldImplementation of hook_field().
link_field_formatterImplementation of hook_field_formatter().
link_field_formatter_infoImplementation of hook_field_formatter_info().
link_field_infoImplementation of hook_field_info().
link_field_settingsImplementation of hook_field_settings().
link_menuImplementation of hook_menu().
link_validate_urlA lenient verification for URLs. Accepts all URLs following RFC 1738 standard for URL formation and all email addresses following the RFC 2368 standard for mailto address formation.
link_views_argument_handlerViews module argument handler for link fields
link_views_protocol_filter_handlerViews modules filter handler for link protocol filtering
link_widgetImplementation of hook_widget().
link_widget_infoImplementation of hook_widget_info().
link_widget_js
theme_link_field_settingsTheme the settings form for the link field.
theme_link_widget_formTheme the display of the entire link set
theme_link_widget_form_rowTheme the display of a single form row
_link_field_formatter_titleHelper function for link_field_formatter().
_link_widget_form
_link_widget_prepare
_link_widget_process
_link_widget_validate

Constants

NameDescription
LINK_DOMAINS
LINK_EMAIL
LINK_EXTERNAL
LINK_FRONT
LINK_ICHARS
LINK_INTERNAL

File

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