node_reference.module 44 KB

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