SA-CONTRIB-2010-088 follow up fix for nodereference_autocomplete_access() and content...
[project/cck.git] / modules / nodereference / nodereference.module
CommitLineData
e1a2f8a6
JC
1<?php
2// $Id$
3
4/**
5 * @file
0f6ca9bb 6 * Defines a field type for referencing one node from another.
e1a2f8a6
JC
7 */
8
9/**
0f6ca9bb
JC
10 * Implementation of hook_menu().
11 */
4a6e00e0 12function nodereference_menu() {
0f6ca9bb 13 $items = array();
4a6e00e0 14 $items['nodereference/autocomplete'] = array(
c56d4554 15 'title' => 'Nodereference autocomplete',
4a6e00e0 16 'page callback' => 'nodereference_autocomplete',
d6d7c227
KS
17 'access callback' => 'nodereference_autocomplete_access',
18 'access arguments' => array(2),
4a6e00e0
YC
19 'type' => MENU_CALLBACK
20 );
0f6ca9bb
JC
21 return $items;
22}
23
24/**
b01b0973
KS
25 * Implementation of hook_theme().
26 */
27function nodereference_theme() {
28 return array(
ea73d362
KS
29 'nodereference_select' => array(
30 'arguments' => array('element' => NULL),
31 ),
578c7b49
YC
32 'nodereference_buttons' => array(
33 'arguments' => array('element' => NULL),
34 ),
ea73d362
KS
35 'nodereference_autocomplete' => array(
36 'arguments' => array('element' => NULL),
37 ),
e74ada9a
YC
38 'nodereference_formatter_default' => array(
39 'arguments' => array('element'),
40 ),
8f8522cd
KS
41 'nodereference_formatter_plain' => array(
42 'arguments' => array('element'),
43 ),
e74ada9a
YC
44 'nodereference_formatter_full' => array(
45 'arguments' => array('element'),
46 'function' => 'theme_nodereference_formatter_full_teaser',
47 ),
48 'nodereference_formatter_teaser' => array(
49 'arguments' => array('element'),
50 'function' => 'theme_nodereference_formatter_full_teaser',
51 ),
b01b0973
KS
52 );
53}
54
55/**
aff1f800
YC
56 * Implementaion of hook_ctools_plugin_directory().
57 */
58function nodereference_ctools_plugin_directory($module, $plugin) {
59 if ($module == 'ctools' && $plugin == 'relationships') {
60 return 'panels/' . $plugin;
61 }
62}
63
64/**
e1a2f8a6
JC
65 * Implementation of hook_field_info().
66 */
67function nodereference_field_info() {
68 return array(
e9b6b501 69 'nodereference' => array(
2400f882
KS
70 'label' => t('Node reference'),
71 'description' => t('Store the ID of a related node as an integer value.'),
c5482d81 72// 'content_icon' => 'icon_content_noderef.png',
6d7a097b 73 ),
e1a2f8a6
JC
74 );
75}
76
77/**
78 * Implementation of hook_field_settings().
79 */
e2c6522a 80function nodereference_field_settings($op, $field) {
e1a2f8a6
JC
81 switch ($op) {
82 case 'form':
83 $form = array();
1182d802
JC
84 $form['referenceable_types'] = array(
85 '#type' => 'checkboxes',
86 '#title' => t('Content types that can be referenced'),
87 '#multiple' => TRUE,
3973f7be 88 '#default_value' => is_array($field['referenceable_types']) ? $field['referenceable_types'] : array(),
4b205056 89 '#options' => array_map('check_plain', node_get_types('names')),
1182d802 90 );
63e0b10d
KS
91 if (module_exists('views')) {
92 $views = array('--' => '--');
93 $all_views = views_get_all_views();
94 foreach ($all_views as $view) {
e1655be5
YC
95 // Only 'node' views that have fields will work for our purpose.
96 if ($view->base_table == 'node' && !empty($view->display['default']->display_options['fields'])) {
97 if ($view->type == 'Default') {
98 $views[t('Default Views')][$view->name] = $view->name;
99 }
100 else {
101 $views[t('Existing Views')][$view->name] = $view->name;
102 }
a2ec9362 103 }
63e0b10d
KS
104 }
105
0e968023
YC
106 $form['advanced'] = array(
107 '#type' => 'fieldset',
108 '#title' => t('Advanced - Nodes that can be referenced (View)'),
109 '#collapsible' => TRUE,
110 '#collapsed' => !isset($field['advanced_view']) || $field['advanced_view'] == '--',
111 );
63e0b10d 112 if (count($views) > 1) {
63e0b10d
KS
113 $form['advanced']['advanced_view'] = array(
114 '#type' => 'select',
3a2d35f5 115 '#title' => t('View used to select the nodes'),
63e0b10d
KS
116 '#options' => $views,
117 '#default_value' => isset($field['advanced_view']) ? $field['advanced_view'] : '--',
0e968023
YC
118 '#description' => t('<p>Choose the "Views module" view that selects the nodes that can be referenced.<br />Note:</p>') .
119 t('<ul><li>Only views that have fields will work for this purpose.</li><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>'),
63e0b10d
KS
120 );
121 $form['advanced']['advanced_view_args'] = array(
122 '#type' => 'textfield',
123 '#title' => t('View arguments'),
124 '#default_value' => isset($field['advanced_view_args']) ? $field['advanced_view_args'] : '',
125 '#required' => FALSE,
126 '#description' => t('Provide a comma separated list of arguments to pass to the view.'),
127 );
128 }
0e968023
YC
129 else {
130 $form['advanced']['no_view_help'] = array(
131 '#value' => t('<p>The list of nodes that can be referenced can be based on a "Views module" view but no appropriate views were found. <br />Note:</p>') .
132 t('<ul><li>Only views that have fields will work for this purpose.</li><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>'),
133 );
134 }
63e0b10d 135 }
e1a2f8a6
JC
136 return $form;
137
e1a2f8a6 138 case 'save':
0c2959db 139 $settings = array('referenceable_types');
63e0b10d
KS
140 if (module_exists('views')) {
141 $settings[] = 'advanced_view';
142 $settings[] = 'advanced_view_args';
143 }
0c2959db 144 return $settings;
2d3f00e8 145
bbbd884e
JC
146 case 'database columns':
147 $columns = array(
972214b8 148 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'index' => TRUE),
734116ed
YC
149 );
150 return $columns;
151
b91d7f2b
YC
152 case 'views data':
153 $data = content_views_field_views_data($field);
154 $db_info = content_database_info($field);
f33cc12c
KS
155 $table_alias = content_views_tablename($field);
156
47f94b0d 157 // Filter: swap the handler to the 'in' operator.
bea5ae87 158 $data[$table_alias][$field['field_name'] .'_nid']['filter']['handler'] = 'content_handler_filter_many_to_one';
47f94b0d 159 // Argument: use node.title for summaries.
017108ce
YC
160 $data["node_$table_alias"]['table']['join']['node'] = array(
161 'table' => 'node',
162 'field' => 'nid',
163 'left_table' => $table_alias,
164 'left_field' => $field['field_name'] .'_nid',
165 );
2a63f940 166 $data[$table_alias][$field['field_name'] .'_nid']['argument']['handler'] = 'content_handler_argument_reference';
017108ce 167 $data[$table_alias][$field['field_name'] .'_nid']['argument']['name table'] = "node_$table_alias";
0b20f95b 168 $data[$table_alias][$field['field_name'] .'_nid']['argument']['name field'] = 'title';
47f94b0d 169 // Relationship: add a relationship for related node.
f33cc12c 170 $data[$table_alias][$field['field_name'] .'_nid']['relationship'] = array(
330807d0 171 'base' => 'node',
b91d7f2b 172 'field' => $db_info['columns']['nid']['column'],
0bb3edd5 173 'handler' => 'content_handler_relationship',
f458bc00 174 'label' => t($field['widget']['label']),
b6ca681f 175 'content_field_name' => $field['field_name'],
b91d7f2b
YC
176 );
177 return $data;
e1a2f8a6
JC
178 }
179}
180
181/**
182 * Implementation of hook_field().
183 */
1fbe637d 184function nodereference_field($op, &$node, $field, &$items, $teaser, $page) {
d6d7c227
KS
185 static $sanitized_nodes = array();
186
1fbe637d 187 switch ($op) {
c9eaf362
YC
188 // When preparing a translation, load any translations of existing references.
189 case 'prepare translation':
190 $addition = array();
191 $addition[$field['field_name']] = array();
192 if (isset($node->translation_source->$field['field_name']) && is_array($node->translation_source->$field['field_name'])) {
193 foreach ($node->translation_source->$field['field_name'] as $key => $reference) {
194 $reference_node = node_load($reference['nid']);
195 // Test if the referenced node type is translatable and, if so,
196 // load translations if the reference is not for the current language.
197 // We can assume the translation module is present because it invokes 'prepare translation'.
198 if (translation_supported_type($reference_node->type) && !empty($reference_node->language) && $reference_node->language != $node->language && $translations = translation_node_get_translations($reference_node->tnid)) {
199 // If there is a translation for the current language, use it.
200 $addition[$field['field_name']][] = array(
201 'nid' => isset($translations[$node->language]) ? $translations[$node->language]->nid : $reference['nid'],
202 );
203 }
204 }
205 }
206 return $addition;
14ddf71e 207
1fbe637d 208 case 'validate':
14ddf71e
YC
209 // Extract nids to check.
210 $ids = array();
211 foreach ($items as $delta => $item) {
91a7f662
YC
212 if (is_array($item) && !empty($item['nid'])) {
213 if (is_numeric($item['nid'])) {
214 $ids[] = $item['nid'];
215 }
216 else {
217 $error_element = isset($item['_error_element']) ? $item['_error_element'] : '';
218 if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']);
219 form_set_error($error_element, t("%name: invalid input.", array('%name' => t($field['widget']['label']))));
220 }
14ddf71e
YC
221 }
222 }
91a7f662
YC
223 // Prevent performance hog if there are no ids to check.
224 if ($ids) {
225 $refs = _nodereference_potential_references($field, '', NULL, $ids);
226 foreach ($items as $delta => $item) {
227 if (is_array($item)) {
228 $error_element = isset($item['_error_element']) ? $item['_error_element'] : '';
229 if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']);
230 if (!empty($item['nid']) && !isset($refs[$item['nid']])) {
231 form_set_error($error_element, t("%name: this post can't be referenced.", array('%name' => t($field['widget']['label']))));
232 }
40423ad9 233 }
1fbe637d
KS
234 }
235 }
9b5083a1 236 return $items;
d6d7c227
KS
237
238 case 'sanitize':
239 // We can't just check the node is 'referenceable', because Views-mode
240 // could rely on 'current user' (at edit time).
241
242 // Extract nids to check.
243 $ids = array();
244 foreach ($items as $delta => $item) {
245 if (is_array($item)) {
246 // Default to 'non accessible'.
247 $items[$delta]['safe'] = array();
248 if (!empty($item['nid']) && is_numeric($item['nid'])) {
249 $ids[] = $item['nid'];
250 }
251 }
252 }
253 if ($ids) {
254 // Load information about nids that we haven't already loaded during
255 // this page request.
256 $missing_ids = array_diff($ids, array_keys($sanitized_nodes));
257 if (!empty($missing_ids)) {
258 $where = array('n.nid in ('. db_placeholders($missing_ids) . ')');
259 if (!user_access('administer nodes')) {
260 $where[] = 'n.status = 1';
261 }
262 $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.status FROM {node} n WHERE '. implode(' AND ', $where)), $missing_ids);
263 while ($row = db_fetch_array($result)) {
264 $sanitized_nodes[$row['nid']] = $row;
265 }
266 }
267 foreach ($items as $delta => $item) {
268 if (is_array($item) && !empty($item['nid']) && isset($sanitized_nodes[$item['nid']])) {
269 $items[$delta]['safe'] = $sanitized_nodes[$item['nid']];
270 }
271 }
272 }
273 return $items;
6130c745
KS
274 }
275}
e9b6b501 276
6130c745
KS
277/**
278 * Implementation of hook_content_is_empty().
279 */
92041ef8 280function nodereference_content_is_empty($item, $field) {
6130c745
KS
281 if (empty($item['nid'])) {
282 return TRUE;
1fbe637d 283 }
6130c745 284 return FALSE;
1fbe637d 285}
1182d802 286
319350d8 287/**
53d5a65c 288 * Implementation of hook_field_formatter_info().
319350d8 289 */
53d5a65c
KS
290function nodereference_field_formatter_info() {
291 return array(
292 'default' => array(
d4353143 293 'label' => t('Title (link)'),
53d5a65c 294 'field types' => array('nodereference'),
089838e7 295 'multiple values' => CONTENT_HANDLE_CORE,
53d5a65c
KS
296 ),
297 'plain' => array(
d4353143 298 'label' => t('Title (no link)'),
53d5a65c 299 'field types' => array('nodereference'),
089838e7 300 'multiple values' => CONTENT_HANDLE_CORE,
53d5a65c 301 ),
c519cdf1 302 'full' => array(
d4353143 303 'label' => t('Full node'),
c519cdf1 304 'field types' => array('nodereference'),
089838e7 305 'multiple values' => CONTENT_HANDLE_CORE,
c519cdf1
YC
306 ),
307 'teaser' => array(
d4353143 308 'label' => t('Teaser'),
c519cdf1 309 'field types' => array('nodereference'),
089838e7 310 'multiple values' => CONTENT_HANDLE_CORE,
c519cdf1 311 ),
53d5a65c
KS
312 );
313}
314
315/**
e74ada9a 316 * Theme function for 'default' nodereference field formatter.
53d5a65c 317 */
e74ada9a
YC
318function theme_nodereference_formatter_default($element) {
319 $output = '';
d6d7c227
KS
320 if (!empty($element['#item']['safe']['nid'])) {
321 $output = l($element['#item']['safe']['title'], 'node/'. $element['#item']['safe']['nid']);
322 if (!$element['#item']['safe']['status']) {
323 $output = '<span class="node-unpublished"> '. t('(Unpublished)') ." $output</span>";
324 }
325 }
e74ada9a
YC
326 return $output;
327}
62823fb9 328
e74ada9a
YC
329/**
330 * Theme function for 'plain' nodereference field formatter.
331 */
332function theme_nodereference_formatter_plain($element) {
333 $output = '';
d6d7c227
KS
334 if (!empty($element['#item']['safe']['nid'])) {
335 $output = check_plain($element['#item']['safe']['title']);
336 if (!$element['#item']['safe']['status']) {
337 $output = '<span class="node-unpublished"> '. t('(Unpublished)') ." $output</span>";
338 }
e411c26d 339 }
e74ada9a
YC
340 return $output;
341}
53d5a65c 342
e74ada9a
YC
343/**
344 * Proxy theme function for 'full' and 'teaser' nodereference field formatters.
345 */
346function theme_nodereference_formatter_full_teaser($element) {
347 static $recursion_queue = array();
348 $output = '';
d6d7c227
KS
349 if (!empty($element['#item']['safe']['nid'])) {
350 $nid = $element['#item']['safe']['nid'];
f4004b24
YC
351 $node = $element['#node'];
352 $field = content_fields($element['#field_name'], $element['#type_name']);
62823fb9
YC
353 // If no 'referencing node' is set, we are starting a new 'reference thread'
354 if (!isset($node->referencing_node)) {
355 $recursion_queue = array();
356 }
357 $recursion_queue[] = $node->nid;
d6d7c227 358 if (in_array($nid, $recursion_queue)) {
3a2d35f5 359 // Prevent infinite recursion caused by reference cycles:
62823fb9
YC
360 // if the node has already been rendered earlier in this 'thread',
361 // we fall back to 'default' (node title) formatter.
e74ada9a 362 return theme('nodereference_formatter_default', $element);
62823fb9 363 }
d6d7c227
KS
364 if ($referenced_node = node_load($nid)) {
365 $referenced_node->referencing_node = $node;
366 $referenced_node->referencing_field = $field;
367 $output = node_view($referenced_node, $element['#formatter'] == 'teaser');
1d2ddfd3 368 }
c6396b9d 369 }
e74ada9a
YC
370 return $output;
371}
c519cdf1 372
e74ada9a
YC
373/**
374 * Helper function for formatters.
375 *
376 * Store node titles collected in the curent request.
377 */
378function _nodereference_titles($nid, $known_title = NULL) {
379 static $titles = array();
380 if (!isset($titles[$nid])) {
3cba966e 381 $title = $known_title ? $known_title : db_result(db_query(db_rewrite_sql("SELECT n.title FROM {node} n WHERE n.nid=%d"), $nid));
e74ada9a 382 $titles[$nid] = $title ? $title : '';
1182d802 383 }
e74ada9a 384 return $titles[$nid];
1182d802
JC
385}
386
1182d802
JC
387/**
388 * Implementation of hook_widget_info().
e9b6b501
KS
389 *
390 * We need custom handling of multiple values for the nodereference_select
391 * widget because we need to combine them into a options list rather
392 * than display multiple elements.
393 *
394 * We will use the content module's default handling for default value.
395 *
396 * Callbacks can be omitted if default handing is used.
397 * They're included here just so this module can be used
398 * as an example for custom modules that might do things
399 * differently.
1182d802
JC
400 */
401function nodereference_widget_info() {
402 return array(
403 'nodereference_select' => array(
2400f882 404 'label' => t('Select list'),
1182d802 405 'field types' => array('nodereference'),
a4418efc 406 'multiple values' => CONTENT_HANDLE_MODULE,
e9b6b501
KS
407 'callbacks' => array(
408 'default value' => CONTENT_CALLBACK_DEFAULT,
d4353143 409 ),
1182d802 410 ),
578c7b49
YC
411 'nodereference_buttons' => array(
412 'label' => t('Check boxes/radio buttons'),
413 'field types' => array('nodereference'),
414 'multiple values' => CONTENT_HANDLE_MODULE,
415 'callbacks' => array(
416 'default value' => CONTENT_CALLBACK_DEFAULT,
417 ),
418 ),
1182d802 419 'nodereference_autocomplete' => array(
2400f882 420 'label' => t('Autocomplete text field'),
1182d802 421 'field types' => array('nodereference'),
a4418efc 422 'multiple values' => CONTENT_HANDLE_CORE,
e9b6b501
KS
423 'callbacks' => array(
424 'default value' => CONTENT_CALLBACK_DEFAULT,
d4353143 425 ),
1182d802
JC
426 ),
427 );
428}
429
430/**
ea73d362
KS
431 * Implementation of FAPI hook_elements().
432 *
433 * Any FAPI callbacks needed for individual widgets can be declared here,
434 * and the element will be passed to those callbacks for processing.
435 *
436 * Drupal will automatically theme the element using a theme with
437 * the same name as the hook_elements key.
438 *
439 * Autocomplete_path is not used by text_widget but other widgets can use it
440 * (see nodereference and userreference).
441 */
442function nodereference_elements() {
443 return array(
444 'nodereference_select' => array(
445 '#input' => TRUE,
446 '#columns' => array('uid'), '#delta' => 0,
447 '#process' => array('nodereference_select_process'),
4ae62d83 448 ),
578c7b49
YC
449 'nodereference_buttons' => array(
450 '#input' => TRUE,
451 '#columns' => array('uid'), '#delta' => 0,
452 '#process' => array('nodereference_buttons_process'),
4ae62d83 453 ),
ea73d362
KS
454 'nodereference_autocomplete' => array(
455 '#input' => TRUE,
456 '#columns' => array('name'), '#delta' => 0,
457 '#process' => array('nodereference_autocomplete_process'),
458 '#autocomplete_path' => FALSE,
459 ),
460 );
461}
462
463/**
14ddf71e
YC
464 * Implementation of hook_widget_settings().
465 */
466function nodereference_widget_settings($op, $widget) {
467 switch ($op) {
468 case 'form':
469 $form = array();
470 $match = isset($widget['autocomplete_match']) ? $widget['autocomplete_match'] : 'contains';
0a6df163 471 $size = (isset($widget['size']) && is_numeric($widget['size'])) ? $widget['size'] : 60;
14ddf71e
YC
472 if ($widget['type'] == 'nodereference_autocomplete') {
473 $form['autocomplete_match'] = array(
474 '#type' => 'select',
475 '#title' => t('Autocomplete matching'),
476 '#default_value' => $match,
477 '#options' => array(
478 'starts_with' => t('Starts with'),
479 'contains' => t('Contains'),
480 ),
481 '#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.'),
482 );
0a6df163
YC
483 $form['size'] = array(
484 '#type' => 'textfield',
485 '#title' => t('Size of textfield'),
486 '#default_value' => $size,
487 '#element_validate' => array('_element_validate_integer_positive'),
488 '#required' => TRUE,
489 );
14ddf71e
YC
490 }
491 else {
492 $form['autocomplete_match'] = array('#type' => 'hidden', '#value' => $match);
0a6df163 493 $form['size'] = array('#type' => 'hidden', '#value' => $size);
14ddf71e
YC
494 }
495 return $form;
496
497 case 'save':
0a6df163 498 return array('autocomplete_match', 'size');
14ddf71e
YC
499 }
500}
501
502/**
1182d802 503 * Implementation of hook_widget().
e9b6b501
KS
504 *
505 * Attach a single form element to the form. It will be built out and
506 * validated in the callback(s) listed in hook_elements. We build it
507 * out in the callbacks rather than here in hook_widget so it can be
508 * plugged into any module that can provide it with valid
509 * $field information.
510 *
511 * Content module will set the weight, field name and delta values
512 * for each form element. This is a change from earlier CCK versions
513 * where the widget managed its own multiple values.
514 *
515 * If there are multiple values for this field, the content module will
516 * call this function as many times as needed.
517 *
518 * @param $form
519 * the entire form array, $form['#node'] holds node information
520 * @param $form_state
521 * the form_state, $form_state['values'][$field['field_name']]
522 * holds the field's form values.
523 * @param $field
524 * the field array
525 * @param $items
526 * array of default values for this field
527 * @param $delta
528 * the order of this item in the array of subelements (0, 1, 2, etc)
529 *
530 * @return
531 * the form item for a single element for this field
1182d802 532 */
e9b6b501 533function nodereference_widget(&$form, &$form_state, $field, $items, $delta = 0) {
e9b6b501
KS
534 switch ($field['widget']['type']) {
535 case 'nodereference_select':
e9b6b501 536 $element = array(
ea73d362 537 '#type' => 'nodereference_select',
e9b6b501
KS
538 '#default_value' => $items,
539 );
540 break;
541
578c7b49
YC
542 case 'nodereference_buttons':
543 $element = array(
544 '#type' => 'nodereference_buttons',
545 '#default_value' => $items,
546 );
547 break;
548
e9b6b501 549 case 'nodereference_autocomplete':
e9b6b501 550 $element = array(
ea73d362 551 '#type' => 'nodereference_autocomplete',
e9b6b501
KS
552 '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL,
553 '#value_callback' => 'nodereference_autocomplete_value',
e9b6b501
KS
554 );
555 break;
556 }
557 return $element;
558}
aa5d88fc 559
e9b6b501
KS
560/**
561 * Value for a nodereference autocomplete element.
562 *
563 * Substitute in the node title for the node nid.
564 */
565function nodereference_autocomplete_value($element, $edit = FALSE) {
77684855
KS
566 $field_key = $element['#columns'][0];
567 if (!empty($element['#default_value'][$field_key])) {
568 $nid = $element['#default_value'][$field_key];
569 $value = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $nid));
570 $value .= ' [nid:'. $nid .']';
571 return array($field_key => $value);
ea73d362 572 }
77684855 573 return array($field_key => NULL);
ea73d362
KS
574}
575
576/**
577 * Process an individual element.
578 *
579 * Build the form element. When creating a form using FAPI #process,
580 * note that $element['#value'] is already set.
581 *
32ead958 582 * The $fields array is in $form['#field_info'][$element['#field_name']].
ea73d362
KS
583 */
584function nodereference_select_process($element, $edit, $form_state, $form) {
585 // The nodereference_select widget doesn't need to create its own
586 // element, it can wrap around the optionwidgets_select element.
617d3264 587 // This will create a new, nested instance of the field.
ea73d362
KS
588 // Add a validation step where the value can be unwrapped.
589 $field_key = $element['#columns'][0];
590 $element[$field_key] = array(
591 '#type' => 'optionwidgets_select',
443b2f04 592 '#default_value' => isset($element['#value']) ? $element['#value'] : '',
8c06d539
KS
593 // The following values were set by the content module and need
594 // to be passed down to the nested element.
8c06d539 595 '#title' => $element['#title'],
341f4230 596 '#required' => $element['#required'],
7dbb8603 597 '#description' => $element['#description'],
07c92176
YC
598 '#field_name' => $element['#field_name'],
599 '#type_name' => $element['#type_name'],
600 '#delta' => $element['#delta'],
601 '#columns' => $element['#columns'],
ea73d362 602 );
617d3264
KS
603 if (empty($element[$field_key]['#element_validate'])) {
604 $element[$field_key]['#element_validate'] = array();
605 }
606 array_unshift($element[$field_key]['#element_validate'], 'nodereference_optionwidgets_validate');
ea73d362
KS
607 return $element;
608}
609
610/**
611 * Process an individual element.
612 *
613 * Build the form element. When creating a form using FAPI #process,
614 * note that $element['#value'] is already set.
615 *
578c7b49
YC
616 * The $fields array is in $form['#field_info'][$element['#field_name']].
617 */
618function nodereference_buttons_process($element, $edit, $form_state, $form) {
619 // The nodereference_select widget doesn't need to create its own
620 // element, it can wrap around the optionwidgets_select element.
617d3264 621 // This will create a new, nested instance of the field.
578c7b49
YC
622 // Add a validation step where the value can be unwrapped.
623 $field_key = $element['#columns'][0];
624 $element[$field_key] = array(
625 '#type' => 'optionwidgets_buttons',
626 '#default_value' => isset($element['#value']) ? $element['#value'] : '',
578c7b49
YC
627 // The following values were set by the content module and need
628 // to be passed down to the nested element.
629 '#title' => $element['#title'],
630 '#required' => $element['#required'],
631 '#description' => $element['#description'],
632 '#field_name' => $element['#field_name'],
633 '#type_name' => $element['#type_name'],
634 '#delta' => $element['#delta'],
635 '#columns' => $element['#columns'],
636 );
617d3264
KS
637 if (empty($element[$field_key]['#element_validate'])) {
638 $element[$field_key]['#element_validate'] = array();
639 }
640 array_unshift($element[$field_key]['#element_validate'], 'nodereference_optionwidgets_validate');
578c7b49
YC
641 return $element;
642}
643
644/**
645 * Process an individual element.
646 *
647 * Build the form element. When creating a form using FAPI #process,
648 * note that $element['#value'] is already set.
649 *
ea73d362
KS
650 */
651function nodereference_autocomplete_process($element, $edit, $form_state, $form) {
7e578d66 652
ea73d362
KS
653 // The nodereference autocomplete widget doesn't need to create its own
654 // element, it can wrap around the text_textfield element and add an autocomplete
655 // path and some extra processing to it.
656 // Add a validation step where the value can be unwrapped.
657 $field_key = $element['#columns'][0];
ea73d362 658
ea73d362
KS
659 $element[$field_key] = array(
660 '#type' => 'text_textfield',
92efa608 661 '#default_value' => isset($element['#value']) ? $element['#value'] : '',
411876a9 662 '#autocomplete_path' => 'nodereference/autocomplete/'. $element['#field_name'],
8c06d539
KS
663 // The following values were set by the content module and need
664 // to be passed down to the nested element.
8c06d539 665 '#title' => $element['#title'],
341f4230 666 '#required' => $element['#required'],
7dbb8603 667 '#description' => $element['#description'],
07c92176
YC
668 '#field_name' => $element['#field_name'],
669 '#type_name' => $element['#type_name'],
670 '#delta' => $element['#delta'],
671 '#columns' => $element['#columns'],
ea73d362 672 );
617d3264
KS
673 if (empty($element[$field_key]['#element_validate'])) {
674 $element[$field_key]['#element_validate'] = array();
675 }
676 array_unshift($element[$field_key]['#element_validate'], 'nodereference_autocomplete_validate');
cabf5d2a 677
452a390c
KS
678 // Used so that hook_field('validate') knows where to flag an error.
679 $element['_error_element'] = array(
680 '#type' => 'value',
d1eac231 681 // Wrapping the element around a text_textfield element creates a
617d3264 682 // nested element, so the final id will look like 'field-name-0-nid-nid'.
452a390c
KS
683 '#value' => implode('][', array_merge($element['#parents'], array($field_key, $field_key))),
684 );
ea73d362 685 return $element;
e9b6b501 686}
bbbd884e 687
ea73d362 688/**
4ae62d83 689 * Validate a select/buttons element.
ea73d362
KS
690 *
691 * Remove the wrapper layer and set the right element's value.
d30e72da
KS
692 * We don't know exactly where this element is, so we drill down
693 * through the element until we get to our key.
d1eac231 694 *
617d3264
KS
695 * We use $form_state['values'] instead of $element['#value']
696 * to be sure we have the most accurate value when other modules
697 * like optionwidgets are using #element_validate to alter the value.
ea73d362 698 */
ecf1a4f0 699function nodereference_optionwidgets_validate($element, &$form_state) {
8cc29b62 700 $field_key = $element['#columns'][0];
d1eac231 701
d30e72da 702 $value = $form_state['values'];
617d3264 703 $new_parents = array();
d30e72da
KS
704 foreach ($element['#parents'] as $parent) {
705 $value = $value[$parent];
a6846447
KS
706 // Use === to be sure we get right results if parent is a zero (delta) value.
707 if ($parent === $field_key) {
d30e72da 708 $element['#parents'] = $new_parents;
3a2d35f5 709 form_set_value($element, $value, $form_state);
d30e72da
KS
710 break;
711 }
712 $new_parents[] = $parent;
713 }
ea73d362
KS
714}
715
716/**
717 * Validate an autocomplete element.
718 *
719 * Remove the wrapper layer and set the right element's value.
d1eac231 720 * This will move the nested value at 'field-name-0-nid-nid'
617d3264 721 * back to its original location, 'field-name-0-nid'.
ea73d362
KS
722 */
723function nodereference_autocomplete_validate($element, &$form_state) {
5ce201bf 724 $field_name = $element['#field_name'];
07c92176
YC
725 $type_name = $element['#type_name'];
726 $field = content_fields($field_name, $type_name);
e9b6b501 727 $field_key = $element['#columns'][0];
8588bda8 728 $delta = $element['#delta'];
e9b6b501 729 $value = $element['#value'][$field_key];
b903d4d0 730 $nid = NULL;
e9b6b501
KS
731 if (!empty($value)) {
732 preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $value, $matches);
733 if (!empty($matches)) {
7e578d66 734 // Explicit [nid:n].
e9b6b501 735 list(, $title, $nid) = $matches;
ab5599ef 736 if (!empty($title) && ($n = node_load($nid)) && trim($title) != trim($n->title)) {
d6166089 737 form_error($element[$field_key], t('%name: title mismatch. Please check your selection.', array('%name' => t($field['widget']['label']))));
e9b6b501
KS
738 }
739 }
740 else {
7e578d66 741 // No explicit nid.
14ddf71e
YC
742 $reference = _nodereference_potential_references($field, $value, 'equals', NULL, 1);
743 if (empty($reference)) {
d6166089 744 form_error($element[$field_key], t('%name: found no valid post with that title.', array('%name' => t($field['widget']['label']))));
58330219
YC
745 }
746 else {
747 // TODO:
748 // the best thing would be to present the user with an additional form,
749 // allowing the user to choose between valid candidates with the same title
750 // ATM, we pick the first matching candidate...
14ddf71e 751 $nid = key($reference);
58330219 752 }
e9b6b501 753 }
e1a2f8a6 754 }
b903d4d0 755 form_set_value($element, $nid, $form_state);
e9b6b501
KS
756}
757
758/**
759 * Implementation of hook_allowed_values().
760 */
761function nodereference_allowed_values($field) {
7e578d66
YC
762 $references = _nodereference_potential_references($field);
763
764 $options = array();
765 foreach ($references as $key => $value) {
948f3ff9 766 $options[$key] = $value['rendered'];
e9b6b501 767 }
d0c6d4fe 768
e9b6b501 769 return $options;
e1a2f8a6
JC
770}
771
772/**
7e578d66
YC
773 * Fetch an array of all candidate referenced nodes.
774 *
8ab268df 775 * This info is used in various places (allowed values, autocomplete results,
7e578d66
YC
776 * input validation...). Some of them only need the nids, others nid + titles,
777 * others yet nid + titles + rendered row (for display in widgets).
778 * The array we return contains all the potentially needed information, and lets
779 * consumers use the parts they actually need.
780 *
781 * @param $field
782 * The field description.
783 * @param $string
14ddf71e
YC
784 * Optional string to filter titles on (used by autocomplete).
785 * @param $match
786 * Operator to match filtered name against, can be any of:
787 * 'contains', 'equals', 'starts_with'
788 * @param $ids
789 * Optional node ids to lookup (the $string and $match arguments will be
790 * ignored).
791 * @param $limit
792 * If non-zero, limit the size of the result set.
7e578d66
YC
793 *
794 * @return
795 * An array of valid nodes in the form:
796 * array(
797 * nid => array(
798 * 'title' => The node title,
799 * 'rendered' => The text to display in widgets (can be HTML)
800 * ),
801 * ...
802 * )
e1a2f8a6 803 */
14ddf71e 804function _nodereference_potential_references($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) {
7e578d66 805 static $results = array();
e1a2f8a6 806
14ddf71e
YC
807 // Create unique id for static cache.
808 $cid = $field['field_name'] .':'. $match .':'. ($string !== '' ? $string : implode('-', $ids)) .':'. $limit;
809 if (!isset($results[$cid])) {
7e578d66
YC
810 $references = FALSE;
811 if (module_exists('views') && !empty($field['advanced_view']) && $field['advanced_view'] != '--') {
14ddf71e 812 $references = _nodereference_potential_references_views($field, $string, $match, $ids, $limit);
0c2959db 813 }
7e578d66 814 // If the view doesn't exist, we got FALSE, and fallback to the regular 'standard mode'.
0c2959db 815
7e578d66 816 if ($references === FALSE) {
14ddf71e 817 $references = _nodereference_potential_references_standard($field, $string, $match, $ids, $limit);
0c2959db
KS
818 }
819
7e578d66 820 // Store the results.
0311a4d5 821 $results[$cid] = !empty($references) ? $references : array();
63e0b10d 822 }
e1a2f8a6 823
14ddf71e 824 return $results[$cid];
e1a2f8a6 825}
0f6ca9bb 826
0f6ca9bb 827/**
7e578d66
YC
828 * Helper function for _nodereference_potential_references():
829 * case of Views-defined referenceable nodes.
328c1ca6 830 */
14ddf71e 831function _nodereference_potential_references_views($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) {
328c1ca6 832 $view_name = $field['advanced_view'];
7e578d66
YC
833
834 if ($view = views_get_view($view_name)) {
c27126a5
YC
835 // We add a display, and let it derive from the 'default' display.
836 // TODO: We should let the user pick a display in the fields settings - sort of requires AHAH...
14ddf71e 837 $display = $view->add_display('content_references');
7e578d66
YC
838 $view->set_display($display);
839
7e578d66
YC
840 // TODO from merlinofchaos on IRC : arguments using summary view can defeat the style setting.
841 // We might also need to check if there's an argument, and set *its* style_plugin as well.
d49aafb0 842 $view->display_handler->set_option('style_plugin', 'content_php_array_autocomplete');
7e578d66 843 $view->display_handler->set_option('row_plugin', 'fields');
d49aafb0
YC
844 // Used in content_plugin_style_php_array::render(), to get
845 // the 'field' to be used as title.
846 $view->display_handler->set_option('content_title_field', 'title');
3a2d35f5 847
14ddf71e
YC
848 // Additional options to let content_plugin_display_references::query()
849 // narrow the results.
850 $options = array(
851 'table' => 'node',
852 'field_string' => 'title',
853 'string' => $string,
854 'match' => $match,
855 'field_id' => 'nid',
856 'ids' => $ids,
857 );
858 $view->display_handler->set_option('content_options', $options);
859
860 // TODO : for consistency, a fair amount of what's below
861 // should be moved to content_plugin_display_references
862
863 // Limit result set size.
7bb93942
YC
864 $limit = isset($limit) ? $limit : 0;
865 $view->display_handler->set_option('items_per_page', $limit);
14ddf71e 866
5cb7f057 867 // Get arguments for the view.
e1655be5 868 if (!empty($field['advanced_view_args'])) {
328c1ca6 869 // TODO: Support Tokens using token.module ?
328c1ca6 870 $view_args = array_map('trim', explode(',', $field['advanced_view_args']));
5cb7f057
KS
871 }
872 else {
873 $view_args = array();
328c1ca6
KS
874 }
875
7e578d66 876 // We do need title field, so add it if not present (unlikely, but...)
c27126a5
YC
877 $fields = $view->get_items('field', $display);
878 if (!isset($fields['title'])) {
5cb7f057
KS
879 $view->add_item($display, 'field', 'node', 'title');
880 }
328c1ca6 881
c27126a5
YC
882 // If not set, make all fields inline and define a separator.
883 $options = $view->display_handler->get_option('row_options');
884 if (empty($options['inline'])) {
885 $options['inline'] = drupal_map_assoc(array_keys($view->get_items('field', $display)));
886 }
887 if (empty($options['separator'])) {
888 $options['separator'] = '-';
889 }
890 $view->display_handler->set_option('row_options', $options);
7e578d66 891
328c1ca6
KS
892 // Make sure the query is not cached
893 $view->is_cacheable = FALSE;
7e578d66
YC
894
895 // Get the results.
896 $result = $view->execute_display($display, $view_args);
897 }
898 else {
899 $result = FALSE;
328c1ca6 900 }
3a2d35f5 901
7e578d66 902 return $result;
328c1ca6
KS
903}
904
905/**
7e578d66
YC
906 * Helper function for _nodereference_potential_references():
907 * referenceable nodes defined by content types.
0f6ca9bb 908 */
14ddf71e 909function _nodereference_potential_references_standard($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) {
7e578d66 910 $related_types = array();
14ddf71e 911 $where = array();
7e578d66
YC
912 $args = array();
913
914 if (is_array($field['referenceable_types'])) {
915 foreach (array_filter($field['referenceable_types']) as $related_type) {
14ddf71e 916 $related_types[] = "n.type = '%s'";
7e578d66
YC
917 $args[] = $related_type;
918 }
0f6ca9bb 919 }
53d5a65c 920
14ddf71e 921 $where[] = implode(' OR ', $related_types);
7e578d66
YC
922
923 if (!count($related_types)) {
924 return array();
63e0b10d 925 }
7e578d66 926
069484bb 927 if ($string !== '') {
121f60a6
KS
928 $like = $GLOBALS["db_type"] == 'pgsql' ? "ILIKE" : "LIKE";
929 $match_clauses = array(
930 'contains' => "$like '%%%s%%'",
14ddf71e 931 'equals' => "= '%s'",
121f60a6 932 'starts_with' => "$like '%s%%'",
14ddf71e 933 );
6dd9b4e4 934 $where[] = 'n.title '. (isset($match_clauses[$match]) ? $match_clauses[$match] : $match_clauses['contains']);
7e578d66 935 $args[] = $string;
63e0b10d 936 }
14ddf71e
YC
937 elseif ($ids) {
938 $where[] = 'n.nid IN (' . db_placeholders($ids) . ')';
939 $args = array_merge($args, $ids);
940 }
0c2959db 941
14ddf71e
YC
942 $where_clause = $where ? 'WHERE ('. implode(') AND (', $where) .')' : '';
943 $sql = db_rewrite_sql("SELECT n.nid, n.title AS node_title, n.type AS node_type FROM {node} n $where_clause ORDER BY n.title, n.type");
944 $result = $limit ? db_query_range($sql, $args, 0, $limit) : db_query($sql, $args);
7e578d66
YC
945 $references = array();
946 while ($node = db_fetch_object($result)) {
947 $references[$node->nid] = array(
948 'title' => $node->node_title,
948f3ff9 949 'rendered' => check_plain($node->node_title),
7e578d66 950 );
0c2959db 951 }
7e578d66
YC
952
953 return $references;
0c2959db
KS
954}
955
d6d7c227
KS
956 /**
957 * Check access to the menu callback of the autocomplete widget.
958 *
959 * Check for both 'edit' and 'view' access in the unlikely event
960 * a user has edit but not view access.
961 */
962function nodereference_autocomplete_access($field_name) {
898a7b60 963 return user_access('access content') && ($field = content_fields($field_name)) && isset($field['field_name']) && content_access('view', $field) && content_access('edit', $field);
d6d7c227
KS
964}
965
7e578d66 966/**
4ae62d83 967 * Menu callback; Retrieve a pipe delimited string of autocomplete suggestions for existing users
7e578d66
YC
968 */
969function nodereference_autocomplete($field_name, $string = '') {
970 $fields = content_fields();
971 $field = $fields[$field_name];
14ddf71e 972 $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains';
7e578d66
YC
973 $matches = array();
974
14ddf71e 975 $references = _nodereference_potential_references($field, $string, $match, array(), 10);
7e578d66
YC
976 foreach ($references as $id => $row) {
977 // Add a class wrapper for a few required CSS overrides.
2a1f05cb 978 $matches[$row['title'] ." [nid:$id]"] = '<div class="reference-autocomplete">'. $row['rendered'] . '</div>';
7e578d66
YC
979 }
980 drupal_json($matches);
0c2959db
KS
981}
982
53d5a65c 983/**
4ae62d83
YC
984 * Implementation of hook_node_types.
985 */
986function nodereference_node_type($op, $info) {
987 switch ($op) {
988 case 'update':
989 // Reflect type name changes to the 'referenceable types' settings.
990 if (!empty($info->old_type) && $info->old_type != $info->type) {
991 // content.module's implementaion of hook_node_type() has already
992 // refreshed _content_type_info().
993 $fields = content_fields();
ffb0541f 994 $rebuild = FALSE;
4ae62d83
YC
995 foreach ($fields as $field_name => $field) {
996 if ($field['type'] == 'nodereference' && isset($field['referenceable_types'][$info->old_type])) {
997 $field['referenceable_types'][$info->type] = empty($field['referenceable_types'][$info->old_type]) ? 0 : $info->type;
998 unset($field['referenceable_types'][$info->old_type]);
ffb0541f
MFM
999 content_field_instance_update($field, FALSE);
1000 $rebuild = TRUE;
4ae62d83
YC
1001 }
1002 }
ffb0541f
MFM
1003
1004 // Clear caches and rebuild menu only if any field has been updated.
1005 if ($rebuild) {
1006 content_clear_type_cache(TRUE);
1007 menu_rebuild();
1008 }
4ae62d83
YC
1009 }
1010 break;
1011 }
1012}
1013
47c53f3b
YC
1014/**
1015 * Theme preprocess function.
1016 *
1017 * Allows specific node templates for nodes displayed as values of a
1018 * nodereference field with the 'full node' / 'teaser' formatters.
1019 */
1020function nodereference_preprocess_node(&$vars) {
1021 // The 'referencing_field' attribute of the node is added by the 'teaser'
1022 // and 'full node' formatters.
1023 if (!empty($vars['node']->referencing_field)) {
1024 $node = $vars['node'];
1025 $field = $node->referencing_field;
1026 $vars['template_files'][] = 'node-nodereference';
1027 $vars['template_files'][] = 'node-nodereference-'. $field['field_name'];
1028 $vars['template_files'][] = 'node-nodereference-'. $node->type;
1029 $vars['template_files'][] = 'node-nodereference-'. $field['field_name'] .'-'. $node->type;
1030 }
1031}
4ae62d83
YC
1032
1033/**
ea73d362
KS
1034 * FAPI theme for an individual elements.
1035 *
1036 * The textfield or select is already rendered by the
1037 * textfield or select themes and the html output
1038 * lives in $element['#children']. Override this theme to
1039 * make custom changes to the output.
1040 *
1041 * $element['#field_name'] contains the field name
1042 * $element['#delta] is the position of this element in the group
1043 */
1044function theme_nodereference_select($element) {
1045 return $element['#children'];
1046}
1047
578c7b49
YC
1048function theme_nodereference_buttons($element) {
1049 return $element['#children'];
1050}
1051
ea73d362
KS
1052function theme_nodereference_autocomplete($element) {
1053 return $element['#children'];
c9eaf362 1054}