link.module 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106
  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_NEWS', 'news');
  11. 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');
  12. define('LINK_TARGET_DEFAULT', 'default');
  13. define('LINK_TARGET_NEW_WINDOW', '_blank');
  14. define('LINK_TARGET_TOP', '_top');
  15. define('LINK_TARGET_USER', 'user');
  16. /**
  17. * Maximum URLs length - needs to match value in link.install.
  18. */
  19. define('LINK_URL_MAX_LENGTH', 2048);
  20. /**
  21. * Implements hook_field_info().
  22. */
  23. function link_field_info() {
  24. return array(
  25. 'link_field' => array(
  26. 'label' => t('Link'),
  27. 'description' => t('Store a title, href, and attributes in the database to assemble a link.'),
  28. 'settings' => array(
  29. 'attributes' => _link_default_attributes(),
  30. 'url' => 0,
  31. 'title' => 'optional',
  32. 'title_value' => '',
  33. 'title_maxlength' => 128, //patch #1307788 from nmc
  34. 'enable_tokens' => 1,
  35. 'display' => array(
  36. 'url_cutoff' => 80,
  37. ),
  38. ),
  39. 'instance_settings' => array(
  40. 'attributes' => _link_default_attributes(),
  41. 'url' => 0,
  42. 'title' => 'optional',
  43. 'title_value' => '',
  44. 'title_maxlength' => 128, // patch #1307788 from nmc
  45. 'enable_tokens' => 1,
  46. 'display' => array(
  47. 'url_cutoff' => 80,
  48. ),
  49. 'validate_url' => 1,
  50. ),
  51. 'default_widget' => 'link_field',
  52. 'default_formatter' => 'link_default',
  53. // Support hook_entity_property_info() from contrib "Entity API".
  54. 'property_type' => 'field_item_link',
  55. 'property_callbacks' => array('link_field_property_info_callback'),
  56. ),
  57. );
  58. }
  59. /**
  60. * Implements hook_field_instance_settings_form().
  61. */
  62. function link_field_instance_settings_form($field, $instance) {
  63. $form = array(
  64. '#element_validate' => array('link_field_settings_form_validate'),
  65. );
  66. $form['validate_url'] = array(
  67. '#type' => 'checkbox',
  68. '#title' => t('Validate URL'),
  69. '#default_value' => isset($instance['settings']['validate_url']) && ($instance['settings']['validate_url'] !== '') ? $instance['settings']['validate_url'] : TRUE,
  70. '#description' => t('If checked, the URL field will be verified as a valid URL during validation.'),
  71. );
  72. $form['url'] = array(
  73. '#type' => 'checkbox',
  74. '#title' => t('Optional URL'),
  75. '#default_value' => isset($instance['settings']['url']) ? $instance['settings']['url'] : '',
  76. '#return_value' => 'optional',
  77. '#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.'),
  78. );
  79. $title_options = array(
  80. 'optional' => t('Optional Title'),
  81. 'required' => t('Required Title'),
  82. 'value' => t('Static Title'),
  83. 'none' => t('No Title'),
  84. );
  85. $form['title'] = array(
  86. '#type' => 'radios',
  87. '#title' => t('Link Title'),
  88. '#default_value' => isset($instance['settings']['title']) ? $instance['settings']['title'] : 'optional',
  89. '#options' => $title_options,
  90. '#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.'),
  91. );
  92. $form['title_value'] = array(
  93. '#type' => 'textfield',
  94. '#title' => t('Static title'),
  95. '#default_value' => isset($instance['settings']['title_value']) ? $instance['settings']['title_value'] : '',
  96. '#description' => t('This title will always be used if &ldquo;Static Title&rdquo; is selected above.'),
  97. );
  98. $form['title_maxlength'] = array( // patch #1307788 from nmc
  99. '#type' => 'textfield',
  100. '#title' => t('Max length of title field'),
  101. '#default_value' => isset($instance['settings']['title_maxlength']) ? $instance['settings']['title_maxlength'] : '128',
  102. '#description' => t('Set a maximum length on the title field (applies only if Link Title is optional or required). The maximum limit is 255 characters.'),
  103. '#maxlength' => 3,
  104. '#size' => 3,
  105. );
  106. if (module_exists('token')) {
  107. // Add token module replacements fields
  108. $form['tokens'] = array(
  109. '#type' => 'fieldset',
  110. '#collapsible' => TRUE,
  111. '#collapsed' => TRUE,
  112. '#title' => t('Placeholder tokens'),
  113. '#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."),
  114. );
  115. $token_type = array(
  116. 'theme' => 'token_tree',
  117. 'token_types' => array($instance['entity_type']),
  118. 'global_types' => TRUE,
  119. 'click_insert' => TRUE,
  120. 'recursion_limit' => 2,
  121. );
  122. $form['tokens']['help'] = array(
  123. '#type' => 'markup',
  124. '#markup' => theme('token_tree', $token_type),
  125. );
  126. $form['enable_tokens'] = array(
  127. '#type' => 'checkbox',
  128. '#title' => t('Allow user-entered tokens'),
  129. '#default_value' => isset($instance['settings']['enable_tokens']) ? $instance['settings']['enable_tokens'] : 1,
  130. '#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.'),
  131. );
  132. }
  133. $form['display'] = array(
  134. '#tree' => TRUE,
  135. );
  136. $form['display']['url_cutoff'] = array(
  137. '#type' => 'textfield',
  138. '#title' => t('URL Display Cutoff'),
  139. '#default_value' => isset($instance['settings']['display']['url_cutoff']) ? $instance['settings']['display']['url_cutoff'] : '80',
  140. '#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.'),
  141. '#maxlength' => 3,
  142. '#size' => 3,
  143. );
  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($instance['settings']['attributes']['target']) ? LINK_TARGET_DEFAULT : $instance['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($instance['settings']['attributes']['rel']) ? '' : $instance['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 this class attribute. Multiple classes should be separated by spaces.'),
  172. '#default_value' => empty($instance['settings']['attributes']['class']) ? '' : $instance['settings']['attributes']['class'],
  173. );
  174. $form['attributes']['configurable_title'] = array(
  175. '#title' => t("Allow the user to enter a link 'title' attribute"),
  176. '#type' => 'checkbox',
  177. '#default_value' => empty($instance['settings']['attributes']['configurable_title']) ? '' : $instance['settings']['attributes']['configurable_title'],
  178. );
  179. $form['attributes']['title'] = array(
  180. '#title' => t("Default link 'title' Attribute"),
  181. '#type' => 'textfield',
  182. '#description' => t('When output, links will use this "title" attribute if the user does not provide one and when different from the link text. Read <a href="http://www.w3.org/TR/WCAG10-HTML-TECHS/#links">WCAG 1.0 Guidelines</a> for links comformances. Tokens values will be evaluated.'),
  183. '#default_value' => empty($instance['settings']['attributes']['title']) ? '' : $instance['settings']['attributes']['title'],
  184. '#field_prefix' => 'title = "',
  185. '#field_suffix' => '"',
  186. '#size' => 20,
  187. );
  188. return $form;
  189. }
  190. /**
  191. * Validate the field settings form.
  192. */
  193. function link_field_settings_form_validate($element, &$form_state, $complete_form) {
  194. if ($form_state['values']['instance']['settings']['title'] === 'value'
  195. && empty($form_state['values']['instance']['settings']['title_value'])) {
  196. form_set_error('title_value', t('A default title must be provided if the title is a static value.'));
  197. }
  198. if (!empty($form_state['values']['instance']['settings']['display']['url_cutoff']) // patch #1307788 from nmc
  199. && !is_numeric($form_state['values']['instance']['settings']['display']['url_cutoff'])) {
  200. form_set_error('display', t('URL Display Cutoff value must be numeric.'));
  201. }
  202. if (empty($form_state['values']['instance']['settings']['title_maxlength'])) { // patch #1307788 from nmc
  203. form_set_value($element['title_maxlength'], '128', $form_state);
  204. } elseif (!is_numeric($form_state['values']['instance']['settings']['title_maxlength'])) {
  205. form_set_error('title_maxlength', t('The max length of the link title must be numeric.'));
  206. } elseif ($form_state['values']['instance']['settings']['title_maxlength'] > 255) {
  207. form_set_error('title_maxlength', t('The max length of the link title cannot be greater than 255 characters.'));
  208. }
  209. }
  210. /**
  211. * Implement hook_field_is_empty().
  212. */
  213. function link_field_is_empty($item, $field) {
  214. return empty($item['title']) && empty($item['url']);
  215. }
  216. /**
  217. * Implements hook_field_load().
  218. */
  219. function link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  220. foreach ($entities as $id => $entity) {
  221. foreach ($items[$id] as $delta => $item) {
  222. $items[$id][$delta]['attributes'] = _link_load($field, $item, $instances[$id]);
  223. }
  224. }
  225. }
  226. /**
  227. * Implements hook_field_validate().
  228. */
  229. function link_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  230. $optional_field_found = FALSE;
  231. if ($instance['settings']['validate_url'] !== 0 || is_null($instance['settings']['validate_url']) || !isset($instance['settings']['validate_url'])) {
  232. foreach ($items as $delta => $value) {
  233. _link_validate($items[$delta], $delta, $field, $entity, $instance, $optional_field_found);
  234. }
  235. }
  236. if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] && !$optional_field_found) {
  237. form_set_error($field['field_name'] .'][0][title', t('At least one title or URL must be entered.'));
  238. }
  239. }
  240. /**
  241. * Implements hook_field_presave().
  242. */
  243. function link_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
  244. foreach ($items as $delta => $value) {
  245. _link_process($items[$delta], $delta, $field, $entity);
  246. }
  247. }
  248. /**
  249. * Implements hook_field_prepare_view().
  250. */
  251. function link_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  252. foreach ($items as $entity_id => $entity_items) {
  253. foreach ($entity_items as $delta => $value) {
  254. _link_sanitize($items[$entity_id][$delta], $delta, $field, $instances[$entity_id], $entities[$entity_id]);
  255. }
  256. }
  257. }
  258. /**
  259. * Implements hook_field_widget_info().
  260. */
  261. function link_field_widget_info() {
  262. return array(
  263. 'link_field' => array(
  264. 'label' => 'Link',
  265. 'field types' => array('link_field'),
  266. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  267. ),
  268. );
  269. }
  270. /**
  271. * Implements hook_field_widget_form().
  272. */
  273. function link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  274. $element += array(
  275. '#type' => $instance['widget']['type'],
  276. '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
  277. );
  278. return $element;
  279. }
  280. /**
  281. * Unpacks the item attributes for use.
  282. */
  283. function _link_load($field, $item, $instance) {
  284. /*return $item['attributes'] = isset($item['attributes']) ?
  285. unserialize($item['attributes']) :
  286. $instance['settings']['attributes'];*/
  287. if (isset($item['attributes'])) {
  288. return unserialize($item['attributes']);
  289. }
  290. else if (isset($instance['settings']['attributes'])) {
  291. return $instance['settings']['attributes'];
  292. }
  293. else {
  294. return $field['settings']['attributes'];
  295. }
  296. }
  297. /**
  298. * Prepares the item attributes and url for storage.
  299. */
  300. function _link_process(&$item, $delta = 0, $field, $entity) {
  301. // Trim whitespace from URL.
  302. $item['url'] = trim($item['url']);
  303. // if no attributes are set then make sure $item['attributes'] is an empty array - this lets $field['attributes'] override it.
  304. if (empty($item['attributes'])) {
  305. $item['attributes'] = array();
  306. }
  307. // Serialize the attributes array.
  308. if (!is_string($item['attributes'])) {
  309. $item['attributes'] = serialize($item['attributes']);
  310. }
  311. // Don't save an invalid default value (e.g. 'http://').
  312. if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url'])
  313. && is_object($node)) {
  314. if (!link_validate_url($item['url'])) {
  315. unset($item['url']);
  316. }
  317. }
  318. }
  319. /**
  320. * Validates that the link field has been entered properly.
  321. */
  322. function _link_validate(&$item, $delta, $field, $node, $instance, &$optional_field_found) {
  323. if ($item['url']
  324. && !(isset($instance['default_value'][$delta]['url'])
  325. && $item['url'] === $instance['default_value'][$delta]['url']
  326. && !$instance['required'])) {
  327. // Validate the link.
  328. if (link_validate_url(trim($item['url'])) == FALSE) {
  329. form_set_error($field['field_name'] .']['. $delta .'][url', t('Not a valid URL.'));
  330. }
  331. // Require a title for the link if necessary.
  332. if ($instance['settings']['title'] == 'required' && strlen(trim($item['title'])) == 0) {
  333. form_set_error($field['field_name'] .']['. $delta .'][title', t('Titles are required for all links.'));
  334. }
  335. }
  336. // Require a link if we have a title.
  337. if ($instance['settings']['url'] !== 'optional'
  338. && strlen(isset($item['title']) ? $item['title'] : NULL) > 0
  339. && strlen(trim($item['url'])) == 0) {
  340. form_set_error($field['field_name'] .']['. $delta .'][url', t('You cannot enter a title without a link url.'));
  341. }
  342. // In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link.
  343. if ($instance['settings']['url'] === 'optional'
  344. && $instance['settings']['title'] === 'optional'
  345. && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
  346. $optional_field_found = TRUE;
  347. }
  348. // Require entire field
  349. if ($instance['settings']['url'] === 'optional'
  350. && $instance['settings']['title'] === 'optional'
  351. && $instance['required'] == 1
  352. && !$optional_field_found
  353. && isset($instance['id'])) {
  354. form_set_error($instance['field_name'] .'][0][title',
  355. t('At least one title or URL must be entered.'));
  356. }
  357. }
  358. /**
  359. * Cleanup user-entered values for a link field according to field settings.
  360. *
  361. * @param $item
  362. * A single link item, usually containing url, title, and attributes.
  363. * @param $delta
  364. * The delta value if this field is one of multiple fields.
  365. * @param $field
  366. * The CCK field definition.
  367. * @param $node
  368. * The node containing this link.
  369. */
  370. function _link_sanitize(&$item, $delta, &$field, $instance, &$node) {
  371. // Don't try to process empty links.
  372. if (empty($item['url']) && empty($item['title'])) {
  373. return;
  374. }
  375. // Replace URL tokens.
  376. if (isset($instance['settings']['enable_tokens']) && $instance['settings']['enable_tokens']) {
  377. global $user;
  378. // Load the node if necessary for nodes in views.
  379. $token_node = isset($node->nid) ? node_load($node->nid) : $node;
  380. $item['url'] = token_replace($item['url'], array('node' => $token_node));
  381. }
  382. $type = link_validate_url($item['url']);
  383. // If we can't determine the type of url, and we've been told not to validate it,
  384. // then we assume it's a LINK_EXTERNAL type for later processing. #357604
  385. if ($type == FALSE && $instance['settings']['validate_url'] === 0) {
  386. $type = LINK_EXTERNAL;
  387. }
  388. $url = link_cleanup_url($item['url']);
  389. // Separate out the anchor if any.
  390. if (strpos($url, '#') !== FALSE) {
  391. $item['fragment'] = substr($url, strpos($url, '#') + 1);
  392. $url = substr($url, 0, strpos($url, '#'));
  393. }
  394. // Separate out the query string if any.
  395. if (strpos($url, '?') !== FALSE) {
  396. $query = substr($url, strpos($url, '?') + 1);
  397. parse_str($query, $query_array);
  398. $item['query'] = $query_array;
  399. $url = substr($url, 0, strpos($url, '?'));
  400. }
  401. $item['url'] = check_plain($url);
  402. // Create a shortened URL for display.
  403. $display_url = $type == LINK_EMAIL ?
  404. str_replace('mailto:', '', $url) :
  405. url($url, array('query' => isset($item['query']) ?
  406. $item['query'] :
  407. NULL,
  408. 'fragment' => isset($item['fragment']) ?
  409. $item['fragment'] :
  410. NULL,
  411. 'absolute' => TRUE));
  412. if ($instance['settings']['display']['url_cutoff'] && strlen($display_url) > $instance['settings']['display']['url_cutoff']) {
  413. $display_url = substr($display_url, 0, $instance['settings']['display']['url_cutoff']) ."...";
  414. }
  415. $item['display_url'] = $display_url;
  416. // Use the title defined at the instance level.
  417. if ($instance['settings']['title'] == 'value' && strlen(trim($instance['settings']['title_value']))) {
  418. $title = $instance['settings']['title_value'];
  419. }
  420. // Use the title defined by the user at the widget level.
  421. else if (isset($item['title'])) {
  422. $title = $item['title'];
  423. }
  424. else {
  425. $title = '';
  426. }
  427. // Replace tokens.
  428. if ($title && ($instance['settings']['title'] == 'value' || $instance['settings']['enable_tokens'])) {
  429. // Load the node if necessary for nodes in views.
  430. $token_node = isset($node->nid) ? node_load($node->nid) : $node;
  431. $title = filter_xss(token_replace($title, array('node' => $token_node)),
  432. array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
  433. $item['html'] = TRUE;
  434. }
  435. $item['title'] = empty($title) ? $item['display_url'] : $title;
  436. if (!isset($item['attributes'])) {
  437. $item['attributes'] = array();
  438. }
  439. // Unserialize attributtes array if it has not been unserialized yet.
  440. if (!is_array($item['attributes'])) {
  441. $item['attributes'] = (array)unserialize($item['attributes']);
  442. }
  443. // Add default attributes.
  444. if (!is_array($instance['settings']['attributes'])){
  445. $instance['settings']['attributes'] = _link_default_attributes();
  446. }
  447. else {
  448. $instance['settings']['attributes'] += _link_default_attributes();
  449. }
  450. // Merge item attributes with attributes defined at the field level.
  451. $item['attributes'] += $instance['settings']['attributes'];
  452. // If user is not allowed to choose target attribute, use default defined at
  453. // field level.
  454. if ($instance['settings']['attributes']['target'] != LINK_TARGET_USER) {
  455. $item['attributes']['target'] = $instance['settings']['attributes']['target'];
  456. }
  457. // Remove the target attribute if the default (no target) is selected.
  458. if (empty($item['attributes']) || $item['attributes']['target'] == LINK_TARGET_DEFAULT) {
  459. unset($item['attributes']['target']);
  460. }
  461. // Remove the rel=nofollow for internal links.
  462. if ($type != LINK_EXTERNAL && strpos($item['attributes']['rel'], 'nofollow') !== FALSE) {
  463. $item['attributes']['rel'] = str_replace('nofollow', '', $item['attributes']);
  464. }
  465. // Handle "title" link attribute.
  466. if (!empty($item['attributes']['title']) && module_exists('token')) {
  467. // Load the node (necessary for nodes in views).
  468. $token_node = isset($node->nid) ? node_load($node->nid) : $node;
  469. $item['attributes']['title'] = filter_xss(token_replace($item['attributes']['title'], array('node' => $token_node)),
  470. array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
  471. }
  472. // Remove title attribute if it's equal to link text.
  473. if (isset($item['attributes']['title']) && $item['attributes']['title'] == $item['title']) {
  474. unset($item['attributes']['title']);
  475. }
  476. unset($item['attributes']['configurable_title']);
  477. // Remove empty attributes.
  478. $item['attributes'] = array_filter($item['attributes']);
  479. // Sets title to trimmed url if one exists
  480. // @TODO: Do we need this? It seems not.
  481. /*if(!empty($item['display_url']) && empty($item['title'])) {
  482. $item['title'] = $item['display_url'];
  483. }
  484. elseif(!isset($item['title'])) {
  485. $item['title'] = $item['url'];
  486. }*/
  487. }
  488. /**
  489. * Implements hook_theme().
  490. */
  491. function link_theme() {
  492. return array(
  493. /*'link_field_settings' => array(
  494. 'variables' => array('element' => NULL),
  495. ),*/
  496. 'link_formatter_link_default' => array(
  497. 'variables' => array('element' => NULL),
  498. ),
  499. 'link_formatter_link_plain' => array(
  500. 'variables' => array('element' => NULL),
  501. ),
  502. 'link_formatter_link_title_plain' => array(
  503. 'variables' => array('element' => NULL),
  504. ),
  505. 'link_formatter_link_url' => array(
  506. 'variables' => array('element' => NULL),
  507. ),
  508. 'link_formatter_link_short' => array(
  509. 'variables' => array('element' => NULL),
  510. ),
  511. 'link_formatter_link_label' => array(
  512. 'variables' => array('element' => NULL),
  513. ),
  514. 'link_formatter_link_separate' => array(
  515. 'variables' => array('element' => NULL),
  516. ),
  517. 'link_field' => array(
  518. 'render element' => 'element',
  519. ),
  520. );
  521. }
  522. /**
  523. * FAPI theme for an individual text elements.
  524. */
  525. function theme_link_field($vars) {
  526. drupal_add_css(drupal_get_path('module', 'link') .'/link.css');
  527. $element = $vars['element'];
  528. // Prefix single value link fields with the name of the field.
  529. if (empty($element['#field']['multiple'])) {
  530. if (isset($element['url']) && !isset($element['title'])) {
  531. unset($element['url']['#title']);
  532. }
  533. }
  534. $output = '';
  535. $output .= '<div class="link-field-subrow clearfix">';
  536. if (isset($element['title'])) {
  537. $output .= '<div class="link-field-title link-field-column">'. drupal_render($element['title']) .'</div>';
  538. }
  539. $output .= '<div class="link-field-url'. (isset($element['title']) ? ' link-field-column' : '') .'">'. drupal_render($element['url']) .'</div>';
  540. $output .= '</div>';
  541. if (!empty($element['attributes']['target'])) {
  542. $output .= '<div class="link-attributes">'. drupal_render($element['attributes']['target']) .'</div>';
  543. }
  544. if (!empty($element['attributes']['title'])) {
  545. $output .= '<div class="link-attributes">'. drupal_render($element['attributes']['title']) .'</div>';
  546. }
  547. return $output;
  548. }
  549. /**
  550. * Implements hook_element_info().
  551. */
  552. function link_element_info() {
  553. $elements = array();
  554. $elements['link_field'] = array(
  555. '#input' => TRUE,
  556. '#process' => array('link_field_process'),
  557. '#theme' => 'link_field',
  558. '#theme_wrappers' => array('form_element'),
  559. );
  560. return $elements;
  561. }
  562. function _link_default_attributes() {
  563. return array(
  564. 'target' => LINK_TARGET_DEFAULT,
  565. 'class' => '',
  566. 'rel' => '',
  567. );
  568. }
  569. /**
  570. * Process the link type element before displaying the field.
  571. *
  572. * Build the form element. When creating a form using FAPI #process,
  573. * note that $element['#value'] is already set.
  574. *
  575. * The $fields array is in $complete_form['#field_info'][$element['#field_name']].
  576. */
  577. function link_field_process($element, $form_state, $complete_form) {
  578. $instance = field_widget_instance($element, $form_state);
  579. $settings = $instance['settings'];
  580. $element['url'] = array(
  581. '#type' => 'textfield',
  582. '#maxlength' => LINK_URL_MAX_LENGTH,
  583. '#title' => t('URL'),
  584. '#required' => ($element['#delta'] == 0 && $settings['url'] !== 'optional') ? $element['#required'] : FALSE,
  585. '#default_value' => isset($element['#value']['url']) ? $element['#value']['url'] : NULL,
  586. );
  587. if ($settings['title'] !== 'none' && $settings['title'] !== 'value') {
  588. $element['title'] = array(
  589. '#type' => 'textfield',
  590. '#maxlength' => $settings['title_maxlength'], // patch #1307788 from nmc
  591. '#title' => t('Title'),
  592. '#description' => t('The link title is limited to '.$settings['title_maxlength'].' characters maximum.'), // patch #1307788 from nmc
  593. '#required' => ($settings['title'] == 'required' && (($element['#delta'] == 0 && $element['#required']) || !empty($element['#value']['url']))) ? TRUE : FALSE, // davereids patch from jan 2011
  594. '#default_value' => isset($element['#value']['title']) ? $element['#value']['title'] : NULL,
  595. );
  596. }
  597. // Initialize field attributes as an array if it is not an array yet.
  598. if (!is_array($settings['attributes'])) {
  599. $settings['attributes'] = array();
  600. }
  601. // Add default attributes.
  602. $settings['attributes'] += _link_default_attributes();
  603. $attributes = isset($element['#value']['attributes']) ? $element['#value']['attributes'] : $settings['attributes'];
  604. if (!empty($settings['attributes']['target']) && $settings['attributes']['target'] == LINK_TARGET_USER) {
  605. $element['attributes']['target'] = array(
  606. '#type' => 'checkbox',
  607. '#title' => t('Open URL in a New Window'),
  608. '#return_value' => LINK_TARGET_NEW_WINDOW,
  609. '#default_value' => isset($attributes['target']) ? $attributes['target'] : FALSE,
  610. );
  611. }
  612. if (!empty($settings['attributes']['configurable_title']) && $settings['attributes']['configurable_title'] == 1) {
  613. $element['attributes']['title'] = array(
  614. '#type' => 'textfield',
  615. '#title' => t('Link "title" attribute'),
  616. '#default_value' => isset($attributes['title']) ? $attributes['title'] : '',
  617. '#field_prefix' => 'title = "',
  618. '#field_suffix' => '"',
  619. );
  620. }
  621. // To prevent an extra required indicator, disable the required flag on the
  622. // base element since all the sub-fields are already required if desired.
  623. $element['#required'] = FALSE; // davereids patch from jan 2011
  624. return $element;
  625. }
  626. /**
  627. * Implementation of hook_field_formatter_info().
  628. */
  629. function link_field_formatter_info() {
  630. return array(
  631. 'link_default' => array(
  632. 'label' => t('Title, as link (default)'),
  633. 'field types' => array('link_field'),
  634. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  635. ),
  636. 'link_title_plain' => array(
  637. 'label' => t('Title, as plain text'),
  638. 'field types' => array('link_field'),
  639. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  640. ),
  641. 'link_url' => array(
  642. 'label' => t('URL, as link'),
  643. 'field types' => array('link_field'),
  644. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  645. ),
  646. 'link_plain' => array(
  647. 'label' => t('URL, as plain text'),
  648. 'field types' => array('link_field'),
  649. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  650. ),
  651. 'link_short' => array(
  652. 'label' => t('Short, as link with title "Link"'),
  653. 'field types' => array('link_field'),
  654. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  655. ),
  656. 'link_label' => array(
  657. 'label' => t('Label, as link with label as title'),
  658. 'field types' => array('link_field'),
  659. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  660. ),
  661. 'link_separate' => array(
  662. 'label' => t('Separate title and URL'),
  663. 'field types' => array('link_field'),
  664. 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
  665. ),
  666. );
  667. }
  668. /**
  669. * Implements hook_field_formatter_view().
  670. */
  671. function link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  672. $elements = array();
  673. foreach ($items as $delta => $item) {
  674. $elements[$delta] = array(
  675. '#markup' => theme('link_formatter_'. $display['type'], array('element' => $item, 'field' => $instance)),
  676. );
  677. }
  678. return $elements;
  679. }
  680. /**
  681. * Theme function for 'default' text field formatter.
  682. */
  683. function theme_link_formatter_link_default($vars) {
  684. $link_options = $vars['element'];
  685. unset($link_options['element']['title']);
  686. unset($link_options['element']['url']);
  687. // Issue #1199806 by ss81: Fixes fatal error when the link URl is equal to page URL
  688. if (isset($link_options['attributes']['class'])) {
  689. $link_options['attributes']['class'] = array($link_options['attributes']['class']);
  690. }
  691. // Display a normal link if both title and URL are available.
  692. if (!empty($vars['element']['title']) && !empty($vars['element']['url'])) {
  693. return l($vars['element']['title'], $vars['element']['url'], $link_options);
  694. }
  695. // If only a title, display the title.
  696. elseif (!empty($vars['element']['title'])) {
  697. return check_plain($vars['element']['title']);
  698. }
  699. elseif (!empty($vars['element']['url'])) {
  700. return l($vars['element']['title'], $vars['element']['url'], $link_options);
  701. }
  702. }
  703. /**
  704. * Theme function for 'plain' text field formatter.
  705. */
  706. function theme_link_formatter_link_plain($vars) {
  707. $link_options = $vars['element'];
  708. unset($link_options['element']['title']);
  709. unset($link_options['element']['url']);
  710. return empty($vars['element']['url']) ?
  711. check_plain($vars['element']['title']) :
  712. url($vars['element']['url'], $link_options);
  713. }
  714. /**
  715. * Theme function for 'title_plain' text field formatter.
  716. */
  717. function theme_link_formatter_link_title_plain($vars) {
  718. return empty($vars['element']['title']) ? '' : check_plain($vars['element']['title']);
  719. }
  720. /**
  721. * Theme function for 'url' text field formatter.
  722. */
  723. function theme_link_formatter_link_url($vars) {
  724. $link_options = $vars['element'];
  725. unset($link_options['element']['title']);
  726. unset($link_options['element']['url']);
  727. return $vars['element']['url'] ? l($vars['element']['display_url'], $vars['element']['url'], $link_options) : '';
  728. }
  729. /**
  730. * Theme function for 'short' text field formatter.
  731. */
  732. function theme_link_formatter_link_short($vars) {
  733. $link_options = $vars['element'];
  734. unset($link_options['element']['title']);
  735. unset($link_options['element']['url']);
  736. return $vars['element']['url'] ? l(t('Link'), $vars['element']['url'], $link_options) : '';
  737. }
  738. /**
  739. * Theme function for 'label' text field formatter.
  740. */
  741. function theme_link_formatter_link_label($vars) {
  742. $link_options = $vars['element'];
  743. unset($link_options['element']['title']);
  744. unset($link_options['element']['url']);
  745. return $vars['element']['url'] ? l($vars['field']['label'], $vars['element']['url'], $link_options) : '';
  746. }
  747. /**
  748. * Theme function for 'separate' text field formatter.
  749. */
  750. function theme_link_formatter_link_separate($vars) {
  751. $class = empty($vars['element']['attributes']['class']) ? '' : ' '. $vars['element']['attributes']['class'];
  752. unset($vars['element']['attributes']['class']);
  753. $link_options = $vars['element'];
  754. unset($link_options['element']['title']);
  755. unset($link_options['element']['url']);
  756. $title = empty($vars['element']['title']) ? '' : check_plain($vars['element']['title']);
  757. /**
  758. * @TODO static html markup looks not very elegant to me (who takes it off?)
  759. * needs smarter output solution and an optional title/url seperator (digidog)
  760. */
  761. $output = '';
  762. $output .= '<div class="link-item '. $class .'">';
  763. if (!empty($title)) {
  764. $output .= '<div class="link-title">'. $title .'</div>';
  765. }
  766. $output .= '<div class="link-url">'. l($vars['element']['url'], $vars['element']['url'], $link_options) .'</div>';
  767. $output .= '</div>';
  768. return $output;
  769. }
  770. function link_token_list($type = 'all') {
  771. if ($type === 'field' || $type === 'all') {
  772. $tokens = array();
  773. $tokens['link']['url'] = t("Link URL");
  774. $tokens['link']['title'] = t("Link title");
  775. $tokens['link']['view'] = t("Formatted html link");
  776. return $tokens;
  777. }
  778. }
  779. function link_token_values($type, $object = NULL) {
  780. if ($type === 'field') {
  781. $item = $object[0];
  782. $tokens['url'] = $item['url'];
  783. $tokens['title'] = $item['title'];
  784. $tokens['view'] = isset($item['view']) ? $item['view'] : '';
  785. return $tokens;
  786. }
  787. }
  788. /**
  789. * Implements hook_views_api().
  790. */
  791. function link_views_api() {
  792. return array(
  793. 'api' => 2,
  794. 'path' => drupal_get_path('module', 'link') .'/views',
  795. );
  796. }
  797. /**
  798. * Forms a valid URL if possible from an entered address.
  799. * Trims whitespace and automatically adds an http:// to addresses without a protocol specified
  800. *
  801. * @param string $url
  802. * @param string $protocol The protocol to be prepended to the url if one is not specified
  803. */
  804. function link_cleanup_url($url, $protocol = "http") {
  805. $url = trim($url);
  806. $type = link_validate_url($url);
  807. if ($type === LINK_EXTERNAL) {
  808. // Check if there is no protocol specified.
  809. $protocol_match = preg_match("/^([a-z0-9][a-z0-9\.\-_]*:\/\/)/i", $url);
  810. if (empty($protocol_match)) {
  811. // But should there be? Add an automatic http:// if it starts with a domain name.
  812. $domain_match = preg_match('/^(([a-z0-9]([a-z0-9\-_]*\.)+)('. LINK_DOMAINS .'|[a-z]{2}))/i', $url);
  813. if (!empty($domain_match)) {
  814. $url = $protocol ."://". $url;
  815. }
  816. }
  817. }
  818. return $url;
  819. }
  820. /**
  821. * A lenient verification for URLs. Accepts all URLs following RFC 1738 standard
  822. * for URL formation and all email addresses following the RFC 2368 standard for
  823. * mailto address formation.
  824. *
  825. * @param string $text
  826. * @return mixed Returns boolean FALSE if the URL is not valid. On success, returns an object with
  827. * the following attributes: protocol, hostname, ip, and port.
  828. */
  829. function link_validate_url($text) {
  830. $LINK_ICHARS_DOMAIN = (string) html_entity_decode(implode("", array( // @TODO completing letters ...
  831. "&#x00E6;", // æ
  832. "&#x00C6;", // Æ
  833. "&#x00C0;", // À
  834. "&#x00E0;", // à
  835. "&#x00C1;", // Á
  836. "&#x00E1;", // á
  837. "&#x00C2;", // Â
  838. "&#x00E2;", // â
  839. "&#x00E5;", // å
  840. "&#x00C5;", // Å
  841. "&#x00E4;", // ä
  842. "&#x00C4;", // Ä
  843. "&#x00C7;", // Ç
  844. "&#x00E7;", // ç
  845. "&#x00D0;", // Ð
  846. "&#x00F0;", // ð
  847. "&#x00C8;", // È
  848. "&#x00E8;", // è
  849. "&#x00C9;", // É
  850. "&#x00E9;", // é
  851. "&#x00CA;", // Ê
  852. "&#x00EA;", // ê
  853. "&#x00CB;", // Ë
  854. "&#x00EB;", // ë
  855. "&#x00CE;", // Î
  856. "&#x00EE;", // î
  857. "&#x00CF;", // Ï
  858. "&#x00EF;", // ï
  859. "&#x00F8;", // ø
  860. "&#x00D8;", // Ø
  861. "&#x00F6;", // ö
  862. "&#x00D6;", // Ö
  863. "&#x00D4;", // Ô
  864. "&#x00F4;", // ô
  865. "&#x00D5;", // Õ
  866. "&#x00F5;", // õ
  867. "&#x0152;", // Œ
  868. "&#x0153;", // œ
  869. "&#x00FC;", // ü
  870. "&#x00DC;", // Ü
  871. "&#x00D9;", // Ù
  872. "&#x00F9;", // ù
  873. "&#x00DB;", // Û
  874. "&#x00FB;", // û
  875. "&#x0178;", // Ÿ
  876. "&#x00FF;", // ÿ
  877. "&#x00D1;", // Ñ
  878. "&#x00F1;", // ñ
  879. "&#x00FE;", // þ
  880. "&#x00DE;", // Þ
  881. "&#x00FD;", // ý
  882. "&#x00DD;", // Ý
  883. "&#x00BF;", // ¿
  884. )), ENT_QUOTES, 'UTF-8');
  885. $LINK_ICHARS = $LINK_ICHARS_DOMAIN . (string) html_entity_decode(implode("", array(
  886. "&#x00DF;", // ß
  887. )), ENT_QUOTES, 'UTF-8');
  888. $allowed_protocols = variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'));
  889. // Starting a parenthesis group with (?: means that it is grouped, but is not captured
  890. $protocol = '((?:'. implode("|", $allowed_protocols) .'):\/\/)';
  891. $authentication = "(?:(?:(?:[\w\.\-\+!$&'\(\)*\+,;=" . $LINK_ICHARS . "]|%[0-9a-f]{2})+(?::(?:[\w". $LINK_ICHARS ."\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})*)?)?@)";
  892. $domain = '(?:(?:[a-z0-9' . $LINK_ICHARS_DOMAIN . ']([a-z0-9'. $LINK_ICHARS_DOMAIN . '\-_\[\]])*)(\.(([a-z0-9' . $LINK_ICHARS_DOMAIN . '\-_\[\]])+\.)*('. LINK_DOMAINS .'|[a-z]{2}))?)';
  893. $ipv4 = '(?:[0-9]{1,3}(\.[0-9]{1,3}){3})';
  894. $ipv6 = '(?:[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})';
  895. $port = '(?::([0-9]{1,5}))';
  896. // Pattern specific to external links.
  897. $external_pattern = '/^'. $protocol .'?'. $authentication .'?('. $domain .'|'. $ipv4 .'|'. $ipv6 .' |localhost)'. $port .'?';
  898. // Pattern specific to internal links.
  899. $internal_pattern = "/^(?:[a-z0-9". $LINK_ICHARS ."_\-+\[\]]+)";
  900. $internal_pattern_file = "/^(?:[a-z0-9". $LINK_ICHARS ."_\-+\[\]\.]+)$/i";
  901. $directories = "(?:\/[a-z0-9". $LINK_ICHARS ."_\-\.~+%=&,$'#!():;*@\[\]]*)*";
  902. // Yes, four backslashes == a single backslash.
  903. $query = "(?:\/?\?([?a-z0-9". $LINK_ICHARS ."+_|\-\.~\/\\\\%=&,$'():;*@\[\]{} ]*))";
  904. $anchor = "(?:#[a-z0-9". $LINK_ICHARS ."_\-\.~+%=&,$'():;*@\[\]\/\?]*)";
  905. // The rest of the path for a standard URL.
  906. $end = $directories .'?'. $query .'?'. $anchor .'?'.'$/i';
  907. $message_id = '[^@].*@'. $domain;
  908. $newsgroup_name = '(?:[0-9a-z+-]*\.)*[0-9a-z+-]*';
  909. $news_pattern = '/^news:('. $newsgroup_name .'|'. $message_id .')$/i';
  910. $user = '[a-zA-Z0-9'. $LINK_ICHARS .'_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'\[\]]+';
  911. $email_pattern = '/^mailto:'. $user .'@'.'(?:'. $domain .'|'. $ipv4 .'|'. $ipv6 .'|localhost)'. $query .'?$/';
  912. if (strpos($text, '<front>') === 0) {
  913. return LINK_FRONT;
  914. }
  915. if (in_array('mailto', $allowed_protocols) && preg_match($email_pattern, $text)) {
  916. return LINK_EMAIL;
  917. }
  918. if (in_array('news', $allowed_protocols) && preg_match($news_pattern, $text)) {
  919. return LINK_NEWS;
  920. }
  921. if (preg_match($internal_pattern . $end, $text)) {
  922. return LINK_INTERNAL;
  923. }
  924. if (preg_match($external_pattern . $end, $text)) {
  925. return LINK_EXTERNAL;
  926. }
  927. if (preg_match($internal_pattern_file, $text)) {
  928. return LINK_INTERNAL;
  929. }
  930. return FALSE;
  931. }
  932. /**
  933. * Implements hook_migrate_field_alter().
  934. */
  935. function link_content_migrate_field_alter(&$field_value, $instance_value) {
  936. if ($field_value['type'] == 'link') {
  937. // need to change the type:
  938. $field_value['type'] = 'link_field';
  939. }
  940. }
  941. /**
  942. * Implements hook_migrate_instance_alter().
  943. *
  944. * Widget type also changed to link_field.
  945. */
  946. function link_content_migrate_instance_alter(&$instance_value, $field_value) {
  947. if ($instance_value['widget']['module'] == 'link'
  948. && $instance_value['widget']['type'] == 'link') {
  949. $instance_value['widget']['type'] = 'link_field';
  950. }
  951. }
  952. /**
  953. * Implements hook_field_settings_form().
  954. */
  955. function link_field_settings_form() {
  956. return array();
  957. }
  958. /**
  959. * Additional callback to adapt the property info of link fields.
  960. * @see entity_metadata_field_entity_property_info().
  961. */
  962. function link_field_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  963. $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  964. // Define a data structure so it's possible to deal with both the link title
  965. // and URL.
  966. $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  967. $property['setter callback'] = 'entity_metadata_field_verbatim_set';
  968. // Auto-create the field item as soon as a property is set.
  969. $property['auto creation'] = 'link_field_item_create';
  970. $property['property info'] = link_field_item_property_info();
  971. $property['property info']['url']['required'] = !$instance['settings']['url'];
  972. $property['property info']['title']['required'] = ($instance['settings']['title'] == 'required');
  973. if ($instance['settings']['title'] == 'none') {
  974. unset($property['property info']['title']);
  975. }
  976. unset($property['query callback']);
  977. }
  978. /**
  979. * Callback for creating a new, empty link field item.
  980. *
  981. * @see link_field_property_info_callback()
  982. */
  983. function link_field_item_create() {
  984. return array('title' => NULL, 'url' => NULL);
  985. }
  986. /**
  987. * Defines info for the properties of the link-field item data structure.
  988. */
  989. function link_field_item_property_info() {
  990. $properties['title'] = array(
  991. 'type' => 'text',
  992. 'label' => t('The title of the link.'),
  993. 'setter callback' => 'entity_property_verbatim_set',
  994. );
  995. $properties['url'] = array(
  996. 'type' => 'uri',
  997. 'label' => t('The URL of the link.'),
  998. 'setter callback' => 'entity_property_verbatim_set',
  999. );
  1000. return $properties;
  1001. }