Security fixes : http://drupal.org/node/330546
[project/cck.git] / nodereference.module
1 <?php
2 // $Id$
3
4 /**
5 * @file
6 * Defines a field type for referencing one node from another.
7 */
8
9
10 /**
11 * Implementation of hook_menu().
12 */
13 function nodereference_menu($may_cache) {
14 $items = array();
15
16 if ($may_cache) {
17 $items[] = array('path' => 'nodereference/autocomplete', 'title' => t('node reference autocomplete'),
18 'callback' => 'nodereference_autocomplete', 'access' => user_access('access content'), 'type' => MENU_CALLBACK);
19 }
20
21 return $items;
22 }
23
24 /**
25 * Implementation of hook_field_info().
26 */
27 function nodereference_field_info() {
28 return array(
29 'nodereference' => array('label' => t('Node Reference')),
30 );
31 }
32
33 /**
34 * Implementation of hook_field_settings().
35 */
36 function nodereference_field_settings($op, $field) {
37 switch ($op) {
38 case 'form':
39 $form = array();
40 $form['referenceable_types'] = array(
41 '#type' => 'checkboxes',
42 '#title' => t('Content types that can be referenced'),
43 '#multiple' => TRUE,
44 '#default_value' => isset($field['referenceable_types']) ? $field['referenceable_types'] : array(),
45 '#options' => array_map('check_plain', node_get_types('names')),
46 );
47 if (module_exists('views')) {
48 $views = array('--' => '--');
49 $result = db_query("SELECT name FROM {view_view} ORDER BY name");
50 while ($view = db_fetch_array($result)) {
51 $views[t('Existing Views')][$view['name']] = $view['name'];
52 }
53 views_load_cache();
54 $default_views = _views_get_default_views();
55 foreach ($default_views as $view) {
56 $views[t('Default Views')][$view->name] = $view->name;
57 }
58 if (count($views) > 1) {
59 $form['advanced'] = array(
60 '#type' => 'fieldset',
61 '#title' => t('Advanced - Nodes that can be referenced (View)'),
62 '#collapsible' => TRUE,
63 '#collapsed' => !isset($field['advanced_view']) || $field['advanced_view'] == '--',
64 );
65 $form['advanced']['advanced_view'] = array(
66 '#type' => 'select',
67 '#title' => t('View'),
68 '#options' => $views,
69 '#default_value' => isset($field['advanced_view']) ? $field['advanced_view'] : '--',
70 '#description' => t('Choose the "Views module" view that selects the nodes that can be referenced.<br>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>'),
71 );
72 $form['advanced']['advanced_view_args'] = array(
73 '#type' => 'textfield',
74 '#title' => t('View arguments'),
75 '#default_value' => isset($field['advanced_view_args']) ? $field['advanced_view_args'] : '',
76 '#required' => FALSE,
77 '#description' => t('Provide a comma separated list of arguments to pass to the view.'),
78 );
79 }
80 }
81 return $form;
82
83 case 'save':
84 $settings = array('referenceable_types');
85 if (module_exists('views')) {
86 $settings[] = 'advanced_view';
87 $settings[] = 'advanced_view_args';
88 }
89 return $settings;
90
91 case 'database columns':
92 $columns = array(
93 'nid' => array('type' => 'int', 'not null' => TRUE, 'default' => '0'),
94 );
95 return $columns;
96
97 case 'filters':
98 return array(
99 'default' => array(
100 'list' => '_nodereference_filter_handler',
101 'list-type' => 'list',
102 'operator' => 'views_handler_operator_or',
103 'value-type' => 'array',
104 'extra' => array('field' => $field),
105 ),
106 );
107 }
108 }
109
110 /**
111 * Implementation of hook_field().
112 */
113 function nodereference_field($op, &$node, $field, &$items, $teaser, $page) {
114 switch ($op) {
115 case 'validate':
116 $refs = _nodereference_potential_references($field, TRUE);
117 foreach ($items as $delta => $item) {
118 $error_field = isset($item['error_field']) ? $item['error_field'] : '';
119 unset($item['error_field']);
120 if (!empty($item['nid'])) {
121 if (!in_array($item['nid'], array_keys($refs))) {
122 form_set_error($error_field, t('%name : This post can\'t be referenced.', array('%name' => t($field['widget']['label']))));
123 }
124 }
125 }
126 return;
127 }
128 }
129
130 /**
131 * Implementation of hook_field_formatter_info().
132 */
133 function nodereference_field_formatter_info() {
134 return array(
135 'default' => array(
136 'label' => t('Title (link)'),
137 'field types' => array('nodereference'),
138 ),
139 'plain' => array(
140 'label' => t('Title (no link)'),
141 'field types' => array('nodereference'),
142 ),
143 'full' => array(
144 'label' => t('Full node'),
145 'field types' => array('nodereference'),
146 ),
147 'teaser' => array(
148 'label' => t('Teaser'),
149 'field types' => array('nodereference'),
150 ),
151 );
152 }
153
154 /**
155 * Implementation of hook_field_formatter().
156 */
157 function nodereference_field_formatter($field, $item, $formatter, $node) {
158 static $titles = array();
159
160 // We store the rendered nids in order to prevent infinite recursion
161 // when using the 'full node' / 'teaser' formatters.
162 static $recursion_queue = array();
163
164 if (empty($item['nid']) || !is_numeric($item['nid'])) {
165 return '';
166 }
167
168 if ($formatter == 'full' || $formatter == 'teaser') {
169 // If no 'referencing node' is set, we are starting a new 'reference thread'
170 if (!isset($node->referencing_node)) {
171 $recursion_queue = array();
172 }
173 $recursion_queue[] = $node->nid;
174 if (in_array($item['nid'], $recursion_queue)) {
175 // Prevent infinite recursion caused by reference cycles :
176 // if the node has already been rendered earlier in this 'thread',
177 // we fall back to 'default' (node title) formatter.
178 $formatter = 'default';
179 }
180 elseif ($referenced_node = node_load($item['nid'])) {
181 $referenced_node->referencing_node = $node;
182 $referenced_node->referencing_field = $field;
183 $titles[$item['nid']] = $referenced_node->title;
184 }
185 }
186
187 if (!isset($titles[$item['nid']])) {
188 $title = db_result(db_query("SELECT title FROM {node} WHERE nid=%d", $item['nid']));
189 $titles[$item['nid']] = $title ? $title : '';
190 }
191
192 switch ($formatter) {
193 case 'full':
194 return $referenced_node ? node_view($referenced_node, FALSE) : '';
195
196 case 'teaser':
197 return $referenced_node ? node_view($referenced_node, TRUE) : '';
198
199 case 'plain':
200 return check_plain($titles[$item['nid']]);
201
202 default:
203 return $titles[$item['nid']] ? l($titles[$item['nid']], 'node/'. $item['nid']) : '';
204 }
205 }
206
207 /**
208 * Implementation of hook_widget_info().
209 */
210 function nodereference_widget_info() {
211 return array(
212 'nodereference_select' => array(
213 'label' => t('Select List'),
214 'field types' => array('nodereference'),
215 ),
216 'nodereference_autocomplete' => array(
217 'label' => t('Autocomplete Text Field'),
218 'field types' => array('nodereference'),
219 ),
220 );
221 }
222
223 /**
224 * Implementation of hook_widget().
225 */
226 function nodereference_widget($op, &$node, $field, &$items) {
227 if ($field['widget']['type'] == 'nodereference_select') {
228 switch ($op) {
229 case 'prepare form values':
230 $items_transposed = content_transpose_array_rows_cols($items);
231 $items['default nids'] = $items_transposed['nid'];
232 break;
233
234 case 'form':
235 $form = array();
236
237 $options = _nodereference_potential_references($field, TRUE);
238 foreach ($options as $key => $value) {
239 $options[$key] = _nodereference_item($field, $value, FALSE);
240 }
241 if (!$field['required']) {
242 $options = array(0 => t('<none>')) + $options;
243 }
244
245 $form[$field['field_name']] = array('#tree' => TRUE);
246 $form[$field['field_name']]['nids'] = array(
247 '#type' => 'select',
248 '#title' => t($field['widget']['label']),
249 '#default_value' => $items['default nids'],
250 '#multiple' => $field['multiple'],
251 '#size' => $field['multiple'] ? min(count($options), 6) : 0,
252 '#options' => $options,
253 '#required' => $field['required'],
254 '#description' => content_filter_xss(t($field['widget']['description'])),
255 );
256
257 return $form;
258
259 case 'process form values':
260 if ($field['multiple']) {
261 // if nothing selected, make it 'none'
262 if (empty($items['nids'])) {
263 $items['nids'] = array(0 => '0');
264 }
265 // drop the 'none' options if other items were also selected
266 elseif (count($items['nids']) > 1) {
267 unset($items['nids'][0]);
268 }
269
270 $items = array_values(content_transpose_array_rows_cols(array('nid' => $items['nids'])));
271 }
272 else {
273 $items[0]['nid'] = $items['nids'];
274 }
275 // Remove the widget's data representation so it isn't saved.
276 unset($items['nids']);
277 foreach ($items as $delta => $item) {
278 $items[$delta]['error_field'] = $field['field_name'] .'][nids';
279 }
280 }
281 }
282 else {
283 switch ($op) {
284 case 'prepare form values':
285 foreach ($items as $delta => $item) {
286 if (!empty($items[$delta]['nid'])) {
287 $items[$delta]['default node_name'] = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $items[$delta]['nid']));
288 $items[$delta]['default node_name'] .= ' [nid:'. $items[$delta]['nid'] .']';
289 }
290 }
291 break;
292
293 case 'form':
294 $form = array();
295 $form[$field['field_name']] = array('#tree' => TRUE);
296
297 if ($field['multiple']) {
298 $form[$field['field_name']]['#type'] = 'fieldset';
299 $form[$field['field_name']]['#description'] = content_filter_xss(t($field['widget']['description']));
300 $delta = 0;
301 foreach ($items as $item) {
302 if ($item['nid']) {
303 $form[$field['field_name']][$delta]['node_name'] = array(
304 '#type' => 'textfield',
305 '#title' => ($delta == 0) ? t($field['widget']['label']) : '',
306 '#autocomplete_path' => 'nodereference/autocomplete/'. $field['field_name'],
307 '#default_value' => $item['default node_name'],
308 '#required' => ($delta == 0) ? $field['required'] : FALSE,
309 );
310 $delta++;
311 }
312 }
313 foreach (range($delta, $delta + 2) as $delta) {
314 $form[$field['field_name']][$delta]['node_name'] = array(
315 '#type' => 'textfield',
316 '#title' => ($delta == 0) ? t($field['widget']['label']) : '',
317 '#autocomplete_path' => 'nodereference/autocomplete/'. $field['field_name'],
318 '#default_value' => '',
319 '#required' => ($delta == 0) ? $field['required'] : FALSE,
320 );
321 }
322 }
323 else {
324 $form[$field['field_name']][0]['node_name'] = array(
325 '#type' => 'textfield',
326 '#title' => t($field['widget']['label']),
327 '#autocomplete_path' => 'nodereference/autocomplete/'. $field['field_name'],
328 '#default_value' => $items[0]['default node_name'],
329 '#required' => $field['required'],
330 '#description' => content_filter_xss( t( $field['widget']['description'] )),
331 );
332 }
333 return $form;
334
335 case 'validate':
336 foreach ($items as $delta => $item) {
337 $error_field = $field['field_name'] .']['. $delta .'][node_name';
338 if (!empty($item['node_name'])) {
339 preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $item['node_name'], $matches);
340 if (!empty($matches)) {
341 // explicit nid
342 list(, $title, $nid) = $matches;
343 if (!empty($title) && ($n = node_load($nid)) && $title != $n->title) {
344 form_set_error($error_field, t('%name : Title mismatch. Please check your selection.', array('%name' => t($field['widget']['label']))));
345 }
346 }
347 else {
348 // no explicit nid
349 $nids = _nodereference_potential_references($field, FALSE, $item['node_name'], TRUE);
350 if (empty($nids)) {
351 form_set_error($error_field, t('%name: Found no valid post with that title.', array('%name' => t($field['widget']['label']))));
352 }
353 else {
354 // TODO:
355 // the best thing would be to present the user with an additional form,
356 // allowing the user to choose between valid candidates with the same title
357 // ATM, we pick the first matching candidate...
358 $nid = array_shift(array_keys($nids));
359 }
360 }
361 }
362 }
363 return;
364
365 case 'process form values':
366 foreach ($items as $delta => $item) {
367 $nid = 0;
368 if (!empty($item['node_name'])) {
369 preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $item['node_name'], $matches);
370 if (!empty($matches)) {
371 // explicit nid
372 $nid = $matches[2];
373 }
374 else {
375 // no explicit nid
376 // TODO :
377 // the best thing would be to present the user with an additional form,
378 // allowing the user to choose between valid candidates with the same title
379 // ATM, we pick the first matching candidate...
380 $nids = _nodereference_potential_references($field, FALSE, $item['node_name'], TRUE);
381 $nid = (!empty($nids)) ? array_shift(array_keys($nids)) : 0;
382 }
383 }
384 // Remove the widget's data representation so it isn't saved.
385 unset($items[$delta]['node_name']);
386 if (!empty($nid)) {
387 $items[$delta]['nid'] = $nid;
388 $items[$delta]['error_field'] = $field['field_name'] .']['. $delta .'][node_name';
389 }
390 elseif ($delta > 0) {
391 // Don't save empty fields when they're not the first value (keep '0' otherwise)
392 unset($items[$delta]);
393 }
394 }
395 break;
396 }
397 }
398 }
399
400 /**
401 * Fetch an array of all candidate referenced nodes, for use in presenting the selection form to the user.
402 */
403 function _nodereference_potential_references($field, $return_full_nodes = FALSE, $string = '', $exact_string = false) {
404 if (module_exists('views') && isset($field['advanced_view']) && $field['advanced_view'] != '--' && ($view = views_get_view($field['advanced_view']))) {
405 // advanced field : referenceable nodes defined by a view
406 // let views.module build the query
407
408 // arguments for the view
409 $view_args = array();
410 if (isset($field['advanced_view_args'])) {
411 // TODO: Support Tokens using token.module ?
412 $view_args = array_map(trim, explode(',', $field['advanced_view_args']));
413 }
414
415 if (isset($string)) {
416 views_view_add_filter($view, 'node', 'title', $exact_string ? '=' : 'contains', $string, null);
417 }
418
419 // we do need title field, so add it if not present (unlikely, but...)
420 $has_title = array_reduce($view->field, create_function('$a, $b', 'return ($b["field"] == "title") || $a;'), false);
421 if (!$has_title) {
422 views_view_add_field($view, 'node', 'title', '');
423 }
424 views_load_cache();
425 views_sanitize_view($view);
426
427 // make sure the fields get included in the query
428 $view->page = true;
429 $view->page_type = 'list';
430
431 // make sure the query is not cached
432 unset($view->query); // Views 1.5-
433 $view->is_cacheable = FALSE; // Views 1.6+
434
435 $view_result = views_build_view('result', $view, $view_args);
436 $result = $view_result['result'];
437 }
438 else {
439 // standard field : referenceable nodes defined by content types
440 // build the appropriate query
441 $related_types = array();
442 $args = array();
443
444 if (isset($field['referenceable_types'])) {
445 foreach ($field['referenceable_types'] as $related_type) {
446 if ($related_type) {
447 $related_types[] = " n.type = '%s'";
448 $args[] = $related_type;
449 }
450 }
451 }
452
453 $related_clause = implode(' OR ', $related_types);
454
455 if (!count($related_types)) {
456 return array();
457 }
458
459 if ($string !== '') {
460 $string_clause = $exact_string ? " AND n.title = '%s'" : " AND n.title LIKE '%%%s%'";
461 $related_clause = "(". $related_clause .")". $string_clause;
462 $args[] = $string;
463 }
464
465 $result = db_query(db_rewrite_sql("SELECT n.nid, n.title AS node_title, n.type AS node_type FROM {node} n WHERE ". $related_clause ." ORDER BY n.title, n.type"), $args);
466 }
467
468 if (db_num_rows($result) == 0) {
469 return array();
470 }
471
472 $rows = array();
473
474 while ($node = db_fetch_object($result)) {
475 if ($return_full_nodes) {
476 $rows[$node->nid] = $node;
477 }
478 else {
479 $rows[$node->nid] = $node->node_title;
480 }
481 }
482
483 return $rows;
484 }
485
486 /**
487 * Retrieve a pipe delimited string of autocomplete suggestions
488 */
489 function nodereference_autocomplete($field_name, $string = '') {
490 $fields = content_fields();
491 $field = $fields[$field_name];
492 $matches = array();
493
494 foreach (_nodereference_potential_references($field, TRUE, $string) as $row) {
495 $matches[$row->node_title .' [nid:'. $row->nid .']'] = _nodereference_item($field, $row);
496 }
497 print drupal_to_js($matches);
498 exit();
499 }
500
501 function _nodereference_item($field, $item, $html = TRUE) {
502 if (module_exists('views') && isset($field['advanced_view']) && $field['advanced_view'] != '--' && ($view = views_get_view($field['advanced_view']))) {
503 $output = theme('nodereference_item_advanced', $item, $view);
504 if (!$html) {
505 // Views theming runs check_plain (htmlentities) on the values.
506 // We reverse that with html_entity_decode.
507 $output = html_entity_decode(strip_tags($output), ENT_QUOTES);
508 }
509 }
510 else {
511 $output = theme('nodereference_item_simple', $item);
512 $output = $html ? check_plain($output) : $output;
513 }
514 return $output;
515 }
516
517 function theme_nodereference_item_advanced($item, $view) {
518 $fields = _views_get_fields();
519 $item_fields = array();
520 foreach ($view->field as $field) {
521 $value = views_theme_field('views_handle_field', $field['queryname'], $fields, $field, $item, $view);
522 // remove link tags (ex : for node titles)
523 $value = preg_replace('/<a[^>]*>(.*)<\/a>/iU', '$1', $value);
524 if (!empty($value)) {
525 $item_fields[] = "<span class='view-field view-data-$field[queryname]'>$value</span>";;
526 }
527 }
528 $output = implode(' - ', $item_fields);
529 $output = "<span class='view-item view-item-$view->name'>$output</span>";
530 return $output;
531 }
532
533 function theme_nodereference_item_simple($item) {
534 return $item->node_title;
535 }
536
537 /**
538 * Provide a list of nodes to filter on.
539 */
540 function _nodereference_filter_handler($op, $filterinfo) {
541 $options = array(0 => t('<empty>'));
542 $options = $options + _nodereference_potential_references($filterinfo['extra']['field']);
543 return $options;
544 }
545
546 /**
547 * Implementation of hook_panels_relationships().
548 */
549 function nodereference_panels_relationships() {
550 $args = array();
551 $args['node_from_noderef'] = array(
552 'title' => t('Node from reference'),
553 'keyword' => 'nodereference',
554 'description' => t('Adds a node from a node reference in a node context; if multiple nodes are referenced, this will get the first referenced node only.'),
555 'required context' => new panels_required_context(t('Node'), 'node'),
556 'context' => 'nodereference_node_from_noderef_context',
557 'settings form' => 'nodereference_node_from_noderef_settings_form',
558 'settings form validate' => 'nodereference_node_from_noderef_settings_form_validate',
559 );
560 return $args;
561 }
562
563 /**
564 * Return a new panels context based on an existing context
565 */
566 function nodereference_node_from_noderef_context($context = NULL, $conf) {
567 // If unset it wants a generic, unfilled context, which is just NULL
568 if (empty($context->data)) {
569 return panels_context_create_empty('node', NULL);
570 }
571 if (isset($context->data->{$conf['field_name']}[0]['nid']) && ($nid = $context->data->{$conf['field_name']}[0]['nid'])) {
572 if ($node = node_load($nid)) {
573 return panels_context_create('node', $node);
574 }
575 }
576 }
577
578 /**
579 * Settings form for the panels relationship.
580 */
581 function nodereference_node_from_noderef_settings_form($conf) {
582 $options = array();
583 foreach (content_fields() as $field) {
584 if ($field['type'] == 'nodereference') {
585 $options[$field['field_name']] = t($field['widget']['label']);
586 }
587 }
588 $form['field_name'] = array(
589 '#title' => t('Node reference field'),
590 '#type' => 'select',
591 '#options' => $options,
592 '#default_value' => $conf['field_name'],
593 '#prefix' => '<div class="clear-block">',
594 '#suffix' => '</div>',
595 );
596
597 return $form;
598 }