link.module 48.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|com|cat|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|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_maxlength' => 128,
50
51
52
53
        'enable_tokens' => 1,
        'display' => array(
          'url_cutoff' => 80,
        ),
54
        'validate_url' => 1,
55
      ),
56
      'default_widget' => 'link_field',
57
      'default_formatter' => 'link_default',
58
59
60
      // Support hook_entity_property_info() from contrib "Entity API".
      'property_type' => 'field_item_link',
      'property_callbacks' => array('link_field_property_info_callback'),
61
    ),
Nathan Haug's avatar
Nathan Haug committed
62
63
64
  );
}

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

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

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

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

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

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

110
  $form['title_maxlength'] = array(
111
112
113
114
115
116
    '#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,
117
  );
118

119
120
  if (module_exists('token')) {
    // Add token module replacements fields
121
122
123
124
125
    $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.'),
126
    );
127
    
128
    $entity_info = entity_get_info($instance['entity_type']);
129
    $form['tokens_help'] = array(
130
131
132
133
      '#theme' => 'token_tree',
      '#token_types' => array($entity_info['token type']),
      '#global_types' => TRUE,
      '#click_insert' => TRUE,
134
      '#dialog' => TRUE,
135
136
    );
  }
137

138
139
140
141
142
143
  $form['display'] = array(
    '#tree' => TRUE,
  );
  $form['display']['url_cutoff'] = array(
    '#type' => 'textfield',
    '#title' => t('URL Display Cutoff'),
144
    '#default_value' => isset($instance['settings']['display']['url_cutoff']) ? $instance['settings']['display']['url_cutoff'] : '80',
145
146
    '#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,
147
    '#size' => 3,
148
  );
149

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

/**
210
 * #element_validate handler for link_field_instance_settings_form().
211
212
 */
function link_field_settings_form_validate($element, &$form_state, $complete_form) {
213
  if ($form_state['values']['instance']['settings']['title'] === 'value' && empty($form_state['values']['instance']['settings']['title_value'])) {
214
215
    form_set_error('title_value', t('A default title must be provided if the title is a static value.'));
  }
216
  if (!empty($form_state['values']['instance']['settings']['display']['url_cutoff']) && !is_numeric($form_state['values']['instance']['settings']['display']['url_cutoff'])) {
217
218
    form_set_error('display', t('URL Display Cutoff value must be numeric.'));
  }
219
  if (empty($form_state['values']['instance']['settings']['title_maxlength'])) {
220
    form_set_value($element['title_maxlength'], '128', $form_state);
221
222
  }
  elseif (!is_numeric($form_state['values']['instance']['settings']['title_maxlength'])) {
223
    form_set_error('title_maxlength', t('The max length of the link title must be numeric.'));
224
225
  }
  elseif ($form_state['values']['instance']['settings']['title_maxlength'] > 255) {
226
227
    form_set_error('title_maxlength', t('The max length of the link title cannot be greater than 255 characters.'));
  }
228
229
}

230
/**
231
 * Implements hook_field_is_empty().
232
 */
233
function link_field_is_empty($item, $field) {
234
235
236
  return empty($item['title']) && empty($item['url']);
}

237
238
239
/**
 * Implements hook_field_load().
 */
240
241
242
function link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => $item) {
243
      $items[$id][$delta]['attributes'] = _link_load($field, $item, $instances[$id]);
244
    }
245
246
247
  }
}

Nathan Haug's avatar
Nathan Haug committed
248
/**
249
 * Implements hook_field_validate().
Nathan Haug's avatar
Nathan Haug committed
250
 */
251
252
function link_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  $optional_field_found = FALSE;
253
254
  if ($instance['settings']['validate_url'] !== 0 || is_null($instance['settings']['validate_url']) || !isset($instance['settings']['validate_url'])) {
    foreach ($items as $delta => $value) {
255
      _link_validate($items[$delta], $delta, $field, $entity, $instance, $langcode, $optional_field_found, $errors);
256
257
258
    }
  }

259
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] && !$optional_field_found) {
260
261
262
263
264
    $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),
    );
265
266
  }
}
267

268
/**
269
 * Implements hook_field_insert().
270
 */
271
272
273
274
275
276
277
278
279
280
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) {
281
282
  foreach ($items as $delta => $value) {
    _link_process($items[$delta], $delta, $field, $entity);
Nathan Haug's avatar
Nathan Haug committed
283
284
285
  }
}

286
287
288
289
290
291
292
293
294
295
296
/**
 * 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
297
/**
298
 * Implements hook_field_widget_info().
Nathan Haug's avatar
Nathan Haug committed
299
 */
300
function link_field_widget_info() {
Nathan Haug's avatar
Nathan Haug committed
301
  return array(
302
    'link_field' => array(
303
      'label' => 'Link',
304
305
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
Nathan Haug's avatar
Nathan Haug committed
306
307
308
309
310
    ),
  );
}

/**
311
 * Implements hook_field_widget_form().
Nathan Haug's avatar
Nathan Haug committed
312
 */
313
314
315
function link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $element += array(
    '#type' => $instance['widget']['type'],
316
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
317
  );
318
  return $element;
319
320
}

321
322
323
324
325
326
327
/**
 * 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']);
  }
328
  elseif ($error['error_element']['url']) {
329
330
331
332
    form_error($element['url'], $error['message']);
  }
}

333
/**
334
 * Unpacks the item attributes for use.
335
 */
336
337
function _link_load($field, $item, $instance) {
  if (isset($item['attributes'])) {
338
339
340
341
    if (!is_array($item['attributes'])) {
      $item['attributes'] = unserialize($item['attributes']);
    }
    return $item['attributes'];
342
  }
343
  elseif (isset($instance['settings']['attributes'])) {
344
345
346
347
    return $instance['settings']['attributes'];
  }
  else {
    return $field['settings']['attributes'];
348
  }
349
350
}

351
352
353
/**
 * Prepares the item attributes and url for storage.
 */
354
function _link_process(&$item, $delta = 0, $field, $entity) {
355
  // Trim whitespace from URL.
356
  $item['url'] = trim($item['url']);
357

358
359
  // If no attributes are set then make sure $item['attributes'] is an empty
  // array, so $field['attributes'] can override it.
360
  if (empty($item['attributes'])) {
361
    $item['attributes'] = array();
362
363
  }

364
  // Serialize the attributes array.
365
366
367
  if (!is_string($item['attributes'])) {
    $item['attributes'] = serialize($item['attributes']);
  }
368
369

  // Don't save an invalid default value (e.g. 'http://').
370
  if ((isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url']) && is_object($entity)) {
371
372
    if (!link_validate_url($item['url'])) {
      unset($item['url']);
373
374
375
376
    }
  }
}

377
378
379
/**
 * Validates that the link field has been entered properly.
 */
380
function _link_validate(&$item, $delta, $field, $entity, $instance, $langcode, &$optional_field_found, &$errors) {
381
  if ($item['url'] && !(isset($instance['default_value'][$delta]['url']) && $item['url'] === $instance['default_value'][$delta]['url'] && !$instance['required'])) {
382
383
    // Validate the link.
    if (link_validate_url(trim($item['url'])) == FALSE) {
384
385
386
387
388
      $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),
      );
389
390
    }
    // Require a title for the link if necessary.
391
    if ($instance['settings']['title'] == 'required' && strlen(trim($item['title'])) == 0) {
392
393
394
395
396
      $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),
      );
397
    }
398
  }
399
  // Require a link if we have a title.
400
  if ($instance['settings']['url'] !== 'optional' && strlen(isset($item['title']) ? $item['title'] : NULL) > 0 && strlen(trim($item['url'])) == 0) {
401
402
403
404
405
    $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),
    );
406
407
  }
  // In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link.
408
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
409
    $optional_field_found = TRUE;
410
  }
411
  // Require entire field
412
  if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] == 1 && !$optional_field_found && isset($instance['id'])) {
413
414
415
416
417
    $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),
    );
418
  }
419
420
421
}

/**
422
 * Clean up user-entered values for a link field according to field settings.
423
 *
424
425
426
427
428
429
 * @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.
430
431
 * @param $entity
 *   The entity containing this link.
432
 */
433
function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
434
435
436
437
  // Don't try to process empty links.
  if (empty($item['url']) && empty($item['title'])) {
    return;
  }
438
439
440
  if (empty($item['html'])) {
    $item['html'] = FALSE;
  }
441

442
  // Replace URL tokens.
443
444
  $entity_type = $instance['entity_type'];
  $entity_info = entity_get_info($entity_type);
445
  $property_id = $entity_info['entity keys']['id'];
446
447
448
  $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
  );
449
  if (isset($instance['settings']['enable_tokens']) && $instance['settings']['enable_tokens']) {
450
    global $user;
451
    // Load the entity if necessary for entities in views.
452
453
454
455
456
457
458
    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;
    }
459
    $item['url'] = token_replace($item['url'], array($entity_token_type => $entity_loaded));
460
461
462
  }

  $type = link_validate_url($item['url']);
463
464
  // If the type of the URL cannot be determined and URL validation is disabled,
  // then assume LINK_EXTERNAL for later processing.
465
466
467
  if ($type == FALSE && $instance['settings']['validate_url'] === 0) {
    $type = LINK_EXTERNAL;
  }
468
  $url = link_cleanup_url($item['url']);
469
  $url_parts = _link_parse_url($url);
470

471
472
473
474
475
476
477
478
479
480
  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,
      )
    );
  }
481
482

  // Create a shortened URL for display.
483
484
485
486
  if ($type == LINK_EMAIL) {
    $display_url = str_replace('mailto:', '', $url);
  }
  else {
487
    $display_url = url($url_parts['url'],
488
489
490
491
492
493
494
      array(
        'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
        'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
        'absolute' => TRUE,
      )
    );
  }
495
  if ($instance['settings']['display']['url_cutoff'] && strlen($display_url) > $instance['settings']['display']['url_cutoff']) {
496
    $display_url = substr($display_url, 0, $instance['settings']['display']['url_cutoff']) . "...";
497
498
499
  }
  $item['display_url'] = $display_url;

500
501
502
  // Use the title defined at the instance level.
  if ($instance['settings']['title'] == 'value' && strlen(trim($instance['settings']['title_value']))) {
    $title = $instance['settings']['title_value'];
503
504
505
506
    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);
    }
507
508
  }
  // Use the title defined by the user at the widget level.
509
  elseif (isset($item['title'])) {
510
511
    $title = $item['title'];
  }
512
513
514
  else {
    $title = '';
  }
515

516
  // Replace tokens.
517
  if ($title && ($instance['settings']['title'] == 'value' || $instance['settings']['enable_tokens'])) {
518
    // Load the entity if necessary for entities in views.
519
520
521
522
523
524
525
    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;
    }
526
527
    $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'));
528
    $item['html'] = TRUE;
529
  }
530
  $item['title'] = empty($title) ? $item['display_url'] : $title;
531

532
533
534
535
536
537
538
539
540
541
  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.
542
  if (!is_array($instance['settings']['attributes'])) {
543
544
545
546
547
    $instance['settings']['attributes'] = _link_default_attributes();
  }
  else {
    $instance['settings']['attributes'] += _link_default_attributes();
  }
548
549

  // Merge item attributes with attributes defined at the field level.
550
  $item['attributes'] += $instance['settings']['attributes'];
551
552
553

  // If user is not allowed to choose target attribute, use default defined at
  // field level.
554
555
  if ($instance['settings']['attributes']['target'] != LINK_TARGET_USER) {
    $item['attributes']['target'] = $instance['settings']['attributes']['target'];
556
  }
557
558
559
  elseif ($item['attributes']['target'] == LINK_TARGET_USER) {
    $item['attributes']['target'] = LINK_TARGET_DEFAULT;
  }
560
561

  // Remove the target attribute if the default (no target) is selected.
562
  if (empty($item['attributes']) || (isset($item['attributes']['target']) && $item['attributes']['target'] == LINK_TARGET_DEFAULT)) {
563
564
565
    unset($item['attributes']['target']);
  }

566
567
568
  // 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) ||
569
570
      ($instance['settings']['rel_remove'] != 'rel_remove_external' && $type != LINK_EXTERNAL)) {
      unset($item['attributes']['rel']);
571
    }
572
  }
573

574
575
  // Handle "title" link attribute.
  if (!empty($item['attributes']['title']) && module_exists('token')) {
576
    // Load the entity (necessary for entities in views).
577
578
579
580
581
582
583
    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;
    }
584
585
    $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'));
586
587
  }
  // Remove title attribute if it's equal to link text.
588
  if (isset($item['attributes']['title']) && $item['attributes']['title'] == $item['title']) {
589
590
591
592
    unset($item['attributes']['title']);
  }
  unset($item['attributes']['configurable_title']);

593
594
  // Remove empty attributes.
  $item['attributes'] = array_filter($item['attributes']);
595
596
}

597
598
599
600
601
602
603
604
605
606
607
/**
 * 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();
608
  // Separate out the anchor, if any.
609
610
611
612
  if (strpos($url, '#') !== FALSE) {
    $url_parts['fragment'] = substr($url, strpos($url, '#') + 1);
    $url = substr($url, 0, strpos($url, '#'));
  }
613
  // Separate out the query string, if any.
614
615
  if (strpos($url, '?') !== FALSE) {
    $query = substr($url, strpos($url, '?') + 1);
616
    $url_parts['query'] = _link_parse_str($query);
617
618
619
620
621
622
    $url = substr($url, 0, strpos($url, '?'));
  }
  $url_parts['url'] = $url;
  return $url_parts;
}

623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
/**
 * 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) {
    $name_value = explode('=', $pair);
    $name = urldecode($name_value[0]);
    $value = isset($name_value[1]) ? urldecode($name_value[1]) : NULL;
    $query_array[$name] = $value;
  }

  return $query_array;
}

654
/**
655
 * Implements hook_theme().
656
 */
657
658
function link_theme() {
  return array(
659
    'link_formatter_link_default' => array(
660
      'variables' => array('element' => NULL, 'field' => NULL),
661
    ),
662
    'link_formatter_link_plain' => array(
663
      'variables' => array('element' => NULL, 'field' => NULL),
664
    ),
665
    'link_formatter_link_absolute' => array(
666
      'variables' => array('element' => NULL, 'field' => NULL),
667
    ),
668
    'link_formatter_link_domain' => array(
669
      'variables' => array('element' => NULL, 'display' => NULL, 'field' => NULL),
670
    ),
671
    'link_formatter_link_title_plain' => array(
672
      'variables' => array('element' => NULL, 'field' => NULL),
673
    ),
674
    'link_formatter_link_url' => array(
675
      'variables' => array('element' => NULL, 'field' => NULL),
676
    ),
677
    'link_formatter_link_short' => array(
678
      'variables' => array('element' => NULL, 'field' => NULL),
679
    ),
680
    'link_formatter_link_label' => array(
681
      'variables' => array('element' => NULL, 'field' => NULL),
682
    ),
683
    'link_formatter_link_separate' => array(
684
      'variables' => array('element' => NULL, 'field' => NULL),
685
    ),
686
    'link_field' => array(
687
      'render element' => 'element',
688
689
690
691
692
    ),
  );
}

/**
693
 * Formats a link field widget.
694
 */
695
function theme_link_field($vars) {
696
  drupal_add_css(drupal_get_path('module', 'link') . '/link.css');
697
  $element = $vars['element'];
698
699
  // Prefix single value link fields with the name of the field.
  if (empty($element['#field']['multiple'])) {
700
    if (isset($element['url']) && !isset($element['title'])) {
701
      $element['url']['#title_display'] = 'invisible';
702
    }
703
704
  }

705
  $output = '';
706
  $output .= '<div class="link-field-subrow clearfix">';
707
  if (isset($element['title'])) {
708
    $output .= '<div class="link-field-title link-field-column">' . drupal_render($element['title']) . '</div>';
709
  }
710
  $output .= '<div class="link-field-url' . (isset($element['title']) ? ' link-field-column' : '') . '">' . drupal_render($element['url']) . '</div>';
711
  $output .= '</div>';
712
  if (!empty($element['attributes']['target'])) {
713
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['target']) . '</div>';
714
  }
715
  if (!empty($element['attributes']['title'])) {
716
    $output .= '<div class="link-attributes">' . drupal_render($element['attributes']['title']) . '</div>';
717
  }
718
  $output .= drupal_render_children($element);
719
  return $output;
720
721
}

722
/**
723
 * Implements hook_element_info().
724
 */
725
function link_element_info() {
726
  $elements = array();
727
  $elements['link_field'] = array(
728
    '#input' => TRUE,
729
    '#process' => array('link_field_process'),
730
731
    '#theme' => 'link_field',
    '#theme_wrappers' => array('form_element'),
732
733
734
735
  );
  return $elements;
}

736
737
738
/**
 * Returns the default attributes and their values.
 */
739
740
741
742
743
744
745
746
function _link_default_attributes() {
  return array(
    'target' => LINK_TARGET_DEFAULT,
    'class' => '',
    'rel' => '',
  );
}

747
/**
748
 * Processes the link type element before displaying the field.
749
750
751
752
 *
 * Build the form element. When creating a form using FAPI #process,
 * note that $element['#value'] is already set.
 *
753
 * The $fields array is in $complete_form['#field_info'][$element['#field_name']].
754
 */
755
function link_field_process($element, $form_state, $complete_form) {
756
757
  $instance = field_widget_instance($element, $form_state);
  $settings = $instance['settings'];
758
759
760
761
762
763
764
765
766
767
  $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') {
    $element['title'] = array(
      '#type' => 'textfield',
768
      '#maxlength' => $settings['title_maxlength'],
769
      '#title' => t('Title'),
770
      '#description' => t('The link title is limited to @maxlength characters maximum.', array('@maxlength' => $settings['title_maxlength'])),
771
      '#required' => ($settings['title'] == 'required' && (($element['#delta'] == 0 && $element['#required']) || !empty($element['#value']['url']))) ? TRUE : FALSE,
772
773
774
775
776
777
778
779
      '#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();
  }
780
  // Add default attributes.
781
782
783
784
785
786
787
  $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,
788
      '#default_value' => isset($attributes['target']) ? $attributes['target'] : FALSE,
789
790
    );
  }
791
792
793
794
795
796
797
798
799
  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' => '"',
    );
  }
800

801
802
803
804
805
806
807
  // 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;
  }
808

809
  return $element;
810
811
}

Nathan Haug's avatar
Nathan Haug committed
812
/**
813
 * Implements hook_field_formatter_info().
814
815
816
 */
function link_field_formatter_info() {
  return array(
817
    'link_default' => array(
818
      'label' => t('Title, as link (default)'),
819
820
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
821
    ),
822
823
824
825
826
    'link_title_plain' => array(
      'label' => t('Title, as plain text'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
827
    'link_url' => array(
828
      'label' => t('URL, as link'),
829
830
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
831
    ),
832
    'link_plain' => array(
833
      'label' => t('URL, as plain text'),
834
835
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
836
    ),
837
838
839
840
841
    'link_absolute' => array(
      'label' => t('URL, absolute'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
842
843
844
845
846
847
848
849
    'link_domain' => array(
      'label' => t('Domain, as link'),
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
      'settings' => array(
        'strip_www' => FALSE,
      ),
    ),
850
    'link_short' => array(
851
      'label' => t('Short, as link with title "Link"'),
852
853
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
854
    ),
855
    'link_label' => array(
856
      'label' => t('Label, as link with label as title'),
857
858
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
859
    ),
860
    'link_separate' => array(
861
      'label' => t('Separate title and URL'),
862
863
      'field types' => array('link_field'),
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
864
    ),
865
866
867
  );
}

868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
/**
 * 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;
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function link_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  if ($display['type'] == 'link_domain') {
    if ($display['settings']['strip_www']) {
      return t('Strip www. from domain');
    }
    else {
      return t('Leave www. in domain');
    }
  }
  return '';
}

902
903
904
/**
 * Implements hook_field_formatter_view().
 */
905
906
907
908
function link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $elements = array();
  foreach ($items as $delta => $item) {
    $elements[$delta] = array(
909
910
911
912
      '#theme' => 'link_formatter_' . $display['type'],
      '#element' => $item,
      '#field' => $instance,
      '#display' => $display,
913
914
915
916
917
    );
  }
  return $elements;
}

918
/**
919
 * Formats a link.
Nathan Haug's avatar
Nathan Haug committed
920
 */
921
function theme_link_formatter_link_default($vars) {
922
  $link_options = $vars['element'];
923
924
  unset($link_options['title']);
  unset($link_options['url']);
925

926
927
928
  if (isset($link_options['attributes']['class'])) {
    $link_options['attributes']['class'] = array($link_options['attributes']['class']);
  }
929
  // Display a normal link if both title and URL are available.
930
  if (!empty($vars['element']['title']) && !empty($vars['element']['url'])) {