link.module 61.7 KB
Newer Older
Nathan Haug's avatar
Nathan Haug committed
1
2
3
4
5
6
7
<?php

/**
 * @file
 * Defines simple link field types.
 */

8
9
10
11
define('LINK_EXTERNAL', 'external');
define('LINK_INTERNAL', 'internal');
define('LINK_FRONT', 'front');
define('LINK_EMAIL', 'email');
12
define('LINK_NEWS', 'news');
13
define('LINK_FILE', 'file');
14
15
16
17
define('LINK_TARGET_DEFAULT', 'default');
define('LINK_TARGET_NEW_WINDOW', '_blank');
define('LINK_TARGET_TOP', '_top');
define('LINK_TARGET_USER', 'user');
18

Nathan Haug's avatar
Nathan Haug committed
19
/**
20
 * Maximum URLs length - needs to match value in link.install.
21
22
23
 */
define('LINK_URL_MAX_LENGTH', 2048);

24
25
26
27
28
29
/**
 * Implements hook_help().
 */
function link_help($path, $arg) {
  switch ($path) {
    case 'admin/help#link':
30
      $output = '<p><strong>About</strong></p>';
31
32
33
      $output .= '<p>' . 'The link provides a standard custom content field for links. Links can be easily added to any content types and profiles and include advanced validating and different ways of storing internal or external links and URLs. It also supports additional link text title, site wide tokens for titles and title attributes, target attributes, css class attribution, static repeating values, input conversion, and many more.' . '</p>';
      $output .= '<p>' . '<strong>Requirements / Dependencies</strong>' . '</p>';
      $output .= '<p>' . 'Fields API is provided already by core [no dependencies].' . '</p>';
34
      $output .= '<p><strong>Configuration</strong></p>';
35
36
37
38
39
      $output .= '<p>' . 'Configuration is only slightly more complicated than a text field. Link text titles for URLs can be made required, set as instead of URL, optional (default), or left out entirely. If no link text title is provided, the trimmed version of the complete URL will be displayed. The target attribute should be set to "_blank", "top", or left out completely (checkboxes provide info). The rel=nofollow attribute prevents the link from being followed by certain search engines.' . '</p>';
      return $output;
  }
}

40
41
/**
 * Implements hook_field_info().
Nathan Haug's avatar
Nathan Haug committed
42
43
44
 */
function link_field_info() {
  return array(
45
    'link_field' => array(
46
47
      'label' => t('Link'),
      'description' => t('Store a title, href, and attributes in the database to assemble a link.'),
48
      'settings' => array(
49
50
51
52
        'attributes' => _link_default_attributes(),
        'url' => 0,
        'title' => 'optional',
        'title_value' => '',
53
        'title_maxlength' => 128,
54
55
56
57
58
59
60
61
62
63
        'enable_tokens' => 1,
        'display' => array(
          'url_cutoff' => 80,
        ),
      ),
      'instance_settings' => array(
        'attributes' => _link_default_attributes(),
        'url' => 0,
        'title' => 'optional',
        'title_value' => '',
64
        'title_label_use_field_label' => FALSE,
65
        'title_maxlength' => 128,
66
67
68
69
        'enable_tokens' => 1,
        'display' => array(
          'url_cutoff' => 80,
        ),
70
        'validate_url' => 1,
71
        'absolute_url' => 1,
72
      ),
73
      'default_widget' => 'link_field',
74
      'default_formatter' => 'link_default',
75
76
77
      // Support hook_entity_property_info() from contrib "Entity API".
      'property_type' => 'field_item_link',
      'property_callbacks' => array('link_field_property_info_callback'),
78
    ),
Nathan Haug's avatar
Nathan Haug committed
79
80
81
  );
}

82
/**
83
 * Implements hook_field_instance_settings_form().
84
 */
85
function link_field_instance_settings_form($field, $instance) {
86
87
88
  $form = array(
    '#element_validate' => array('link_field_settings_form_validate'),
  );
89

90
91
92
93
94
95
96
  $form['absolute_url'] = array(
    '#type' => 'checkbox',
    '#title' => t('Absolute URL'),
    '#default_value' => isset($instance['settings']['absolute_url']) && ($instance['settings']['absolute_url'] !== '') ? $instance['settings']['absolute_url'] : TRUE,
    '#description' => t('If checked, the URL will always render as an absolute URL.'),
  );

97
98
99
100
101
102
  $form['validate_url'] = array(
    '#type' => 'checkbox',
    '#title' => t('Validate URL'),
    '#default_value' => isset($instance['settings']['validate_url']) && ($instance['settings']['validate_url'] !== '') ? $instance['settings']['validate_url'] : TRUE,
    '#description' => t('If checked, the URL field will be verified as a valid URL during validation.'),
  );
103

104
105
106
  $form['url'] = array(
    '#type' => 'checkbox',
    '#title' => t('Optional URL'),
107
    '#default_value' => isset($instance['settings']['url']) ? $instance['settings']['url'] : '',
108
109
110
    '#return_value' => 'optional',
    '#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.'),
  );
111

112
113
114
115
  $title_options = array(
    'optional' => t('Optional Title'),
    'required' => t('Required Title'),
    'value' => t('Static Title'),
116
    'select' => t('Selected Title'),
117
118
    'none' => t('No Title'),
  );
119

120
121
122
  $form['title'] = array(
    '#type' => 'radios',
    '#title' => t('Link Title'),
123
    '#default_value' => isset($instance['settings']['title']) ? $instance['settings']['title'] : 'optional',
124
    '#options' => $title_options,
125
    '#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 entity 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.'),
126
  );
127

128
129
  $form['title_value'] = array(
    '#type' => 'textfield',
130
    '#title' => t('Static or default title'),
131
    '#default_value' => isset($instance['settings']['title_value']) ? $instance['settings']['title_value'] : '',
132
    '#description' => t('This title will 1) always be used if "Static Title" is selected above, or 2) used if "Optional title" is selected above and no title is entered when creating content.'),
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
    '#states' => array(
      'visible' => array(
        ':input[name="instance[settings][title]"]' => array('value' => 'value'),
      ),
    ),
  );

  $form['title_allowed_values'] = array(
    '#type' => 'textarea',
    '#title' => t('Title allowed values'),
    '#default_value' => isset($instance['settings']['title_allowed_values']) ? $instance['settings']['title_allowed_values'] : '',
    '#description' => t('When using "Selected Title", you can allow users to select the title from a limited set of values (eg. Home, Office, Other). Enter here all possible values that title can take, one value per line.'),
    '#states' => array(
      'visible' => array(
        ':input[name="instance[settings][title]"]' => array('value' => 'select'),
      ),
    ),
150
  );
151

152
153
154
155
156
157
158
  $form['title_label_use_field_label'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use field label as the label for the title field'),
    '#default_value' => isset($instance['settings']['title_label_use_field_label']) ? $instance['settings']['title_label_use_field_label'] : FALSE,
    '#description' => t('If this is checked the field label will be hidden.'),
  );

159
  $form['title_maxlength'] = array(
160
161
162
163
164
165
    '#type' => 'textfield',
    '#title' => t('Max length of title field'),
    '#default_value' => isset($instance['settings']['title_maxlength']) ? $instance['settings']['title_maxlength'] : '128',
    '#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.'),
    '#maxlength' => 3,
    '#size' => 3,
166
  );
167

168
  if (module_exists('token')) {
169
    // Add token module replacements fields.
170
171
172
173
174
    $form['enable_tokens'] = array(
      '#type' => 'checkbox',
      '#title' => t('Allow user-entered tokens'),
      '#default_value' => isset($instance['settings']['enable_tokens']) ? $instance['settings']['enable_tokens'] : 1,
      '#description' => t('Checking will allow users to enter tokens in URLs and Titles on the entity edit form. This does not affect the field settings on this page.'),
175
    );
176

177
    $entity_info = entity_get_info($instance['entity_type']);
178
    $form['tokens_help'] = array(
179
180
181
182
      '#theme' => 'token_tree',
      '#token_types' => array($entity_info['token type']),
      '#global_types' => TRUE,
      '#click_insert' => TRUE,
183
      '#dialog' => TRUE,
184
185
    );
  }
186

187
188
189
190
191
192
  $form['display'] = array(
    '#tree' => TRUE,
  );
  $form['display']['url_cutoff'] = array(
    '#type' => 'textfield',
    '#title' => t('URL Display Cutoff'),
193
    '#default_value' => isset($instance['settings']['display']['url_cutoff']) ? $instance['settings']['display']['url_cutoff'] : '80',
194
195
    '#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.'),
    '#maxlength' => 3,
196
    '#size' => 3,
197
  );
198

199
200
201
202
203
204
205
206
207
208
209
210
  $target_options = array(
    LINK_TARGET_DEFAULT => t('Default (no target attribute)'),
    LINK_TARGET_TOP => t('Open link in window root'),
    LINK_TARGET_NEW_WINDOW => t('Open link in new window'),
    LINK_TARGET_USER => t('Allow the user to choose'),
  );
  $form['attributes'] = array(
    '#tree' => TRUE,
  );
  $form['attributes']['target'] = array(
    '#type' => 'radios',
    '#title' => t('Link Target'),
211
    '#default_value' => empty($instance['settings']['attributes']['target']) ? LINK_TARGET_DEFAULT : $instance['settings']['attributes']['target'],
212
213
214
215
216
    '#options' => $target_options,
  );
  $form['attributes']['rel'] = array(
    '#type' => 'textfield',
    '#title' => t('Rel Attribute'),
217
    '#description' => t('When output, this link will have this rel attribute. The most common usage is <a href="http://en.wikipedia.org/wiki/Nofollow" target="blank">rel=&quot;nofollow&quot;</a> which prevents some search engines from spidering entered links.'),
218
    '#default_value' => empty($instance['settings']['attributes']['rel']) ? '' : $instance['settings']['attributes']['rel'],
219
220
221
222
    '#field_prefix' => 'rel = "',
    '#field_suffix' => '"',
    '#size' => 20,
  );
223
224
225
226
227
228
229
  $rel_remove_options = array(
    'default' => t('Keep rel as set up above (untouched/default)'),
    'rel_remove_external' => t('Remove rel if given link is external'),
    'rel_remove_internal' => t('Remove rel if given link is internal'),
  );
  $form['rel_remove'] = array(
    '#type' => 'radios',
230
    '#title' => t('Remove rel attribute automatically'),
231
    '#default_value' => !isset($instance['settings']['rel_remove']) ? 'default' : $instance['settings']['rel_remove'],
232
    '#description' => t('Turn on/off if rel attribute should be removed automatically, if user given link is internal/external'),
233
234
    '#options' => $rel_remove_options,
  );
235
236
237
238
239
  $form['attributes']['configurable_class'] = array(
    '#title' => t("Allow the user to enter a custom link class per link"),
    '#type' => 'checkbox',
    '#default_value' => empty($instance['settings']['attributes']['configurable_class']) ? '' : $instance['settings']['attributes']['configurable_class'],
  );
240
241
242
  $form['attributes']['class'] = array(
    '#type' => 'textfield',
    '#title' => t('Additional CSS Class'),
243
    '#description' => t('When output, this link will have this class attribute. Multiple classes should be separated by spaces. Only alphanumeric characters and hyphens are allowed'),
244
    '#default_value' => empty($instance['settings']['attributes']['class']) ? '' : $instance['settings']['attributes']['class'],
245
  );
246
247
248
249
250
251
252
253
  $form['attributes']['configurable_title'] = array(
    '#title' => t("Allow the user to enter a link 'title' attribute"),
    '#type' => 'checkbox',
    '#default_value' => empty($instance['settings']['attributes']['configurable_title']) ? '' : $instance['settings']['attributes']['configurable_title'],
  );
  $form['attributes']['title'] = array(
    '#title' => t("Default link 'title' Attribute"),
    '#type' => 'textfield',
254
    '#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" target="blank">WCAG 1.0 Guidelines</a> for links comformances. Tokens values will be evaluated.'),
255
256
257
258
259
    '#default_value' => empty($instance['settings']['attributes']['title']) ? '' : $instance['settings']['attributes']['title'],
    '#field_prefix' => 'title = "',
    '#field_suffix' => '"',
    '#size' => 20,
  );
260
261
262
263
  return $form;
}

/**
264
265
 * Form validate.
 *
266
 * #element_validate handler for link_field_instance_settings_form().
267
268
 */
function link_field_settings_form_validate($element, &$form_state, $complete_form) {
269
  if ($form_state['values']['instance']['settings']['title'] === 'value' && empty($form_state['values']['instance']['settings']['title_value'])) {
270
271
272
    form_set_error('instance][settings][title_value', t('A default title must be provided if the title is a static value.'));
  }
  if ($form_state['values']['instance']['settings']['title'] === 'select'
273
    && empty($form_state['values']['instance']['settings']['title_allowed_values'])) {
274
    form_set_error('instance][settings][title_allowed_values', t('You must enter one or more allowed values for link Title, the title is a selected value.'));
275
  }
276
  if (!empty($form_state['values']['instance']['settings']['display']['url_cutoff']) && !is_numeric($form_state['values']['instance']['settings']['display']['url_cutoff'])) {
277
278
    form_set_error('display', t('URL Display Cutoff value must be numeric.'));
  }
279
  if (empty($form_state['values']['instance']['settings']['title_maxlength'])) {
280
    form_set_value($element['title_maxlength'], '128', $form_state);
281
282
  }
  elseif (!is_numeric($form_state['values']['instance']['settings']['title_maxlength'])) {
283
    form_set_error('title_maxlength', t('The max length of the link title must be numeric.'));
284
285
  }
  elseif ($form_state['values']['instance']['settings']['title_maxlength'] > 255) {
286
287
    form_set_error('title_maxlength', t('The max length of the link title cannot be greater than 255 characters.'));
  }
288
289
}

290
/**
291
 * Implements hook_field_is_empty().
292
 */
293
function link_field_is_empty($item, $field) {
294
295
296
  return empty($item['title']) && empty($item['url']);
}

297
298
299
/**
 * Implements hook_field_load().
 */
300
301
302
function link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => $item) {
303
      $items[$id][$delta]['attributes'] = _link_load($field, $item, $instances[$id]);
304
    }
305
306
307
  }
}

Nathan Haug's avatar
Nathan Haug committed
308
/**
309
 * Implements hook_field_validate().
Nathan Haug's avatar
Nathan Haug committed
310
 */
311
312
function link_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  $optional_field_found = FALSE;
313
314
  if ($instance['settings']['validate_url'] !== 0 || is_null($instance['settings']['validate_url']) || !isset($instance['settings']['validate_url'])) {
    foreach ($items as $delta => $value) {
315
      _link_validate($items[$delta], $delta, $field, $entity, $instance, $langcode, $optional_field_found, $errors);
316
317
318
    }
  }

319
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] && !$optional_field_found) {
320
321
322
323
324
    $errors[$field['field_name']][$langcode][0][] = array(
      'error' => 'link_required',
      'message' => t('At least one title or URL must be entered.'),
      'error_element' => array('url' => FALSE, 'title' => TRUE),
    );
325
326
  }
}
327

328
/**
329
 * Implements hook_field_insert().
330
 */
331
332
function link_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $delta => $value) {
333
    _link_process($items[$delta], $delta, $field, $entity, $instance);
334
335
336
337
338
339
340
  }
}

/**
 * Implements hook_field_update().
 */
function link_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
341
  foreach ($items as $delta => $value) {
342
    _link_process($items[$delta], $delta, $field, $entity, $instance);
Nathan Haug's avatar
Nathan Haug committed
343
344
345
  }
}

346
347
348
349
350
/**
 * Implements hook_field_prepare_view().
 */
function link_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  foreach ($items as $entity_id => $entity_items) {
351
352
353
354
355
356
357
358
359
360
361
362
363
    $settings = $instances[$entity_id]['settings'];
    $trimTitle = trim($settings['title_value']);
    if (empty($entity_items) && !empty($trimTitle) && $settings['title'] == 'value') {
      $token_value = token_replace($settings['title_value'], array($entity_type => $entities[$entity_id]));
      $display_title = htmlspecialchars_decode($token_value, ENT_QUOTES);
      $items[$entity_id][0]['url'] = NULL;
      $items[$entity_id][0]['title'] = $display_title;
      $items[$entity_id][0]['attributes'] = array();
    }
    else {
      foreach ($entity_items as $delta => $value) {
        _link_sanitize($items[$entity_id][$delta], $delta, $field, $instances[$entity_id], $entities[$entity_id]);
      }
364
365
366
367
    }
  }
}

Nathan Haug's avatar
Nathan Haug committed
368
/**
369
 * Implements hook_field_widget_info().
Nathan Haug's avatar
Nathan Haug committed
370
 */
371
function link_field_widget_info() {
Nathan Haug's avatar
Nathan Haug committed
372
  return array(
373
    'link_field' => array(
374
      'label' => 'Link',
375
376
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
Nathan Haug's avatar
Nathan Haug committed
377
378
379
380
381
    ),
  );
}

/**
382
 * Implements hook_field_widget_form().
Nathan Haug's avatar
Nathan Haug committed
383
 */
384
385
386
function link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $element += array(
    '#type' => $instance['widget']['type'],
387
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
388
  );
389
  return $element;
390
391
}

392
393
394
395
/**
 * Implements hook_field_widget_error().
 */
function link_field_widget_error($element, $error, $form, &$form_state) {
396
  if (!empty($error['error_element']['title'])) {
397
398
    form_error($element['title'], $error['message']);
  }
399
  elseif (!empty($error['error_element']['url'])) {
400
401
402
403
    form_error($element['url'], $error['message']);
  }
}

404
/**
405
 * Unpacks the item attributes for use.
406
 */
407
408
function _link_load($field, $item, $instance) {
  if (isset($item['attributes'])) {
409
410
411
412
    if (!is_array($item['attributes'])) {
      $item['attributes'] = unserialize($item['attributes']);
    }
    return $item['attributes'];
413
  }
414
  elseif (isset($instance['settings']['attributes'])) {
415
416
417
418
    return $instance['settings']['attributes'];
  }
  else {
    return $field['settings']['attributes'];
419
  }
420
421
}

422
423
/**
 * Prepares the item attributes and url for storage.
424
 *
425
426
427
428
429
430
431
432
433
434
 * @param array $item
 *   Link field values.
 * @param array $delta
 *   The sequence number for current values.
 * @param array $field
 *   The field structure array.
 * @param object $entity
 *   Entity object.
 * @param array $instance
 *   The instance structure for $field on $entity's bundle.
435
 *
436
 * @codingStandardsIgnoreStart
437
 */
438
function _link_process(&$item, $delta, $field, $entity, $instance) {
439
  // @codingStandardsIgnoreEnd
440
  // Trim whitespace from URL.
441
442
443
  if (!empty($item['url'])) {
    $item['url'] = trim($item['url']);
  }
444

445
446
  // If no attributes are set then make sure $item['attributes'] is an empty
  // array, so $field['attributes'] can override it.
447
  if (empty($item['attributes'])) {
448
    $item['attributes'] = array();
449
450
  }

451
  // Serialize the attributes array.
452
453
454
  if (!is_string($item['attributes'])) {
    $item['attributes'] = serialize($item['attributes']);
  }
455
456

  // Don't save an invalid default value (e.g. 'http://').
457
  if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url']) && is_object($entity)) {
458
459
    $langcode = !empty($entity) ? field_language($instance['entity_type'], $entity, $instance['field_name']) : LANGUAGE_NONE;
    if (!link_validate_url($item['url'], $langcode)) {
460
      unset($item['url']);
461
462
463
464
    }
  }
}

465
466
467
/**
 * Validates that the link field has been entered properly.
 */
468
function _link_validate(&$item, $delta, $field, $entity, $instance, $langcode, &$optional_field_found, &$errors) {
469
  if ($item['url'] && !(isset($instance['default_value'][$delta]['url']) && $item['url'] === $instance['default_value'][$delta]['url'] && !$instance['required'])) {
470
    // Validate the link.
471
    if (!link_validate_url(trim($item['url']), $langcode)) {
472
473
      $errors[$field['field_name']][$langcode][$delta][] = array(
        'error' => 'link_required',
474
475
476
477
        'message' => t('The value %value provided for %field is not a valid URL.', array(
          '%value' => trim($item['url']),
          '%field' => $instance['label'],
        )),
478
479
        'error_element' => array('url' => TRUE, 'title' => FALSE),
      );
480
481
    }
    // Require a title for the link if necessary.
482
    if ($instance['settings']['title'] == 'required' && strlen(trim($item['title'])) == 0) {
483
484
485
486
487
      $errors[$field['field_name']][$langcode][$delta][] = array(
        'error' => 'link_required',
        'message' => t('Titles are required for all links.'),
        'error_element' => array('url' => FALSE, 'title' => TRUE),
      );
488
    }
489
  }
490
  // Require a link if we have a title.
491
  if ($instance['settings']['url'] !== 'optional' && strlen(isset($item['title']) ? $item['title'] : NULL) > 0 && strlen(trim($item['url'])) == 0) {
492
493
494
495
496
    $errors[$field['field_name']][$langcode][$delta][] = array(
      'error' => 'link_required',
      'message' => t('You cannot enter a title without a link url.'),
      'error_element' => array('url' => TRUE, 'title' => FALSE),
    );
497
  }
498
499
  // In a totally bizzaro case, where URLs and titles are optional but the field
  // is required, ensure there is at least one link.
500
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional'
501
    && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
502
    $optional_field_found = TRUE;
503
  }
504
  // Require entire field.
505
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] == 1 && !$optional_field_found && isset($instance['id'])) {
506
507
508
509
510
    $errors[$field['field_name']][$langcode][$delta][] = array(
      'error' => 'link_required',
      'message' => t('At least one title or URL must be entered.'),
      'error_element' => array('url' => FALSE, 'title' => TRUE),
    );
511
  }
512
513
514
}

/**
515
 * Clean up user-entered values for a link field according to field settings.
516
 *
517
 * @param array $item
518
 *   A single link item, usually containing url, title, and attributes.
519
 * @param int $delta
520
 *   The delta value if this field is one of multiple fields.
521
 * @param array $field
522
 *   The CCK field definition.
523
 * @param object $entity
524
 *   The entity containing this link.
525
526
 *
 * @codingStandardsIgnoreStart
527
 */
528
function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
529
  // @codingStandardsIgnoreEnd
530
531
532
533
  // Don't try to process empty links.
  if (empty($item['url']) && empty($item['title'])) {
    return;
  }
534
535
536
  if (empty($item['html'])) {
    $item['html'] = FALSE;
  }
537

538
  // Replace URL tokens.
539
540
  $entity_type = $instance['entity_type'];
  $entity_info = entity_get_info($entity_type);
541
  $property_id = $entity_info['entity keys']['id'];
542
  $entity_token_type = isset($entity_info['token type']) ? $entity_info['token type'] : (
543
  $entity_type == 'taxonomy_term' || $entity_type == 'taxonomy_vocabulary' ? str_replace('taxonomy_', '', $entity_type) : $entity_type
544
  );
545
  if (isset($instance['settings']['enable_tokens']) && $instance['settings']['enable_tokens']) {
546
547
548
549
550
551
552
553
554
555
556
    $text_tokens = token_scan($item['url']);
    if (!empty($text_tokens)) {
      // Load the entity if necessary for entities in views.
      if (isset($entity->{$property_id})) {
        $entity_loaded = entity_load($entity_type, array($entity->{$property_id}));
        $entity_loaded = array_pop($entity_loaded);
      }
      else {
        $entity_loaded = $entity;
      }
      $item['url'] = token_replace($item['url'], array($entity_token_type => $entity_loaded));
557
    }
558
559
  }

560
  $type = link_url_type($item['url']);
561
562
  // If the type of the URL cannot be determined and URL validation is disabled,
  // then assume LINK_EXTERNAL for later processing.
563
564
565
  if ($type == FALSE && $instance['settings']['validate_url'] === 0) {
    $type = LINK_EXTERNAL;
  }
566
  $url = link_cleanup_url($item['url']);
567
  $url_parts = _link_parse_url($url);
568

569
  if (!empty($url_parts['url'])) {
570
    $item['url'] = url($url_parts['url'],
571
572
573
574
575
      array(
        'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
        'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
        'absolute' => !empty($instance['settings']['absolute_url']),
        'html' => TRUE,
576
      )
577
578
    );
  }
579
580

  // Create a shortened URL for display.
581
582
583
584
  if ($type == LINK_EMAIL) {
    $display_url = str_replace('mailto:', '', $url);
  }
  else {
585
    $display_url = url($url_parts['url'],
586
587
588
      array(
        'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
        'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
589
        'absolute' => !empty($instance['settings']['absolute_url']),
590
591
592
      )
    );
  }
593
  if ($instance['settings']['display']['url_cutoff'] && strlen($display_url) > $instance['settings']['display']['url_cutoff']) {
594
    $display_url = substr($display_url, 0, $instance['settings']['display']['url_cutoff']) . "…";
595
596
597
  }
  $item['display_url'] = $display_url;

598
599
600
  // Use the title defined at the instance level.
  if ($instance['settings']['title'] == 'value' && strlen(trim($instance['settings']['title_value']))) {
    $title = $instance['settings']['title_value'];
601
602
603
604
    if (function_exists('i18n_string_translate')) {
      $i18n_string_name = "field:{$instance['field_name']}:{$instance['bundle']}:title_value";
      $title = i18n_string_translate($i18n_string_name, $title);
    }
605
606
  }
  // Use the title defined by the user at the widget level.
607
  elseif (drupal_strlen(trim($item['title']))) {
608
609
    $title = $item['title'];
  }
610
611
612
613
614
  // Use the static title if a user-defined title is optional and a static title
  // has been defined.
  elseif ($instance['settings']['title'] == 'optional' && drupal_strlen(trim($instance['settings']['title_value']))) {
    $title = $instance['settings']['title_value'];
  }
615
616
617
  else {
    $title = '';
  }
618

619
  // Replace title tokens.
620
  if ($title && $instance['settings']['enable_tokens']) {
621
622
623
624
625
626
627
628
629
630
631
    $text_tokens = token_scan($title);
    if (!empty($text_tokens)) {
      // Load the entity if necessary for entities in views.
      if (isset($entity->{$property_id})) {
        $entity_loaded = entity_load($entity_type, array($entity->{$property_id}));
        $entity_loaded = array_pop($entity_loaded);
      }
      else {
        $entity_loaded = $entity;
      }
      $title = token_replace($title, array($entity_token_type => $entity_loaded));
632
    }
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
  }
  if ($title && ($instance['settings']['title'] == 'value' || $instance['settings']['enable_tokens'])) {
    $title = filter_xss($title, array(
      'b',
      'br',
      'code',
      'em',
      'i',
      'img',
      'span',
      'strong',
      'sub',
      'sup',
      'tt',
      'u',
    ));
649
    $item['html'] = TRUE;
650
  }
651
  $item['title'] = empty($title) && $title !== '0' ? $item['display_url'] : $title;
652

653
654
655
656
657
658
  if (!isset($item['attributes'])) {
    $item['attributes'] = array();
  }

  // Unserialize attributtes array if it has not been unserialized yet.
  if (!is_array($item['attributes'])) {
659
    $item['attributes'] = (array) unserialize($item['attributes']);
660
661
662
  }

  // Add default attributes.
663
  if (!is_array($instance['settings']['attributes'])) {
664
665
666
667
668
    $instance['settings']['attributes'] = _link_default_attributes();
  }
  else {
    $instance['settings']['attributes'] += _link_default_attributes();
  }
669
670

  // Merge item attributes with attributes defined at the field level.
671
  $item['attributes'] += $instance['settings']['attributes'];
672
673
674

  // If user is not allowed to choose target attribute, use default defined at
  // field level.
675
676
  if ($instance['settings']['attributes']['target'] != LINK_TARGET_USER) {
    $item['attributes']['target'] = $instance['settings']['attributes']['target'];
677
  }
678
679
680
  elseif ($item['attributes']['target'] == LINK_TARGET_USER) {
    $item['attributes']['target'] = LINK_TARGET_DEFAULT;
  }
681
682

  // Remove the target attribute if the default (no target) is selected.
683
  if (empty($item['attributes']) || (isset($item['attributes']['target']) && $item['attributes']['target'] == LINK_TARGET_DEFAULT)) {
684
685
686
    unset($item['attributes']['target']);
  }

687
688
689
  // Remove rel attribute for internal or external links if selected.
  if (isset($item['attributes']['rel']) && isset($instance['settings']['rel_remove']) && $instance['settings']['rel_remove'] != 'default') {
    if (($instance['settings']['rel_remove'] != 'rel_remove_internal' && $type != LINK_INTERNAL) ||
690
691
      ($instance['settings']['rel_remove'] != 'rel_remove_external' && $type != LINK_EXTERNAL)) {
      unset($item['attributes']['rel']);
692
    }
693
  }
694

695
696
  // Handle "title" link attribute.
  if (!empty($item['attributes']['title']) && module_exists('token')) {
697
    $text_tokens = token_scan($item['attributes']['title']);
698
    if (!empty($text_tokens)) {
699
700
701
702
703
704
705
706
707
      // Load the entity (necessary for entities in views).
      if (isset($entity->{$property_id})) {
        $entity_loaded = entity_load($entity_type, array($entity->{$property_id}));
        $entity_loaded = array_pop($entity_loaded);
      }
      else {
        $entity_loaded = $entity;
      }
      $item['attributes']['title'] = token_replace($item['attributes']['title'], array($entity_token_type => $entity_loaded));
708
    }
709
710
711
712
713
714
715
716
717
718
719
720
721
722
    $item['attributes']['title'] = filter_xss($item['attributes']['title'], array(
      'b',
      'br',
      'code',
      'em',
      'i',
      'img',
      'span',
      'strong',
      'sub',
      'sup',
      'tt',
      'u',
    ));
723
  }
724
725
  // Handle attribute classes.
  if (!empty($item['attributes']['class'])) {
726
727
    $classes = explode(' ', $item['attributes']['class']);
    foreach ($classes as &$class) {
728
      $class = drupal_clean_css_identifier($class);
729
730
731
732
733
    }
    $item['attributes']['class'] = implode(' ', $classes);
  }
  unset($item['attributes']['configurable_class']);

734
  // Remove title attribute if it's equal to link text.
735
  if (isset($item['attributes']['title']) && $item['attributes']['title'] == $item['title']) {
736
737
738
739
    unset($item['attributes']['title']);
  }
  unset($item['attributes']['configurable_title']);

740
741
  // Remove empty attributes.
  $item['attributes'] = array_filter($item['attributes']);
742
743
}

744
745
746
747
748
749
/**
 * Because parse_url doesn't work with relative urls.
 *
 * @param string $url
 *   URL to parse.
 *
750
 * @return array
751
752
753
754
 *   Array of url pieces - only 'url', 'query', and 'fragment'.
 */
function _link_parse_url($url) {
  $url_parts = array();
755
  // Separate out the anchor, if any.
756
757
758
759
  if (strpos($url, '#') !== FALSE) {
    $url_parts['fragment'] = substr($url, strpos($url, '#') + 1);
    $url = substr($url, 0, strpos($url, '#'));
  }
760
  // Separate out the query string, if any.
761
762
  if (strpos($url, '?') !== FALSE) {
    $query = substr($url, strpos($url, '?') + 1);
763
    $url_parts['query'] = _link_parse_str($query);
764
765
766
767
768
769
    $url = substr($url, 0, strpos($url, '?'));
  }
  $url_parts['url'] = $url;
  return $url_parts;
}

770
/**
771
772
773
 * Replaces the PHP parse_str() function.
 *
 * Because parse_str replaces the following characters in query parameters name
774
775
 * in order to maintain compatibility with deprecated register_globals
 * directive:
776
777
778
779
780
781
782
783
784
 *
 *   - chr(32) ( ) (space)
 *   - chr(46) (.) (dot)
 *   - chr(91) ([) (open square bracket)
 *   - chr(128) - chr(159) (various)
 *
 * @param string $query
 *   Query to parse.
 *
785
 * @return array
786
787
788
789
790
791
792
793
794
 *   Array of query parameters.
 *
 * @see http://php.net/manual/en/language.variables.external.php#81080
 */
function _link_parse_str($query) {
  $query_array = array();

  $pairs = explode('&', $query);
  foreach ($pairs as $pair) {
795
    $name_value = explode('=', $pair, 2);
796
797
798
799
800
801
802
803
    $name = urldecode($name_value[0]);
    $value = isset($name_value[1]) ? urldecode($name_value[1]) : NULL;
    $query_array[$name] = $value;
  }

  return $query_array;
}

804
/**
805
 * Implements hook_theme().
806
 */
807
808
function link_theme() {
  return array(
809
    'link_formatter_link_default' => array(
810
      'variables' => array('element' => NULL, 'field' => NULL),
811
    ),
812
    'link_formatter_link_plain' => array(
813
      'variables' => array('element' => NULL, 'field' => NULL),
814
    ),
815
816
817
    'link_formatter_link_host' => array(
      'variables' => array('element' => NULL),
    ),
818
    'link_formatter_link_absolute' => array(
819
      'variables' => array('element' => NULL, 'field' => NULL),
820
    ),
821
    'link_formatter_link_domain' => array(
822
823
824
825
826
      'variables' => array(
        'element' => NULL,
        'display' => NULL,
        'field' => NULL,
      ),
827
    ),
828
829
830
    'link_formatter_link_no_protocol' => array(
      'variables' => array('element' => NULL, 'field' => NULL),
    ),
831
    'link_formatter_link_title_plain' => array(
832
      'variables' => array('element' => NULL, 'field' => NULL),
833
    ),
834
    'link_formatter_link_url' => array(
835
      'variables' => array('element' => NULL, 'field' => NULL),
836
    ),
837
    'link_formatter_link_short' => array(
838
      'variables' => array('element' => NULL, 'field' => NULL),
839
    ),
840
    'link_formatter_link_label' => array(
841
      'variables' => array('element' => NULL, 'field' => NULL),
842
    ),
843
    'link_formatter_link_separate' => array(
844
      'variables' => array('element' => NULL, 'field' => NULL),
845
    ),
846
    'link_field' => array(
847
      'render element' => 'element',
848
849
850
851
852
    ),
  );
}

/**
853
 * Formats a link field widget.
854
 */
855
function theme_link_field($vars) {
856
  drupal_add_css(drupal_get_path('module', 'link') . '/link.css');
857
  $element = $vars['element'];
858
859
  // Prefix single value link fields with the name of the field.
  if (empty($element['#field']['multiple'])) {
860
    if (isset($element['url']) && !isset($element['title'])) {
861
      $element['url']['#title_display'] = 'invisible';
862
    }
863
864
  }

865
  $output = '';
866
  $output .= '<div class="link-field-subrow clearfix">';
867
  if (isset($element['title'])) {
868
    $output .= '<div class="link-field-title link-field-column">' . drupal_render($element['title']) . '</div>';
869
  }
870
  $output .= '<div class="link-field-url' . (isset($element['title']) ? ' link-field-column' : '') . '">' . drupal_render($element['url']) . '</div>';
871
  $output .= '</div>';
872
  if (!empty($element['attributes']['target'])) {
873
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['target']) . '</div>';
874
  }
875
  if (!empty($element['attributes']['title'])) {
876
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['title']) . '</div>';
877
  }
878
  if (!empty($element['attributes']['class'])) {
879
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['class']) . '</div>';
880
  }
881
  $output .= drupal_render_children($element);
882
  return $output;
883
884
}

885
/**
886
 * Implements hook_element_info().
887
 */
888
function link_element_info() {
889
  $elements = array();
890
  $elements['link_field'] = array(