link.module 50.3 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_DOMAINS', 'aero|arpa|asia|biz|build|com|cat|ceo|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|post|pro|tel|travel|mobi|local|xxx');
14
15
16
17
18

define('LINK_TARGET_DEFAULT', 'default');
define('LINK_TARGET_NEW_WINDOW', '_blank');
define('LINK_TARGET_TOP', '_top');
define('LINK_TARGET_USER', 'user');
19

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

/**
 * Implements hook_field_info().
Nathan Haug's avatar
Nathan Haug committed
27
28
29
 */
function link_field_info() {
  return array(
30
    'link_field' => array(
31
32
      'label' => t('Link'),
      'description' => t('Store a title, href, and attributes in the database to assemble a link.'),
33
      'settings' => array(
34
35
36
37
        'attributes' => _link_default_attributes(),
        'url' => 0,
        'title' => 'optional',
        'title_value' => '',
38
        'title_maxlength' => 128,
39
40
41
42
43
44
45
46
47
48
        'enable_tokens' => 1,
        'display' => array(
          'url_cutoff' => 80,
        ),
      ),
      'instance_settings' => array(
        'attributes' => _link_default_attributes(),
        'url' => 0,
        'title' => 'optional',
        'title_value' => '',
49
        'title_label_use_field_label' => FALSE,
50
        'title_maxlength' => 128,
51
52
53
54
        'enable_tokens' => 1,
        'display' => array(
          'url_cutoff' => 80,
        ),
55
        'validate_url' => 1,
56
      ),
57
      'default_widget' => 'link_field',
58
      'default_formatter' => 'link_default',
59
60
61
      // Support hook_entity_property_info() from contrib "Entity API".
      'property_type' => 'field_item_link',
      'property_callbacks' => array('link_field_property_info_callback'),
62
    ),
Nathan Haug's avatar
Nathan Haug committed
63
64
65
  );
}

66
/**
67
 * Implements hook_field_instance_settings_form().
68
 */
69
function link_field_instance_settings_form($field, $instance) {
70
71
72
  $form = array(
    '#element_validate' => array('link_field_settings_form_validate'),
  );
73

74
75
76
77
78
79
  $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.'),
  );
80

81
82
83
  $form['url'] = array(
    '#type' => 'checkbox',
    '#title' => t('Optional URL'),
84
    '#default_value' => isset($instance['settings']['url']) ? $instance['settings']['url'] : '',
85
86
87
    '#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.'),
  );
88

89
90
91
92
93
94
  $title_options = array(
    'optional' => t('Optional Title'),
    'required' => t('Required Title'),
    'value' => t('Static Title'),
    'none' => t('No Title'),
  );
95

96
97
98
  $form['title'] = array(
    '#type' => 'radios',
    '#title' => t('Link Title'),
99
    '#default_value' => isset($instance['settings']['title']) ? $instance['settings']['title'] : 'optional',
100
    '#options' => $title_options,
101
    '#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.'),
102
  );
103

104
105
106
  $form['title_value'] = array(
    '#type' => 'textfield',
    '#title' => t('Static title'),
107
    '#default_value' => isset($instance['settings']['title_value']) ? $instance['settings']['title_value'] : '',
108
109
    '#description' => t('This title will always be used if &ldquo;Static Title&rdquo; is selected above.'),
  );
110

111
112
113
114
115
116
117
  $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.'),
  );

118
  $form['title_maxlength'] = array(
119
120
121
122
123
124
    '#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,
125
  );
126

127
128
  if (module_exists('token')) {
    // Add token module replacements fields
129
130
131
132
133
    $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.'),
134
    );
135
    
136
    $entity_info = entity_get_info($instance['entity_type']);
137
    $form['tokens_help'] = array(
138
139
140
141
      '#theme' => 'token_tree',
      '#token_types' => array($entity_info['token type']),
      '#global_types' => TRUE,
      '#click_insert' => TRUE,
142
      '#dialog' => TRUE,
143
144
    );
  }
145

146
147
148
149
150
151
  $form['display'] = array(
    '#tree' => TRUE,
  );
  $form['display']['url_cutoff'] = array(
    '#type' => 'textfield',
    '#title' => t('URL Display Cutoff'),
152
    '#default_value' => isset($instance['settings']['display']['url_cutoff']) ? $instance['settings']['display']['url_cutoff'] : '80',
153
154
    '#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,
155
    '#size' => 3,
156
  );
157

158
159
160
161
162
163
164
165
166
167
168
169
  $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'),
170
    '#default_value' => empty($instance['settings']['attributes']['target']) ? LINK_TARGET_DEFAULT : $instance['settings']['attributes']['target'],
171
172
173
174
175
176
    '#options' => $target_options,
  );
  $form['attributes']['rel'] = array(
    '#type' => 'textfield',
    '#title' => t('Rel Attribute'),
    '#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.'),
177
    '#default_value' => empty($instance['settings']['attributes']['rel']) ? '' : $instance['settings']['attributes']['rel'],
178
179
180
181
    '#field_prefix' => 'rel = "',
    '#field_suffix' => '"',
    '#size' => 20,
  );
182
183
184
185
186
187
188
  $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',
189
    '#title' => t('Remove rel attribute automatically'),
190
    '#default_value' => !isset($instance['settings']['rel_remove']) ? 'default' : $instance['settings']['rel_remove'],
191
    '#description' => t('Turn on/off if rel attribute should be removed automatically, if user given link is internal/external'),
192
193
    '#options' => $rel_remove_options,
  );
194
195
196
197
198
  $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'],
  );
199
200
201
  $form['attributes']['class'] = array(
    '#type' => 'textfield',
    '#title' => t('Additional CSS Class'),
202
    '#description' => t('When output, this link will have this class attribute. Multiple classes should be separated by spaces.'),
203
    '#default_value' => empty($instance['settings']['attributes']['class']) ? '' : $instance['settings']['attributes']['class'],
204
  );
205
206
207
208
209
210
211
212
213
214
215
216
217
218
  $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',
    '#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.'),
    '#default_value' => empty($instance['settings']['attributes']['title']) ? '' : $instance['settings']['attributes']['title'],
    '#field_prefix' => 'title = "',
    '#field_suffix' => '"',
    '#size' => 20,
  );
219
220
221
222
  return $form;
}

/**
223
 * #element_validate handler for link_field_instance_settings_form().
224
225
 */
function link_field_settings_form_validate($element, &$form_state, $complete_form) {
226
  if ($form_state['values']['instance']['settings']['title'] === 'value' && empty($form_state['values']['instance']['settings']['title_value'])) {
227
228
    form_set_error('title_value', t('A default title must be provided if the title is a static value.'));
  }
229
  if (!empty($form_state['values']['instance']['settings']['display']['url_cutoff']) && !is_numeric($form_state['values']['instance']['settings']['display']['url_cutoff'])) {
230
231
    form_set_error('display', t('URL Display Cutoff value must be numeric.'));
  }
232
  if (empty($form_state['values']['instance']['settings']['title_maxlength'])) {
233
    form_set_value($element['title_maxlength'], '128', $form_state);
234
235
  }
  elseif (!is_numeric($form_state['values']['instance']['settings']['title_maxlength'])) {
236
    form_set_error('title_maxlength', t('The max length of the link title must be numeric.'));
237
238
  }
  elseif ($form_state['values']['instance']['settings']['title_maxlength'] > 255) {
239
240
    form_set_error('title_maxlength', t('The max length of the link title cannot be greater than 255 characters.'));
  }
241
242
}

243
/**
244
 * Implements hook_field_is_empty().
245
 */
246
function link_field_is_empty($item, $field) {
247
248
249
  return empty($item['title']) && empty($item['url']);
}

250
251
252
/**
 * Implements hook_field_load().
 */
253
254
255
function link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => $item) {
256
      $items[$id][$delta]['attributes'] = _link_load($field, $item, $instances[$id]);
257
    }
258
259
260
  }
}

Nathan Haug's avatar
Nathan Haug committed
261
/**
262
 * Implements hook_field_validate().
Nathan Haug's avatar
Nathan Haug committed
263
 */
264
265
function link_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  $optional_field_found = FALSE;
266
267
  if ($instance['settings']['validate_url'] !== 0 || is_null($instance['settings']['validate_url']) || !isset($instance['settings']['validate_url'])) {
    foreach ($items as $delta => $value) {
268
      _link_validate($items[$delta], $delta, $field, $entity, $instance, $langcode, $optional_field_found, $errors);
269
270
271
    }
  }

272
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] && !$optional_field_found) {
273
274
275
276
277
    $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),
    );
278
279
  }
}
280

281
/**
282
 * Implements hook_field_insert().
283
 */
284
285
286
287
288
289
290
291
292
293
function link_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $delta => $value) {
    _link_process($items[$delta], $delta, $field, $entity);
  }
}

/**
 * Implements hook_field_update().
 */
function link_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
294
295
  foreach ($items as $delta => $value) {
    _link_process($items[$delta], $delta, $field, $entity);
Nathan Haug's avatar
Nathan Haug committed
296
297
298
  }
}

299
300
301
302
303
304
305
306
307
308
309
/**
 * Implements hook_field_prepare_view().
 */
function link_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  foreach ($items as $entity_id => $entity_items) {
    foreach ($entity_items as $delta => $value) {
      _link_sanitize($items[$entity_id][$delta], $delta, $field, $instances[$entity_id], $entities[$entity_id]);
    }
  }
}

Nathan Haug's avatar
Nathan Haug committed
310
/**
311
 * Implements hook_field_widget_info().
Nathan Haug's avatar
Nathan Haug committed
312
 */
313
function link_field_widget_info() {
Nathan Haug's avatar
Nathan Haug committed
314
  return array(
315
    'link_field' => array(
316
      'label' => 'Link',
317
318
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
Nathan Haug's avatar
Nathan Haug committed
319
320
321
322
323
    ),
  );
}

/**
324
 * Implements hook_field_widget_form().
Nathan Haug's avatar
Nathan Haug committed
325
 */
326
327
328
function link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $element += array(
    '#type' => $instance['widget']['type'],
329
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
330
  );
331
  return $element;
332
333
}

334
335
336
337
338
339
340
/**
 * Implements hook_field_widget_error().
 */
function link_field_widget_error($element, $error, $form, &$form_state) {
  if ($error['error_element']['title']) {
    form_error($element['title'], $error['message']);
  }
341
  elseif ($error['error_element']['url']) {
342
343
344
345
    form_error($element['url'], $error['message']);
  }
}

346
/**
347
 * Unpacks the item attributes for use.
348
 */
349
350
function _link_load($field, $item, $instance) {
  if (isset($item['attributes'])) {
351
352
353
354
    if (!is_array($item['attributes'])) {
      $item['attributes'] = unserialize($item['attributes']);
    }
    return $item['attributes'];
355
  }
356
  elseif (isset($instance['settings']['attributes'])) {
357
358
359
360
    return $instance['settings']['attributes'];
  }
  else {
    return $field['settings']['attributes'];
361
  }
362
363
}

364
365
366
/**
 * Prepares the item attributes and url for storage.
 */
367
function _link_process(&$item, $delta = 0, $field, $entity) {
368
  // Trim whitespace from URL.
369
  $item['url'] = trim($item['url']);
370

371
372
  // If no attributes are set then make sure $item['attributes'] is an empty
  // array, so $field['attributes'] can override it.
373
  if (empty($item['attributes'])) {
374
    $item['attributes'] = array();
375
376
  }

377
  // Serialize the attributes array.
378
379
380
  if (!is_string($item['attributes'])) {
    $item['attributes'] = serialize($item['attributes']);
  }
381
382

  // Don't save an invalid default value (e.g. 'http://').
383
  if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url']) && is_object($entity)) {
384
385
    if (!link_validate_url($item['url'])) {
      unset($item['url']);
386
387
388
389
    }
  }
}

390
391
392
/**
 * Validates that the link field has been entered properly.
 */
393
function _link_validate(&$item, $delta, $field, $entity, $instance, $langcode, &$optional_field_found, &$errors) {
394
  if ($item['url'] && !(isset($instance['default_value'][$delta]['url']) && $item['url'] === $instance['default_value'][$delta]['url'] && !$instance['required'])) {
395
396
    // Validate the link.
    if (link_validate_url(trim($item['url'])) == FALSE) {
397
398
399
400
401
      $errors[$field['field_name']][$langcode][$delta][] = array(
        'error' => 'link_required',
        'message' => t('The value provided for %field is not a valid URL.', array('%field' => $instance['label'])),
        'error_element' => array('url' => TRUE, 'title' => FALSE),
      );
402
403
    }
    // Require a title for the link if necessary.
404
    if ($instance['settings']['title'] == 'required' && strlen(trim($item['title'])) == 0) {
405
406
407
408
409
      $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),
      );
410
    }
411
  }
412
  // Require a link if we have a title.
413
  if ($instance['settings']['url'] !== 'optional' && strlen(isset($item['title']) ? $item['title'] : NULL) > 0 && strlen(trim($item['url'])) == 0) {
414
415
416
417
418
    $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),
    );
419
420
  }
  // In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link.
421
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
422
    $optional_field_found = TRUE;
423
  }
424
  // Require entire field
425
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] == 1 && !$optional_field_found && isset($instance['id'])) {
426
427
428
429
430
    $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),
    );
431
  }
432
433
434
}

/**
435
 * Clean up user-entered values for a link field according to field settings.
436
 *
437
438
439
440
441
442
 * @param $item
 *   A single link item, usually containing url, title, and attributes.
 * @param $delta
 *   The delta value if this field is one of multiple fields.
 * @param $field
 *   The CCK field definition.
443
444
 * @param $entity
 *   The entity containing this link.
445
 */
446
function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
447
448
449
450
  // Don't try to process empty links.
  if (empty($item['url']) && empty($item['title'])) {
    return;
  }
451
452
453
  if (empty($item['html'])) {
    $item['html'] = FALSE;
  }
454

455
  // Replace URL tokens.
456
457
  $entity_type = $instance['entity_type'];
  $entity_info = entity_get_info($entity_type);
458
  $property_id = $entity_info['entity keys']['id'];
459
460
461
  $entity_token_type = isset($entity_info['token type']) ? $entity_info['token type'] : (
    $entity_type == 'taxonomy_term' || $entity_type == 'taxonomy_vocabulary' ? str_replace('taxonomy_', '', $entity_type) : $entity_type
  );
462
  if (isset($instance['settings']['enable_tokens']) && $instance['settings']['enable_tokens']) {
463
    global $user;
464
    // Load the entity if necessary for entities in views.
465
466
467
468
469
470
471
    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;
    }
472
    $item['url'] = token_replace($item['url'], array($entity_token_type => $entity_loaded));
473
474
475
  }

  $type = link_validate_url($item['url']);
476
477
  // If the type of the URL cannot be determined and URL validation is disabled,
  // then assume LINK_EXTERNAL for later processing.
478
479
480
  if ($type == FALSE && $instance['settings']['validate_url'] === 0) {
    $type = LINK_EXTERNAL;
  }
481
  $url = link_cleanup_url($item['url']);
482
  $url_parts = _link_parse_url($url);
483

484
485
486
487
488
489
490
491
492
493
  if (!empty($url_parts['url'])) {
    $item['url'] = url($url_parts['url'],
      array(
        'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
        'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
        'absolute' => TRUE,
        'html' => TRUE,
      )
    );
  }
494
495

  // Create a shortened URL for display.
496
497
498
499
  if ($type == LINK_EMAIL) {
    $display_url = str_replace('mailto:', '', $url);
  }
  else {
500
    $display_url = url($url_parts['url'],
501
502
503
504
505
506
507
      array(
        'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
        'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
        'absolute' => TRUE,
      )
    );
  }
508
  if ($instance['settings']['display']['url_cutoff'] && strlen($display_url) > $instance['settings']['display']['url_cutoff']) {
509
    $display_url = substr($display_url, 0, $instance['settings']['display']['url_cutoff']) . "...";
510
511
512
  }
  $item['display_url'] = $display_url;

513
514
515
  // Use the title defined at the instance level.
  if ($instance['settings']['title'] == 'value' && strlen(trim($instance['settings']['title_value']))) {
    $title = $instance['settings']['title_value'];
516
517
518
519
    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);
    }
520
521
  }
  // Use the title defined by the user at the widget level.
522
  elseif (isset($item['title'])) {
523
524
    $title = $item['title'];
  }
525
526
527
  else {
    $title = '';
  }
528

529
  // Replace tokens.
530
  if ($title && ($instance['settings']['title'] == 'value' || $instance['settings']['enable_tokens'])) {
531
    // Load the entity if necessary for entities in views.
532
533
534
535
536
537
538
    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;
    }
539
540
    $title = token_replace($title, array($entity_token_type => $entity_loaded));
    $title = filter_xss($title, array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
541
    $item['html'] = TRUE;
542
  }
543
  $item['title'] = empty($title) ? $item['display_url'] : $title;
544

545
546
547
548
549
550
551
552
553
554
  if (!isset($item['attributes'])) {
    $item['attributes'] = array();
  }

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

  // Add default attributes.
555
  if (!is_array($instance['settings']['attributes'])) {
556
557
558
559
560
    $instance['settings']['attributes'] = _link_default_attributes();
  }
  else {
    $instance['settings']['attributes'] += _link_default_attributes();
  }
561
562

  // Merge item attributes with attributes defined at the field level.
563
  $item['attributes'] += $instance['settings']['attributes'];
564
565
566

  // If user is not allowed to choose target attribute, use default defined at
  // field level.
567
568
  if ($instance['settings']['attributes']['target'] != LINK_TARGET_USER) {
    $item['attributes']['target'] = $instance['settings']['attributes']['target'];
569
  }
570
571
572
  elseif ($item['attributes']['target'] == LINK_TARGET_USER) {
    $item['attributes']['target'] = LINK_TARGET_DEFAULT;
  }
573
574

  // Remove the target attribute if the default (no target) is selected.
575
  if (empty($item['attributes']) || (isset($item['attributes']['target']) && $item['attributes']['target'] == LINK_TARGET_DEFAULT)) {
576
577
578
    unset($item['attributes']['target']);
  }

579
580
581
  // 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) ||
582
583
      ($instance['settings']['rel_remove'] != 'rel_remove_external' && $type != LINK_EXTERNAL)) {
      unset($item['attributes']['rel']);
584
    }
585
  }
586

587
588
  // Handle "title" link attribute.
  if (!empty($item['attributes']['title']) && module_exists('token')) {
589
    // Load the entity (necessary for entities in views).
590
591
592
593
594
595
596
    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;
    }
597
598
    $item['attributes']['title'] = token_replace($item['attributes']['title'], array($entity_token_type => $entity_loaded));
    $item['attributes']['title'] = filter_xss($item['attributes']['title'], array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
599
  }
600
601
602
603
604
605
606
607
608
609
  // Handle classes
  if (!empty($item['attributes']['class'])){
    $classes = explode(' ', $item['attributes']['class']);
    foreach ($classes as &$class) {
      $class = drupal_html_class($class);
    }
    $item['attributes']['class'] = implode(' ', $classes);
  }
  unset($item['attributes']['configurable_class']);

610
  // Remove title attribute if it's equal to link text.
611
  if (isset($item['attributes']['title']) && $item['attributes']['title'] == $item['title']) {
612
613
614
615
    unset($item['attributes']['title']);
  }
  unset($item['attributes']['configurable_title']);

616
617
  // Remove empty attributes.
  $item['attributes'] = array_filter($item['attributes']);
618
619
}

620
621
622
623
624
625
626
627
628
629
630
/**
 * Because parse_url doesn't work with relative urls.
 *
 * @param string $url
 *   URL to parse.
 *
 * @return Array
 *   Array of url pieces - only 'url', 'query', and 'fragment'.
 */
function _link_parse_url($url) {
  $url_parts = array();
631
  // Separate out the anchor, if any.
632
633
634
635
  if (strpos($url, '#') !== FALSE) {
    $url_parts['fragment'] = substr($url, strpos($url, '#') + 1);
    $url = substr($url, 0, strpos($url, '#'));
  }
636
  // Separate out the query string, if any.
637
638
  if (strpos($url, '?') !== FALSE) {
    $query = substr($url, strpos($url, '?') + 1);
639
    $url_parts['query'] = _link_parse_str($query);
640
641
642
643
644
645
    $url = substr($url, 0, strpos($url, '?'));
  }
  $url_parts['url'] = $url;
  return $url_parts;
}

646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
/**
 * Bacause parse_str replaces the following characters in query parameters name
 * in order to maintain compability with deprecated register_globals directive:
 *
 *   - chr(32) ( ) (space)
 *   - chr(46) (.) (dot)
 *   - chr(91) ([) (open square bracket)
 *   - chr(128) - chr(159) (various)
 *
 * @param string $query
 *   Query to parse.
 *
 * @return Array
 *   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) {
668
    $name_value = explode('=', $pair, 2);
669
670
671
672
673
674
675
676
    $name = urldecode($name_value[0]);
    $value = isset($name_value[1]) ? urldecode($name_value[1]) : NULL;
    $query_array[$name] = $value;
  }

  return $query_array;
}

677
/**
678
 * Implements hook_theme().
679
 */
680
681
function link_theme() {
  return array(
682
    'link_formatter_link_default' => array(
683
      'variables' => array('element' => NULL, 'field' => NULL),
684
    ),
685
    'link_formatter_link_plain' => array(
686
      'variables' => array('element' => NULL, 'field' => NULL),
687
    ),
688
    'link_formatter_link_absolute' => array(
689
      'variables' => array('element' => NULL, 'field' => NULL),
690
    ),
691
    'link_formatter_link_domain' => array(
692
      'variables' => array('element' => NULL, 'display' => NULL, 'field' => NULL),
693
    ),
694
    'link_formatter_link_title_plain' => array(
695
      'variables' => array('element' => NULL, 'field' => NULL),
696
    ),
697
    'link_formatter_link_url' => array(
698
      'variables' => array('element' => NULL, 'field' => NULL),
699
    ),
700
    'link_formatter_link_short' => array(
701
      'variables' => array('element' => NULL, 'field' => NULL),
702
    ),
703
    'link_formatter_link_label' => array(
704
      'variables' => array('element' => NULL, 'field' => NULL),
705
    ),
706
    'link_formatter_link_separate' => array(
707
      'variables' => array('element' => NULL, 'field' => NULL),
708
    ),
709
    'link_field' => array(
710
      'render element' => 'element',
711
712
713
714
715
    ),
  );
}

/**
716
 * Formats a link field widget.
717
 */
718
function theme_link_field($vars) {
719
  drupal_add_css(drupal_get_path('module', 'link') . '/link.css');
720
  $element = $vars['element'];
721
722
  // Prefix single value link fields with the name of the field.
  if (empty($element['#field']['multiple'])) {
723
    if (isset($element['url']) && !isset($element['title'])) {
724
      $element['url']['#title_display'] = 'invisible';
725
    }
726
727
  }

728
  $output = '';
729
  $output .= '<div class="link-field-subrow clearfix">';
730
  if (isset($element['title'])) {
731
    $output .= '<div class="link-field-title link-field-column">' . drupal_render($element['title']) . '</div>';
732
  }
733
  $output .= '<div class="link-field-url' . (isset($element['title']) ? ' link-field-column' : '') . '">' . drupal_render($element['url']) . '</div>';
734
  $output .= '</div>';
735
  if (!empty($element['attributes']['target'])) {
736
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['target']) . '</div>';
737
  }
738
  if (!empty($element['attributes']['title'])) {
739
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['title']) . '</div>';
740
  }
741
742
743
  if (!empty($element['attributes']['class'])) {
    $output .= '<div class="link-attributes">'. drupal_render($element['attributes']['class']) .'</div>';
  }
744
  $output .= drupal_render_children($element);
745
  return $output;
746
747
}

748
/**
749
 * Implements hook_element_info().
750
 */
751
function link_element_info() {
752
  $elements = array();
753
  $elements['link_field'] = array(
754
    '#input' => TRUE,
755
    '#process' => array('link_field_process'),
756
757
    '#theme' => 'link_field',
    '#theme_wrappers' => array('form_element'),
758
759
760
761
  );
  return $elements;
}

762
763
764
/**
 * Returns the default attributes and their values.
 */
765
766
767
768
769
770
771
772
function _link_default_attributes() {
  return array(
    'target' => LINK_TARGET_DEFAULT,
    'class' => '',
    'rel' => '',
  );
}

773
/**
774
 * Processes the link type element before displaying the field.
775
776
777
778
 *
 * Build the form element. When creating a form using FAPI #process,
 * note that $element['#value'] is already set.
 *
779
 * The $fields array is in $complete_form['#field_info'][$element['#field_name']].
780
 */
781
function link_field_process($element, $form_state, $complete_form) {
782
783
  $instance = field_widget_instance($element, $form_state);
  $settings = $instance['settings'];
784
785
786
787
788
789
790
791
  $element['url'] = array(
    '#type' => 'textfield',
    '#maxlength' => LINK_URL_MAX_LENGTH,
    '#title' => t('URL'),
    '#required' => ($element['#delta'] == 0 && $settings['url'] !== 'optional') ? $element['#required'] : FALSE,
    '#default_value' => isset($element['#value']['url']) ? $element['#value']['url'] : NULL,
  );
  if ($settings['title'] !== 'none' && $settings['title'] !== 'value') {
792
793
794
795
796
797
798
799
800
801
802
    // Figure out the label of the title field.
    if (!empty($settings['title_label_use_field_label'])) {
      // Use the element label as the title field label.
      $title_label = $element['#title'];
      // Hide the field label because there is no need for the duplicate labels.
      $element['#title_display'] = 'invisible';
    }
    else {
      $title_label = t('Title');
    }

803
804
    $element['title'] = array(
      '#type' => 'textfield',
805
      '#maxlength' => $settings['title_maxlength'],
806
      '#title' => $title_label,
807
      '#description' => t('The link title is limited to @maxlength characters maximum.', array('@maxlength' => $settings['title_maxlength'])),
808
      '#required' => ($settings['title'] == 'required' && (($element['#delta'] == 0 && $element['#required']) || !empty($element['#value']['url']))) ? TRUE : FALSE,
809
810
811
812
813
814
815
816
      '#default_value' => isset($element['#value']['title']) ? $element['#value']['title'] : NULL,
    );
  }

  // Initialize field attributes as an array if it is not an array yet.
  if (!is_array($settings['attributes'])) {
    $settings['attributes'] = array();
  }
817
  // Add default attributes.
818
819
820
821
822
823
824
  $settings['attributes'] += _link_default_attributes();
  $attributes = isset($element['#value']['attributes']) ? $element['#value']['attributes'] : $settings['attributes'];
  if (!empty($settings['attributes']['target']) && $settings['attributes']['target'] == LINK_TARGET_USER) {
    $element['attributes']['target'] = array(
      '#type' => 'checkbox',
      '#title' => t('Open URL in a New Window'),
      '#return_value' => LINK_TARGET_NEW_WINDOW,
825
      '#default_value' => isset($attributes['target']) ? $attributes['target'] : FALSE,
826
827
    );
  }
828
829
830
831
832
833
834
835
836
  if (!empty($settings['attributes']['configurable_title']) && $settings['attributes']['configurable_title'] == 1) {
    $element['attributes']['title'] = array(
      '#type' => 'textfield',
      '#title' => t('Link "title" attribute'),
      '#default_value' => isset($attributes['title']) ? $attributes['title'] : '',
      '#field_prefix' => 'title = "',
      '#field_suffix' => '"',
    );
  }
837
838
839
840
841
842
843
844
845
  if (!empty($settings['attributes']['configurable_class']) && $settings['attributes']['configurable_class'] == 1) {
    $element['attributes']['class'] = array(
      '#type' => 'textfield',
      '#title' => t('Custom link class'),
      '#default_value' => isset($attributes['class']) ? $attributes['class'] : '',
      '#field_prefix' => 'class = "',
      '#field_suffix' => '"',
    );
  }
846

847
848
849
850
851
852
853
  // If the title field is avaliable or there are field accepts multiple values
  // then allow the individual field items display the required asterisk if needed.
  if (isset($element['title']) || isset($element['_weight'])) {
    // To prevent an extra required indicator, disable the required flag on the
    // base element since all the sub-fields are already required if desired.
    $element['#required'] = FALSE;
  }
854

855
  return $element;
856
857
}

Nathan Haug's avatar
Nathan Haug committed
858
/**
859
 * Implements hook_field_formatter_info().
860
861
862
 */
function link_field_formatter_info() {
  return array(
863
    'link_default' => array(
864
      'label' => t('Title, as link (default)'),
865
866
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
867
    ),
868
869
870
871
872
    'link_title_plain' => array(
      'label' => t('Title, as plain text'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
873
    'link_url' => array(
874
      'label' => t('URL, as link'),
875
876
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
877
    ),
878
    'link_plain' => array(
879
      'label' => t('URL, as plain text'),
880
881
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
882
    ),
883
884
885
886
887
    'link_absolute' => array(
      'label' => t('URL, absolute'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
888
889
890
891
892
893
894
895
    'link_domain' => array(
      'label' => t('Domain, as link'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
      'settings' => array(
        'strip_www' => FALSE,
      ),
    ),
896
    'link_short' => array(
897
      'label' => t('Short, as link with title "Link"'),
898
899
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
900
    ),
901
    'link_label' => array(
902
      'label' => t('Label, as link with label as title'),
903
904
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
905
    ),
906
    'link_separate' => array(
907
      'label' => t('Separate title and URL'),
908
909
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
910
    ),
911
912
913
  );
}

914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
/**
 * Implements hook_field_formatter_settings_form().
 */
function link_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();
  if ($display['type'] == 'link_domain') {
    $element['strip_www'] = array(
      '#title' => t('Strip www. from domain'),
      '#type' => 'checkbox',
      '#default_value' => $settings['strip_www'],
    );
  }
  return $element;
}

/**