Mercurial > hg > rr-repo
comparison sites/all/modules/references/node_reference/node_reference.module @ 4:ce11bbd8f642
added modules
author | danieleb <danielebarchiesi@me.com> |
---|---|
date | Thu, 19 Sep 2013 10:38:44 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
3:b28be78d8160 | 4:ce11bbd8f642 |
---|---|
1 <?php | |
2 | |
3 /** | |
4 * @file | |
5 * Defines a field type for referencing one node from another. | |
6 */ | |
7 | |
8 /** | |
9 * Implements hook_menu(). | |
10 */ | |
11 function node_reference_menu() { | |
12 $items['node_reference/autocomplete/%/%/%'] = array( | |
13 'page callback' => 'node_reference_autocomplete', | |
14 'page arguments' => array(2, 3, 4), | |
15 'access callback' => 'reference_autocomplete_access', | |
16 'access arguments' => array(2, 3, 4), | |
17 'type' => MENU_CALLBACK, | |
18 ); | |
19 return $items; | |
20 } | |
21 | |
22 /** | |
23 * Implements hook_field_info(). | |
24 */ | |
25 function node_reference_field_info() { | |
26 return array( | |
27 'node_reference' => array( | |
28 'label' => t('Node reference'), | |
29 'description' => t('This field stores the ID of a related node as an integer value.'), | |
30 'settings' => array( | |
31 'referenceable_types' => array(), | |
32 'view' => array( | |
33 'view_name' => '', | |
34 'display_name' => '', | |
35 'args' => array(), | |
36 ), | |
37 ), | |
38 // It probably make more sense to have the referenceable types be per-field than per-instance | |
39 // 'instance settings' => array('referenceable_types' => array()), | |
40 'default_widget' => 'options_select', // node_reference_autocomplete', | |
41 'default_formatter' => 'node_reference_default', | |
42 // Support hook_entity_property_info() from contrib "Entity API". | |
43 'property_type' => 'node', | |
44 // Support default token formatter for field tokens. | |
45 'default_token_formatter' => 'node_reference_plain', | |
46 ), | |
47 ); | |
48 } | |
49 | |
50 /** | |
51 * Implements hook_field_settings_form(). | |
52 */ | |
53 function node_reference_field_settings_form($field, $instance, $has_data) { | |
54 $settings = $field['settings']; | |
55 | |
56 $form = array(); | |
57 $form['referenceable_types'] = array( | |
58 '#type' => 'checkboxes', | |
59 '#title' => t('Content types that can be referenced'), | |
60 '#multiple' => TRUE, | |
61 '#default_value' => $settings['referenceable_types'], | |
62 '#options' => array_map('check_plain', node_type_get_names()), | |
63 ); | |
64 | |
65 if (module_exists('views')) { | |
66 $view_settings = $settings['view']; | |
67 | |
68 $description = '<p>' . t('The list of nodes that can be referenced can provided by a view (Views module) using the "References" display type.') . '</p>'; | |
69 | |
70 // Special note for legacy fields migrated from D6. | |
71 if (!empty($view_settings['view_name']) && $view_settings['display_name'] == 'default') { | |
72 $description .= '<p><strong><span class="admin-missing">'. t("Important D6 migration note:") . '</span></strong>'; | |
73 $description .= '<br/>' . t("The field is currently configured to use the 'Master' display of the view %view_name.", array('%view_name' => $view_settings['view_name'])); | |
74 $description .= '<br/>' . t("It is highly recommended that you: <br/>- edit this view and create a new display using the 'References' display type, <br/>- update the field settings to explicitly select the correct view and display."); | |
75 $description .= '<br/>' . t("The field will work correctly until then, but submitting this form might inadvertently change the field settings.") . '</p>'; | |
76 } | |
77 | |
78 $form['view'] = array( | |
79 '#type' => 'fieldset', | |
80 '#title' => t('Views - Nodes that can be referenced'), | |
81 '#collapsible' => TRUE, | |
82 '#collapsed' => empty($view_settings['view_name']), | |
83 '#description' => $description, | |
84 ); | |
85 | |
86 $views_options = references_get_views_options('node'); | |
87 if ($views_options) { | |
88 // The value of the 'view_and_display' select below will need to be split | |
89 // into 'view_name' and 'view_display' in the final submitted values, so | |
90 // we massage the data at validate time on the wrapping element (not | |
91 // ideal). | |
92 $form['view']['#element_validate'] = array('_node_reference_view_settings_validate'); | |
93 | |
94 $views_options = array('' => '<' . t('none') . '>') + $views_options; | |
95 $default = empty($view_settings['view_name']) ? '' : $view_settings['view_name'] . ':' .$view_settings['display_name']; | |
96 $form['view']['view_and_display'] = array( | |
97 '#type' => 'select', | |
98 '#title' => t('View used to select the nodes'), | |
99 '#options' => $views_options, | |
100 '#default_value' => $default, | |
101 '#description' => '<p>' . t('Choose the view and display that select the nodes that can be referenced.<br />Only views with a display of type "References" are eligible.') . '</p>' . | |
102 t('Note:<ul><li>This will discard the "Content types" settings above. Use the view\'s "filters" section instead.</li><li>Use the view\'s "fields" section to display additional informations about candidate nodes on node creation/edition form.</li><li>Use the view\'s "sort criteria" section to determine the order in which candidate nodes will be displayed.</li></ul>'), | |
103 ); | |
104 | |
105 $default = implode(', ', $view_settings['args']); | |
106 $form['view']['args'] = array( | |
107 '#type' => 'textfield', | |
108 '#title' => t('View arguments'), | |
109 '#default_value' => $default, | |
110 '#required' => FALSE, | |
111 '#description' => t('Provide a comma separated list of arguments to pass to the view.'), | |
112 ); | |
113 } | |
114 else { | |
115 $form['view']['no_view_help'] = array( | |
116 '#markup' => '<p>' . t('No eligible view was found.') .'</p>', | |
117 ); | |
118 } | |
119 } | |
120 | |
121 return $form; | |
122 } | |
123 | |
124 /** | |
125 * Validate callback for the 'view settings' fieldset. | |
126 * | |
127 * Puts back the various form values in the expected shape. | |
128 */ | |
129 function _node_reference_view_settings_validate($element, &$form_state, $form) { | |
130 // Split view name and display name from the 'view_and_display' value. | |
131 if (!empty($element['view_and_display']['#value'])) { | |
132 list($view, $display) = explode(':', $element['view_and_display']['#value']); | |
133 } | |
134 else { | |
135 $view = ''; | |
136 $display = ''; | |
137 } | |
138 | |
139 // Explode the 'args' string into an actual array. Beware, explode() turns an | |
140 // empty string into an array with one empty string. We'll need an empty array | |
141 // instead. | |
142 $args_string = trim($element['args']['#value']); | |
143 $args = ($args_string === '') ? array() : array_map('trim', explode(',', $args_string)); | |
144 | |
145 $value = array('view_name' => $view, 'display_name' => $display, 'args' => $args); | |
146 form_set_value($element, $value, $form_state); | |
147 } | |
148 | |
149 /** | |
150 * Implements hook_field_validate(). | |
151 * | |
152 * Possible error codes: | |
153 * - 'invalid_nid': nid is not valid for the field (not a valid node id, or the node is not referenceable). | |
154 */ | |
155 function node_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { | |
156 // Extract nids to check. | |
157 $ids = array(); | |
158 | |
159 // First check non-numeric "nid's to avoid losing time with them. | |
160 foreach ($items as $delta => $item) { | |
161 if (is_array($item) && !empty($item['nid'])) { | |
162 if (is_numeric($item['nid'])) { | |
163 $ids[] = $item['nid']; | |
164 } | |
165 else { | |
166 $errors[$field['field_name']][$langcode][$delta][] = array( | |
167 'error' => 'invalid_nid', | |
168 'message' => t("%name: invalid input.", | |
169 array('%name' => $instance['label'])), | |
170 ); | |
171 } | |
172 } | |
173 } | |
174 // Prevent performance hog if there are no ids to check. | |
175 if ($ids) { | |
176 $options = array( | |
177 'ids' => $ids, | |
178 ); | |
179 $refs = node_reference_potential_references($field, $options); | |
180 foreach ($items as $delta => $item) { | |
181 if (is_array($item)) { | |
182 if (!empty($item['nid']) && !isset($refs[$item['nid']])) { | |
183 $errors[$field['field_name']][$langcode][$delta][] = array( | |
184 'error' => 'invalid_nid', | |
185 'message' => t("%name: this post can't be referenced.", | |
186 array('%name' => $instance['label'])), | |
187 ); | |
188 } | |
189 } | |
190 } | |
191 } | |
192 } | |
193 | |
194 /** | |
195 * Implements hook_field_prepare_view(). | |
196 */ | |
197 function node_reference_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) { | |
198 $checked_ids = &drupal_static(__FUNCTION__, array()); | |
199 | |
200 // Set an 'access' property on each item (TRUE if the node exists and is | |
201 // accessible by the current user). | |
202 | |
203 // Extract ids to check. | |
204 $ids = array(); | |
205 foreach ($items as $id => $entity_items) { | |
206 foreach ($entity_items as $delta => $item) { | |
207 if (is_array($item)) { | |
208 // Default to 'not accessible'. | |
209 $items[$id][$delta]['access'] = FALSE; | |
210 if (!empty($item['nid']) && is_numeric($item['nid'])) { | |
211 $ids[$item['nid']] = $item['nid']; | |
212 } | |
213 } | |
214 } | |
215 } | |
216 | |
217 if ($ids) { | |
218 // Load information about ids that we haven't already loaded during this | |
219 // page request. | |
220 $ids_to_check = array_diff($ids, array_keys($checked_ids)); | |
221 if (!empty($ids_to_check)) { | |
222 $query = db_select('node', 'n') | |
223 ->addTag('node_access') | |
224 ->addMetaData('id', 'node_reference_field_prepare_view') | |
225 ->addMetaData('field', $field) | |
226 ->fields('n', array('nid')) | |
227 // WHERE n.nid IN (nids to check) AND ... | |
228 ->condition('n.nid', $ids_to_check, 'IN'); | |
229 | |
230 // Unless the user has the right permissions, restrict on the node status. | |
231 // (note: the 'view any unpublished content' permission is provided by the | |
232 // 'view_unpublished' contrib module.) | |
233 if (!user_access('bypass node access') && !user_access('view any unpublished content')) { | |
234 // ... AND n.status = 1 | |
235 $status_condition = db_or() | |
236 ->condition('n.status', NODE_PUBLISHED); | |
237 | |
238 // Take the 'view own unpublished content' permission into account to | |
239 // decide whether some unpublished nodes should still be visible. | |
240 if (user_access('view own unpublished content') && $own_unpublished = db_query('SELECT nid FROM {node} WHERE uid = :uid AND status = :status', array(':uid' => $GLOBALS['user']->uid, ':status' => NODE_NOT_PUBLISHED))->fetchCol()) { | |
241 // ... AND (n.status = 1 OR n.nid IN (own unpublished)) | |
242 $status_condition | |
243 ->condition('n.nid', $own_unpublished, 'IN'); | |
244 } | |
245 | |
246 $query->condition($status_condition); | |
247 } | |
248 | |
249 $accessible_ids = $query->execute()->fetchAllAssoc('nid'); | |
250 | |
251 // Populate our static list so that we do not query on those ids again. | |
252 foreach ($ids_to_check as $id) { | |
253 $checked_ids[$id] = isset($accessible_ids[$id]); | |
254 } | |
255 } | |
256 | |
257 foreach ($items as $id => $entity_items) { | |
258 foreach ($entity_items as $delta => $item) { | |
259 if (is_array($item) && !empty($item['nid']) && !empty($checked_ids[$item['nid']])) { | |
260 $items[$id][$delta]['access'] = TRUE; | |
261 } | |
262 } | |
263 } | |
264 } | |
265 } | |
266 | |
267 /** | |
268 * Implements hook_field_is_empty(). | |
269 */ | |
270 function node_reference_field_is_empty($item, $field) { | |
271 // nid = 0 is empty too, which is exactly what we want. | |
272 return empty($item['nid']); | |
273 } | |
274 | |
275 /** | |
276 * Implements hook_field_formatter_info(). | |
277 */ | |
278 function node_reference_field_formatter_info() { | |
279 $ret = array( | |
280 'node_reference_default' => array( | |
281 'label' => t('Title (link)'), | |
282 'description' => t('Display the title of the referenced node as a link to the node page.'), | |
283 'field types' => array('node_reference'), | |
284 ), | |
285 'node_reference_plain' => array( | |
286 'label' => t('Title (no link)'), | |
287 'description' => t('Display the title of the referenced node as plain text.'), | |
288 'field types' => array('node_reference'), | |
289 ), | |
290 'node_reference_node' => array( | |
291 'label' => t('Rendered node'), | |
292 'description' => t('Display the referenced node in a specific view mode'), | |
293 'field types' => array('node_reference'), | |
294 'settings' => array('node_reference_view_mode' => 'full'), | |
295 ), | |
296 'node_reference_nid' => array( | |
297 'label' => t('Node ID'), | |
298 'description' => t('Display the referenced node ID'), | |
299 'field types' => array('node_reference'), | |
300 ), | |
301 'node_reference_path' => array( | |
302 'label' => t('URL as plain text'), | |
303 'description' => t('Display the URL of the referenced node'), | |
304 'field types' => array('node_reference'), | |
305 'settings' => array( | |
306 'alias' => TRUE, | |
307 'absolute' => FALSE | |
308 ), | |
309 ), | |
310 ); | |
311 return $ret; | |
312 } | |
313 | |
314 /** | |
315 * Implements hook_field_formatter_settings_form(). | |
316 */ | |
317 function node_reference_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { | |
318 $display = $instance['display'][$view_mode]; | |
319 $settings = $display['settings']; | |
320 | |
321 $element = array(); | |
322 | |
323 switch ($display['type']) { | |
324 case 'node_reference_node': | |
325 $entity_info = entity_get_info('node'); | |
326 $modes = $entity_info['view modes']; | |
327 $options = array(); | |
328 foreach ($modes as $name => $mode) { | |
329 $options[$name] = $mode['label']; | |
330 } | |
331 $element['node_reference_view_mode'] = array( | |
332 '#title' => t('View mode'), | |
333 '#type' => 'select', | |
334 '#options' => $options, | |
335 '#default_value' => $settings['node_reference_view_mode'], | |
336 // Never empty, so no #empty_option | |
337 ); | |
338 break; | |
339 | |
340 case 'node_reference_path': | |
341 $element['alias'] = array( | |
342 '#type' => 'checkbox', | |
343 '#title' => t('Display the aliased path (if exists) instead of the system path'), | |
344 '#default_value' => $settings['alias'], | |
345 ); | |
346 $element['absolute'] = array( | |
347 '#type' => 'checkbox', | |
348 '#title' => t('Display an absolute URL'), | |
349 '#default_value' => $settings['absolute'], | |
350 ); | |
351 break; | |
352 } | |
353 | |
354 return $element; | |
355 } | |
356 | |
357 /** | |
358 * Implements hook_field_formatter_settings_summary(). | |
359 */ | |
360 function node_reference_field_formatter_settings_summary($field, $instance, $view_mode) { | |
361 $display = $instance['display'][$view_mode]; | |
362 $settings = $display['settings']; | |
363 $summary = array(); | |
364 | |
365 switch ($display['type']) { | |
366 case 'node_reference_node': | |
367 $entity_info = entity_get_info('node'); | |
368 $modes = $entity_info['view modes']; | |
369 $mode = $modes[$settings['node_reference_view_mode']]['label']; | |
370 $summary[] = t('View mode: %mode', array('%mode' => $mode)); | |
371 break; | |
372 | |
373 case 'node_reference_path': | |
374 $summary[] = t('Aliased path: %yes_no', array('%yes_no' => $settings['alias'] ? t('Yes') : t('No'))); | |
375 $summary[] = t('Absolute URL: %yes_no', array('%yes_no' => $settings['absolute'] ? t('Yes') : t('No'))); | |
376 break; | |
377 } | |
378 | |
379 return implode('<br />', $summary); | |
380 } | |
381 | |
382 /** | |
383 * Implements hook_field_formatter_prepare_view(). | |
384 * | |
385 * Preload all nodes referenced by items using 'full entity' formatters. | |
386 */ | |
387 function node_reference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) { | |
388 // Load the referenced nodes, except for the 'node_reference_nid' which does | |
389 // not need full objects. | |
390 | |
391 // Collect ids to load. | |
392 $ids = array(); | |
393 foreach ($displays as $id => $display) { | |
394 if ($display['type'] != 'node_reference_nid') { | |
395 foreach ($items[$id] as $delta => $item) { | |
396 if ($item['access']) { | |
397 $ids[$item['nid']] = $item['nid']; | |
398 } | |
399 } | |
400 } | |
401 } | |
402 $entities = node_load_multiple($ids); | |
403 | |
404 // Add the loaded nodes to the items. | |
405 foreach ($displays as $id => $display) { | |
406 if ($display['type'] != 'node_reference_nid') { | |
407 foreach ($items[$id] as $delta => $item) { | |
408 if ($item['access']) { | |
409 $items[$id][$delta]['node'] = $entities[$item['nid']]; | |
410 } | |
411 } | |
412 } | |
413 } | |
414 } | |
415 | |
416 /** | |
417 * Implements hook_field_formatter_view(). | |
418 */ | |
419 function node_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { | |
420 $settings = $display['settings']; | |
421 $result = array(); | |
422 | |
423 switch ($display['type']) { | |
424 case 'node_reference_default': | |
425 case 'node_reference_plain': | |
426 foreach ($items as $delta => $item) { | |
427 if ($item['access']) { | |
428 $node = $item['node']; | |
429 $label = entity_label('node', $node); | |
430 if ($display['type'] == 'node_reference_default') { | |
431 $uri = entity_uri('node', $node); | |
432 $result[$delta] = array( | |
433 '#type' => 'link', | |
434 '#title' => $label, | |
435 '#href' => $uri['path'], | |
436 '#options' => $uri['options'], | |
437 ); | |
438 } | |
439 else { | |
440 $result[$delta] = array( | |
441 '#markup' => check_plain($label), | |
442 ); | |
443 } | |
444 if (!$node->status) { | |
445 $result[$delta]['#prefix'] = '<span class="node-unpublished">'; | |
446 $result[$delta]['#suffix'] = '</span>'; | |
447 } | |
448 } | |
449 } | |
450 break; | |
451 | |
452 case 'node_reference_node': | |
453 // To prevent infinite recursion caused by reference cycles, we store | |
454 // diplayed nodes in a recursion queue. | |
455 $recursion_queue = &drupal_static(__FUNCTION__, array()); | |
456 | |
457 // If no 'referencing entity' is set, we are starting a new 'reference | |
458 // thread' and need to reset the queue. | |
459 // @todo Bug: $entity->referencing_entity on nodes referenced in a different | |
460 // thread on the page. E.g: 1 references 1+2 / 2 references 1+2 / visit homepage. | |
461 // We'd need a more accurate way... | |
462 if (!isset($entity->referencing_entity)) { | |
463 $recursion_queue = array(); | |
464 } | |
465 | |
466 // The recursion queue only needs to track nodes. | |
467 if ($entity_type == 'node') { | |
468 list($id) = entity_extract_ids($entity_type, $entity); | |
469 $recursion_queue[$id] = $id; | |
470 } | |
471 | |
472 // Check the recursion queue to determine which nodes should be fully | |
473 // displayed, and which nodes will only be displayed as a title. | |
474 $nodes_display = array(); | |
475 foreach ($items as $delta => $item) { | |
476 if ($item['access'] && !isset($recursion_queue[$item['nid']])) { | |
477 $nodes_display[$item['nid']] = $item['node']; | |
478 } | |
479 } | |
480 | |
481 // Load and build the fully displayed nodes. | |
482 if ($nodes_display) { | |
483 foreach ($nodes_display as $nid => $node) { | |
484 $nodes_display[$nid]->referencing_entity = $entity; | |
485 $nodes_display[$nid]->referencing_field = $field['field_name']; | |
486 } | |
487 $nodes_built = node_view_multiple($nodes_display, $settings['node_reference_view_mode']); | |
488 } | |
489 | |
490 // Assemble the render array. | |
491 foreach ($items as $delta => $item) { | |
492 if ($item['access']) { | |
493 if (isset($nodes_display[$item['nid']])) { | |
494 $result[$delta] = $nodes_built['nodes'][$item['nid']]; | |
495 } | |
496 else { | |
497 $node = $item['node']; | |
498 $label = entity_label('node', $node); | |
499 $uri = entity_uri('node', $node); | |
500 $result[$delta] = array( | |
501 '#type' => 'link', | |
502 '#title' => $label, | |
503 '#href' => $uri['path'], | |
504 '#options' => $uri['options'], | |
505 ); | |
506 if (!$node->status) { | |
507 $result[$delta]['#prefix'] = '<span class="node-unpublished">'; | |
508 $result[$delta]['#suffix'] = '</span>'; | |
509 } | |
510 } | |
511 } | |
512 } | |
513 break; | |
514 | |
515 case 'node_reference_nid': | |
516 foreach ($items as $delta => $item) { | |
517 if ($item['access']) { | |
518 $result[$delta] = array( | |
519 '#markup' => $item['nid'], | |
520 ); | |
521 } | |
522 } | |
523 break; | |
524 | |
525 case 'node_reference_path': | |
526 foreach ($items as $delta => $item) { | |
527 if ($item['access']) { | |
528 $uri = entity_uri('node', $item['node']); | |
529 $options = array( | |
530 'absolute' => $settings['absolute'], | |
531 'alias' => !$settings['alias'], | |
532 ); | |
533 | |
534 $options += $uri['options']; | |
535 $result[$delta] = array( | |
536 '#markup' => url($uri['path'], $options), | |
537 ); | |
538 } | |
539 } | |
540 break; | |
541 } | |
542 | |
543 return $result; | |
544 } | |
545 | |
546 /** | |
547 * Implements hook_field_widget_info(). | |
548 */ | |
549 function node_reference_field_widget_info() { | |
550 return array( | |
551 'node_reference_autocomplete' => array( | |
552 'label' => t('Autocomplete text field'), | |
553 'description' => t('Display the list of referenceable nodes as a textfield with autocomplete behaviour.'), | |
554 'field types' => array('node_reference'), | |
555 'settings' => array( | |
556 'autocomplete_match' => 'contains', | |
557 'size' => 60, | |
558 'autocomplete_path' => 'node_reference/autocomplete', | |
559 ), | |
560 ), | |
561 ); | |
562 } | |
563 | |
564 /** | |
565 * Implements hook_field_widget_info_alter(). | |
566 */ | |
567 function node_reference_field_widget_info_alter(&$info) { | |
568 $info['options_select']['field types'][] = 'node_reference'; | |
569 $info['options_buttons']['field types'][] = 'node_reference'; | |
570 } | |
571 | |
572 /** | |
573 * Implements hook_field_widget_settings_form(). | |
574 */ | |
575 function node_reference_field_widget_settings_form($field, $instance) { | |
576 $widget = $instance['widget']; | |
577 $defaults = field_info_widget_settings($widget['type']); | |
578 $settings = array_merge($defaults, $widget['settings']); | |
579 | |
580 $form = array(); | |
581 if ($widget['type'] == 'node_reference_autocomplete') { | |
582 $form['autocomplete_match'] = array( | |
583 '#type' => 'select', | |
584 '#title' => t('Autocomplete matching'), | |
585 '#default_value' => $settings['autocomplete_match'], | |
586 '#options' => array( | |
587 'starts_with' => t('Starts with'), | |
588 'contains' => t('Contains'), | |
589 ), | |
590 '#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of nodes.'), | |
591 ); | |
592 $form['size'] = array( | |
593 '#type' => 'textfield', | |
594 '#title' => t('Size of textfield'), | |
595 '#default_value' => $settings['size'], | |
596 '#element_validate' => array('_element_validate_integer_positive'), | |
597 '#required' => TRUE, | |
598 ); | |
599 } | |
600 return $form; | |
601 } | |
602 | |
603 /** | |
604 * Implements hook_field_widget_form(). | |
605 */ | |
606 function node_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { | |
607 switch ($instance['widget']['type']) { | |
608 case 'node_reference_autocomplete': | |
609 $element += array( | |
610 '#type' => 'textfield', | |
611 '#default_value' => isset($items[$delta]['nid']) ? $items[$delta]['nid'] : NULL, | |
612 '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'] . '/' . $field['field_name'], | |
613 '#size' => $instance['widget']['settings']['size'], | |
614 '#maxlength' => NULL, | |
615 '#element_validate' => array('node_reference_autocomplete_validate'), | |
616 '#value_callback' => 'node_reference_autocomplete_value', | |
617 ); | |
618 break; | |
619 } | |
620 | |
621 return array('nid' => $element); | |
622 } | |
623 | |
624 /** | |
625 * Value callback for a node_reference autocomplete element. | |
626 * | |
627 * Replace the node nid with a node title. | |
628 */ | |
629 function node_reference_autocomplete_value($element, $input = FALSE, $form_state) { | |
630 if ($input === FALSE) { | |
631 // We're building the displayed 'default value': expand the raw nid into | |
632 // "node title [nid:n]". | |
633 $nid = $element['#default_value']; | |
634 if (!empty($nid)) { | |
635 $q = db_select('node', 'n'); | |
636 $node_title_alias = $q->addField('n', 'title'); | |
637 $q->addTag('node_access') | |
638 ->condition('n.nid', $nid) | |
639 ->range(0, 1); | |
640 $result = $q->execute(); | |
641 // @todo If no result (node doesn't exist or no access). | |
642 $value = $result->fetchField(); | |
643 $value .= ' [nid:' . $nid . ']'; | |
644 return $value; | |
645 } | |
646 } | |
647 } | |
648 | |
649 /** | |
650 * Validation callback for a node_reference autocomplete element. | |
651 */ | |
652 function node_reference_autocomplete_validate($element, &$form_state, $form) { | |
653 $field = field_widget_field($element, $form_state); | |
654 $instance = field_widget_instance($element, $form_state); | |
655 | |
656 $value = $element['#value']; | |
657 $nid = NULL; | |
658 | |
659 if (!empty($value)) { | |
660 // Check whether we have an explicit "[nid:n]" input. | |
661 preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $value, $matches); | |
662 if (!empty($matches)) { | |
663 // Explicit nid. Check that the 'title' part matches the actual title for | |
664 // the nid. | |
665 list(, $title, $nid) = $matches; | |
666 if (!empty($title)) { | |
667 $real_title = db_select('node', 'n') | |
668 ->fields('n', array('title')) | |
669 ->condition('n.nid', $nid) | |
670 ->execute() | |
671 ->fetchField(); | |
672 if (trim($title) != trim($real_title)) { | |
673 form_error($element, t('%name: title mismatch. Please check your selection.', array('%name' => $instance['label']))); | |
674 } | |
675 } | |
676 } | |
677 else { | |
678 // No explicit nid (the submitted value was not populated by autocomplete | |
679 // selection). Get the nid of a referencable node from the entered title. | |
680 $options = array( | |
681 'string' => $value, | |
682 'match' => 'equals', | |
683 'limit' => 1, | |
684 ); | |
685 $references = node_reference_potential_references($field, $options); | |
686 if ($references) { | |
687 // @todo The best thing would be to present the user with an | |
688 // additional form, allowing the user to choose between valid | |
689 // candidates with the same title. ATM, we pick the first | |
690 // matching candidate... | |
691 $nid = key($references); | |
692 } | |
693 else { | |
694 form_error($element, t('%name: found no valid post with that title.', array('%name' => $instance['label']))); | |
695 } | |
696 } | |
697 } | |
698 | |
699 // Set the element's value as the node id that was extracted from the entered | |
700 // input. | |
701 form_set_value($element, $nid, $form_state); | |
702 } | |
703 | |
704 /** | |
705 * Implements hook_field_widget_error(). | |
706 */ | |
707 function node_reference_field_widget_error($element, $error, $form, &$form_state) { | |
708 form_error($element['nid'], $error['message']); | |
709 } | |
710 | |
711 /** | |
712 * Builds a list of referenceable nodes suitable for the '#option' FAPI property. | |
713 * | |
714 * Warning: the function does NOT take care of encoding or escaping the node | |
715 * titles. Proper massaging needs to be performed by the caller, according to | |
716 * the destination FAPI '#type' (radios / checkboxes / select). | |
717 * | |
718 * @param $field | |
719 * The field definition. | |
720 * @param $flat | |
721 * Whether optgroups are allowed. | |
722 * | |
723 * @return | |
724 * An array of referenceable node titles, keyed by node id. If the $flat | |
725 * parameter is TRUE, the list might be nested by optgroup first. | |
726 */ | |
727 function _node_reference_options($field, $flat = TRUE) { | |
728 $references = node_reference_potential_references($field); | |
729 | |
730 $options = array(); | |
731 foreach ($references as $key => $value) { | |
732 // The label, displayed in selects and checkboxes/radios, should have HTML | |
733 // entities unencoded. The widgets (core's options.module) take care of | |
734 // applying the relevant filters (strip_tags() or filter_xss()). | |
735 $label = html_entity_decode($value['rendered'], ENT_QUOTES); | |
736 if (empty($value['group']) || $flat) { | |
737 $options[$key] = $label; | |
738 } | |
739 else { | |
740 // The group name, displayed in selects, cannot contain tags, and should | |
741 // have HTML entities unencoded. | |
742 $group = html_entity_decode(strip_tags($value['group']), ENT_QUOTES); | |
743 $options[$group][$key] = $label; | |
744 } | |
745 } | |
746 | |
747 return $options; | |
748 } | |
749 | |
750 /** | |
751 * Retrieves an array of candidate referenceable nodes. | |
752 * | |
753 * This info is used in various places (allowed values, autocomplete | |
754 * results, input validation...). Some of them only need the nids, | |
755 * others nid + titles, others yet nid + titles + rendered row (for | |
756 * display in widgets). | |
757 * | |
758 * The array we return contains all the potentially needed information, | |
759 * and lets consumers use the parts they actually need. | |
760 * | |
761 * @param $field | |
762 * The field definition. | |
763 * @param $options | |
764 * An array of options to limit the scope of the returned list. The following | |
765 * key/value pairs are accepted: | |
766 * - string: string to filter titles on (used by autocomplete). | |
767 * - match: operator to match the above string against, can be any of: | |
768 * 'contains', 'equals', 'starts_with'. Defaults to 'contains'. | |
769 * - ids: array of specific node ids to lookup. | |
770 * - limit: maximum size of the the result set. Defaults to 0 (no limit). | |
771 * | |
772 * @return | |
773 * An array of valid nodes in the form: | |
774 * array( | |
775 * nid => array( | |
776 * 'title' => The node title, | |
777 * 'rendered' => The text to display in widgets (can be HTML) | |
778 * ), | |
779 * ... | |
780 * ) | |
781 */ | |
782 function node_reference_potential_references($field, $options = array()) { | |
783 // Fill in default options. | |
784 $options += array( | |
785 'string' => '', | |
786 'match' => 'contains', | |
787 'ids' => array(), | |
788 'limit' => 0, | |
789 ); | |
790 | |
791 $results = &drupal_static(__FUNCTION__, array()); | |
792 | |
793 // Create unique id for static cache. | |
794 $cid = $field['field_name'] . ':' . $options['match'] . ':' | |
795 . ($options['string'] !== '' ? $options['string'] : implode('-', $options['ids'])) | |
796 . ':' . $options['limit']; | |
797 if (!isset($results[$cid])) { | |
798 $references = FALSE; | |
799 if (module_exists('views') && !empty($field['settings']['view']['view_name'])) { | |
800 $references = _node_reference_potential_references_views($field, $options); | |
801 } | |
802 | |
803 if ($references === FALSE) { | |
804 $references = _node_reference_potential_references_standard($field, $options); | |
805 } | |
806 | |
807 // Store the results. | |
808 $results[$cid] = !empty($references) ? $references : array(); | |
809 } | |
810 | |
811 return $results[$cid]; | |
812 } | |
813 | |
814 /** | |
815 * Helper function for node_reference_potential_references(). | |
816 * | |
817 * Case of Views-defined referenceable nodes. | |
818 */ | |
819 function _node_reference_potential_references_views($field, $options) { | |
820 $settings = $field['settings']['view']; | |
821 $options['title_field'] = 'title'; | |
822 return references_potential_references_view('node', $settings['view_name'], $settings['display_name'], $settings['args'], $options); | |
823 } | |
824 | |
825 /** | |
826 * Helper function for node_reference_potential_references(). | |
827 * | |
828 * List of referenceable nodes defined by content types. | |
829 */ | |
830 function _node_reference_potential_references_standard($field, $options) { | |
831 // Avoid useless work | |
832 if (!count($field['settings']['referenceable_types'])) { | |
833 return array(); | |
834 } | |
835 | |
836 $query = db_select('node', 'n'); | |
837 $node_nid_alias = $query->addField('n', 'nid'); | |
838 $node_title_alias = $query->addField('n', 'title', 'node_title'); | |
839 $node_type_alias = $query->addField('n', 'type', 'node_type'); | |
840 $query->addTag('node_access') | |
841 ->addMetaData('id', ' _node_reference_potential_references_standard') | |
842 ->addMetaData('field', $field) | |
843 ->addMetaData('options', $options); | |
844 | |
845 if (is_array($field['settings']['referenceable_types'])) { | |
846 $query->condition('n.type', $field['settings']['referenceable_types'], 'IN'); | |
847 } | |
848 | |
849 if ($options['string'] !== '') { | |
850 switch ($options['match']) { | |
851 case 'contains': | |
852 $query->condition('n.title', '%' . $options['string'] . '%', 'LIKE'); | |
853 break; | |
854 | |
855 case 'starts_with': | |
856 $query->condition('n.title', $options['string'] . '%', 'LIKE'); | |
857 break; | |
858 | |
859 case 'equals': | |
860 default: // no match type or incorrect match type: use "=" | |
861 $query->condition('n.title', $options['string']); | |
862 break; | |
863 } | |
864 } | |
865 | |
866 if ($options['ids']) { | |
867 $query->condition('n.nid', $options['ids'], 'IN'); | |
868 } | |
869 | |
870 if ($options['limit']) { | |
871 $query->range(0, $options['limit']); | |
872 } | |
873 | |
874 $query | |
875 ->orderBy($node_title_alias) | |
876 ->orderBy($node_type_alias); | |
877 | |
878 $result = $query->execute()->fetchAll(); | |
879 $references = array(); | |
880 foreach ($result as $node) { | |
881 $references[$node->nid] = array( | |
882 'title' => $node->node_title, | |
883 'rendered' => check_plain($node->node_title), | |
884 ); | |
885 } | |
886 return $references; | |
887 } | |
888 | |
889 /** | |
890 * Menu callback for the autocomplete results. | |
891 */ | |
892 function node_reference_autocomplete($entity_type, $bundle, $field_name, $string = '') { | |
893 $field = field_info_field($field_name); | |
894 $instance = field_info_instance($entity_type, $field_name, $bundle); | |
895 | |
896 $options = array( | |
897 'string' => $string, | |
898 'match' => $instance['widget']['settings']['autocomplete_match'], | |
899 'limit' => 10, | |
900 ); | |
901 $references = node_reference_potential_references($field, $options); | |
902 | |
903 $matches = array(); | |
904 foreach ($references as $id => $row) { | |
905 // Markup is fine in autocompletion results (might happen when rendered | |
906 // through Views) but we want to remove hyperlinks. | |
907 $suggestion = preg_replace('/<a href="([^<]*)">([^<]*)<\/a>/', '$2', $row['rendered']); | |
908 // Add a class wrapper for a few required CSS overrides. | |
909 $matches[$row['title'] . " [nid:$id]"] = '<div class="reference-autocomplete">' . $suggestion . '</div>'; | |
910 } | |
911 | |
912 drupal_json_output($matches); | |
913 } | |
914 | |
915 /** | |
916 * Implements hook_node_type_update(). | |
917 * | |
918 * Reflect type name changes to the 'referenceable types' settings: when | |
919 * the name of a type changes, the change needs to be reflected in the | |
920 * "referenceable types" setting for any node_reference field | |
921 * referencing it. | |
922 */ | |
923 function node_reference_node_type_update($info) { | |
924 if (!empty($info->old_type) && $info->old_type != $info->type) { | |
925 $fields = field_info_fields(); | |
926 foreach ($fields as $field_name => $field) { | |
927 if ($field['type'] == 'node_reference' && isset($field['settings']['referenceable_types'][$info->old_type])) { | |
928 $field['settings']['referenceable_types'][$info->type] = empty($field['settings']['referenceable_types'][$info->old_type]) ? 0 : $info->type; | |
929 unset($field['settings']['referenceable_types'][$info->old_type]); | |
930 field_update_field($field); | |
931 } | |
932 } | |
933 } | |
934 } | |
935 | |
936 /** | |
937 * Theme preprocess function. | |
938 * | |
939 * Allows specific node templates for nodes displayed as values of a | |
940 * node_reference field with a specific view mode. | |
941 */ | |
942 function node_reference_preprocess_node(&$vars) { | |
943 // The 'referencing_field' attribute of the node is added by the | |
944 // node_reference_node mode formatter (display referenced node | |
945 // in a specific view mode). | |
946 if (!empty($vars['node']->referencing_field)) { | |
947 $node = $vars['node']; | |
948 $field_name = $node->referencing_field; | |
949 $vars['theme_hook_suggestions'][] = 'node__node_reference'; | |
950 $vars['theme_hook_suggestions'][] = 'node__node_reference__' . $field_name; | |
951 $vars['theme_hook_suggestions'][] = 'node__node_reference__' . $node->type; | |
952 $vars['theme_hook_suggestions'][] = 'node__node_reference__' . $field_name . '__' . $node->type; | |
953 } | |
954 } | |
955 | |
956 /** | |
957 * Implements hook_field_prepare_translation(). | |
958 * | |
959 * When preparing a translation, load any translations of existing | |
960 * references. | |
961 */ | |
962 function node_reference_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) { | |
963 if (isset($items) && is_array($items)) { | |
964 // Match each reference with its matching translation, if it exists. | |
965 foreach ($items as $key => $item) { | |
966 $reference_node = node_load($item['nid']); | |
967 $items[$key]['nid'] = node_reference_find_translation($reference_node, $entity->language); | |
968 } | |
969 } | |
970 } | |
971 | |
972 /** | |
973 * Find a translation for a specific node reference, if it exists. | |
974 * | |
975 * @param $reference_node | |
976 * The untranslated node reference. | |
977 * @param $langcode | |
978 * | |
979 * @return | |
980 * A nid for the translation of the node reference, | |
981 * otherwise the original untranslated nid if no translation exists. | |
982 */ | |
983 function node_reference_find_translation($reference_node, $langcode) { | |
984 // Check if the source node translation is set and if translations are supported. | |
985 if (isset($reference_node->tnid) && translation_supported_type($reference_node->type)) { | |
986 // Determine whether an alternative language is being used. | |
987 if (!empty($reference_node->language) && $reference_node->language != $langcode) { | |
988 // Return a corresponding translation nid for the reference (if it exists). | |
989 $translations = translation_node_get_translations($reference_node->tnid); | |
990 if (isset($translations[$langcode])) { | |
991 return $translations[$langcode]->nid; | |
992 } | |
993 } | |
994 } | |
995 // Return the untranslated reference nid, no matching translations found. | |
996 return $reference_node->nid; | |
997 } | |
998 | |
999 /** | |
1000 * Implements hook_options_list(). | |
1001 */ | |
1002 function node_reference_options_list($field) { | |
1003 return _node_reference_options($field, FALSE); | |
1004 } | |
1005 | |
1006 /** | |
1007 * Implements hook_content_migrate_field_alter(). | |
1008 * | |
1009 * Use this to tweak the conversion of field settings from the D6 style to the | |
1010 * D7 style for specific situations not handled by basic conversion, as when | |
1011 * field types or settings are changed. | |
1012 * | |
1013 * $field_value['widget_type'] is available to | |
1014 * see what widget type was originally used. | |
1015 */ | |
1016 function node_reference_content_migrate_field_alter(&$field_value, $instance_value) { | |
1017 switch ($field_value['module']) { | |
1018 case 'nodereference': | |
1019 $field_value['module'] = 'node_reference'; | |
1020 $field_value['type'] = 'node_reference'; | |
1021 | |
1022 // Translate 'view' settings. | |
1023 $view_name = isset($field_value['settings']['advanced_view']) ? $field_value['settings']['advanced_view'] : ''; | |
1024 $view_args = isset($field_value['settings']['advanced_view_args']) ? $field_value['settings']['advanced_view_args'] : ''; | |
1025 $view_args = array_map('trim', explode(',', $view_args)); | |
1026 $field_value['settings']['view'] = array( | |
1027 'view_name' => $view_name, | |
1028 'display_name' => 'default', | |
1029 'args' => $view_args, | |
1030 ); | |
1031 if ($view_name) { | |
1032 $field_value['messages'][] = t("The field uses the view @view_name to determine referenceable nodes. You will need to manually edit the view and add a display of type 'References'.", array('@view_name' => $view_name)); | |
1033 } | |
1034 unset($field_value['settings']['advanced_view']); | |
1035 unset($field_value['settings']['advanced_view_args']); | |
1036 | |
1037 break; | |
1038 } | |
1039 } | |
1040 | |
1041 /** | |
1042 * Implements hook_content_migrate_instance_alter(). | |
1043 * | |
1044 * Use this to tweak the conversion of instance or widget settings from the D6 | |
1045 * style to the D7 style for specific situations not handled by basic | |
1046 * conversion, as when formatter or widget names or settings are changed. | |
1047 */ | |
1048 function node_reference_content_migrate_instance_alter(&$instance_value, $field_value) { | |
1049 switch ($field_value['type']) { | |
1050 case 'nodereference': | |
1051 // Massage formatters. | |
1052 foreach ($instance_value['display'] as $context => &$display) { | |
1053 switch ($display['type']) { | |
1054 case 'full': | |
1055 case 'teaser': | |
1056 // Those two formatters have been merged into | |
1057 // 'node_reference_view_mode', with a formatter setting. | |
1058 $display['type'] = 'node_reference_node'; | |
1059 $display['settings']['node_reference_view_mode'] = $display['type']; | |
1060 break; | |
1061 | |
1062 default: | |
1063 // The formatter names changed, all are prefixed with | |
1064 // 'node_reference_'. | |
1065 $display['type'] = 'node_reference_' . $display['type']; | |
1066 break; | |
1067 } | |
1068 } | |
1069 // Massage the widget. | |
1070 switch ($instance_value['widget']['type']) { | |
1071 case 'nodereference_autocomplete': | |
1072 $instance_value['widget']['type'] = 'node_reference_autocomplete'; | |
1073 $instance_value['widget']['module'] = 'node_reference'; | |
1074 break; | |
1075 case 'nodereference_select': | |
1076 $instance_value['widget']['type'] = 'options_select'; | |
1077 $instance_value['widget']['module'] = 'options'; | |
1078 break; | |
1079 case 'nodereference_buttons': | |
1080 $instance_value['widget']['type'] = 'options_buttons'; | |
1081 $instance_value['widget']['module'] = 'options'; | |
1082 } | |
1083 break; | |
1084 } | |
1085 } | |
1086 | |
1087 /** | |
1088 * Implements hook_field_views_data(). | |
1089 * | |
1090 * In addition to the default field information we add the relationship for | |
1091 * views to connect back to the node table. | |
1092 */ | |
1093 function node_reference_field_views_data($field) { | |
1094 // No module_load_include(): this hook is invoked from | |
1095 // views/modules/field.views.inc, which is where that function is defined. | |
1096 $data = field_views_field_default_views_data($field); | |
1097 | |
1098 $storage = $field['storage']['details']['sql']; | |
1099 | |
1100 foreach ($storage as $age => $table_data) { | |
1101 $table = key($table_data); | |
1102 $columns = current($table_data); | |
1103 $id_column = $columns['nid']; | |
1104 if (isset($data[$table])) { | |
1105 // Filter: swap the handler to the 'in' operator. The callback receives | |
1106 // the field name instead of the whole $field structure to keep views | |
1107 // data to a reasonable size. | |
1108 $data[$table][$id_column]['filter']['handler'] = 'views_handler_filter_in_operator'; | |
1109 $data[$table][$id_column]['filter']['options callback'] = 'node_reference_views_filter_options'; | |
1110 $data[$table][$id_column]['filter']['options arguments'] = array($field['field_name']); | |
1111 | |
1112 // Argument: display node.title in argument titles (handled in our custom | |
1113 // handler) and summary lists (handled by the base views_handler_argument | |
1114 // handler). | |
1115 // Both mechanisms rely on the 'name table' and 'name field' information | |
1116 // below, by joining to a separate copy of the base table from the field | |
1117 // data table. | |
1118 $data[$table][$id_column]['argument']['handler'] = 'references_handler_argument'; | |
1119 $data[$table][$id_column]['argument']['name table'] = $table . '_reference'; | |
1120 $data[$table][$id_column]['argument']['name field'] = 'title'; | |
1121 $data[$table . '_reference']['table']['join'][$table] = array( | |
1122 'left_field' => $id_column, | |
1123 'table' => 'node', | |
1124 'field' => 'nid', | |
1125 ); | |
1126 | |
1127 // Relationship. | |
1128 $data[$table][$id_column]['relationship'] = array( | |
1129 'handler' => 'references_handler_relationship', | |
1130 'base' => 'node', | |
1131 'base field' => 'nid', | |
1132 'field' => $id_column, | |
1133 'label' => $field['field_name'], | |
1134 'field_name' => $field['field_name'], | |
1135 ); | |
1136 } | |
1137 } | |
1138 | |
1139 return $data; | |
1140 } | |
1141 | |
1142 /** | |
1143 * Implements hook_field_views_data_views_data_alter(). | |
1144 */ | |
1145 function node_reference_field_views_data_views_data_alter(&$data, $field) { | |
1146 foreach ($field['bundles'] as $entity_type => $bundles) { | |
1147 $entity_info = entity_get_info($entity_type); | |
1148 $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; | |
1149 | |
1150 list($label, $all_labels) = field_views_field_label($field['field_name']); | |
1151 $entity = $entity_info['label']; | |
1152 if ($entity == t('Node')) { | |
1153 $entity = t('Content'); | |
1154 } | |
1155 | |
1156 // Only specify target entity type if the field is used in more than one. | |
1157 if (count($field['bundles']) > 1) { | |
1158 $title = t('@field (@field_name) - reverse (to @entity)', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name'])); | |
1159 } | |
1160 else { | |
1161 $title = t('@field (@field_name) - reverse', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name'])); | |
1162 } | |
1163 $data['node'][$pseudo_field_name]['relationship'] = array( | |
1164 'title' => $title, | |
1165 'help' => t('Relate each @entity referencing the node through @field.', array('@entity' => $entity, '@field' => $label)), | |
1166 'handler' => 'views_handler_relationship_entity_reverse', | |
1167 'field_name' => $field['field_name'], | |
1168 'field table' => _field_sql_storage_tablename($field), | |
1169 'field field' => $field['field_name'] . '_nid', | |
1170 'base' => $entity_info['base table'], | |
1171 'base field' => $entity_info['entity keys']['id'], | |
1172 'label' => t('!field_name', array('!field_name' => $field['field_name'])), | |
1173 ); | |
1174 } | |
1175 } | |
1176 | |
1177 /** | |
1178 * 'options callback' for the views_handler_filter_in_operator filter. | |
1179 * | |
1180 * @param $field_name | |
1181 * The field name. | |
1182 */ | |
1183 function node_reference_views_filter_options($field_name) { | |
1184 $options = array(); | |
1185 | |
1186 if ($field = field_info_field($field_name)) { | |
1187 $options = _node_reference_options($field, TRUE); | |
1188 | |
1189 // The options are displayed in checkboxes within the filter admin form, and | |
1190 // in a select within an exposed filter. Checkboxes accept HTML, other | |
1191 // entities should be encoded; selects require the exact opposite: no HTML, | |
1192 // no encoding. We go for a middle ground: strip tags, leave entities | |
1193 // unencoded. | |
1194 foreach ($options as $key => $value) { | |
1195 $options[$key] = strip_tags($value); | |
1196 } | |
1197 } | |
1198 | |
1199 return $options; | |
1200 } |