12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229 |
- <?php
- /**
- * @file
- * Defines a field type for referencing one node from another.
- */
- /**
- * Implements hook_menu().
- */
- function node_reference_menu() {
- $items['node_reference/autocomplete/%/%/%'] = array(
- 'page callback' => 'node_reference_autocomplete',
- 'page arguments' => array(2, 3, 4),
- 'access callback' => 'reference_autocomplete_access',
- 'access arguments' => array(2, 3, 4),
- 'type' => MENU_CALLBACK,
- );
- return $items;
- }
- /**
- * Implements hook_field_info().
- */
- function node_reference_field_info() {
- return array(
- 'node_reference' => array(
- 'label' => t('Node reference'),
- 'description' => t('This field stores the ID of a related node as an integer value.'),
- 'settings' => array(
- 'referenceable_types' => array(),
- 'view' => array(
- 'view_name' => '',
- 'display_name' => '',
- 'args' => array(),
- ),
- ),
- // It probably make more sense to have the referenceable types be per-field than per-instance
- // 'instance settings' => array('referenceable_types' => array()),
- 'default_widget' => 'options_select', // node_reference_autocomplete',
- 'default_formatter' => 'node_reference_default',
- // Support hook_entity_property_info() from contrib "Entity API".
- 'property_type' => 'node',
- // Support default token formatter for field tokens.
- 'default_token_formatter' => 'node_reference_plain',
- ),
- );
- }
- /**
- * Implements hook_field_settings_form().
- */
- function node_reference_field_settings_form($field, $instance, $has_data) {
- $settings = $field['settings'];
- $form = array();
- $form['referenceable_types'] = array(
- '#type' => 'checkboxes',
- '#title' => t('Content types that can be referenced'),
- '#multiple' => TRUE,
- '#default_value' => $settings['referenceable_types'],
- '#options' => array_map('check_plain', node_type_get_names()),
- );
- if (module_exists('views')) {
- $view_settings = $settings['view'];
- $description = '<p>' . t('The list of nodes that can be referenced can provided by a view (Views module) using the "References" display type.') . '</p>';
- // Special note for legacy fields migrated from D6.
- if (!empty($view_settings['view_name']) && $view_settings['display_name'] == 'default') {
- $description .= '<p><strong><span class="admin-missing">'. t("Important D6 migration note:") . '</span></strong>';
- $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']));
- $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.");
- $description .= '<br/>' . t("The field will work correctly until then, but submitting this form might inadvertently change the field settings.") . '</p>';
- }
- $form['view'] = array(
- '#type' => 'fieldset',
- '#title' => t('Views - Nodes that can be referenced'),
- '#collapsible' => TRUE,
- '#collapsed' => empty($view_settings['view_name']),
- '#description' => $description,
- );
- $views_options = references_get_views_options('node');
- if ($views_options) {
- // The value of the 'view_and_display' select below will need to be split
- // into 'view_name' and 'view_display' in the final submitted values, so
- // we massage the data at validate time on the wrapping element (not
- // ideal).
- $form['view']['#element_validate'] = array('_node_reference_view_settings_validate');
- $views_options = array('' => '<' . t('none') . '>') + $views_options;
- $default = empty($view_settings['view_name']) ? '' : $view_settings['view_name'] . ':' .$view_settings['display_name'];
- $form['view']['view_and_display'] = array(
- '#type' => 'select',
- '#title' => t('View used to select the nodes'),
- '#options' => $views_options,
- '#default_value' => $default,
- '#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>' .
- 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>'),
- );
- $default = implode(', ', $view_settings['args']);
- $form['view']['args'] = array(
- '#type' => 'textfield',
- '#title' => t('View arguments'),
- '#default_value' => $default,
- '#required' => FALSE,
- '#description' => t('Provide a comma separated list of arguments to pass to the view.'),
- );
- }
- else {
- $form['view']['no_view_help'] = array(
- '#markup' => '<p>' . t('No eligible view was found.') .'</p>',
- );
- }
- }
- return $form;
- }
- /**
- * Validate callback for the 'view settings' fieldset.
- *
- * Puts back the various form values in the expected shape.
- */
- function _node_reference_view_settings_validate($element, &$form_state, $form) {
- // Split view name and display name from the 'view_and_display' value.
- if (!empty($element['view_and_display']['#value'])) {
- list($view, $display) = explode(':', $element['view_and_display']['#value']);
- }
- else {
- $view = '';
- $display = '';
- }
- // Explode the 'args' string into an actual array. Beware, explode() turns an
- // empty string into an array with one empty string. We'll need an empty array
- // instead.
- $args_string = trim($element['args']['#value']);
- $args = ($args_string === '') ? array() : array_map('trim', explode(',', $args_string));
- $value = array('view_name' => $view, 'display_name' => $display, 'args' => $args);
- form_set_value($element, $value, $form_state);
- }
- /**
- * Implements hook_field_validate().
- *
- * Possible error codes:
- * - 'invalid_nid': nid is not valid for the field (not a valid node id, or the node is not referenceable).
- */
- function node_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
- // Extract nids to check.
- $ids = array();
- // First check non-numeric "nid's to avoid losing time with them.
- foreach ($items as $delta => $item) {
- if (is_array($item) && !empty($item['nid'])) {
- if (is_numeric($item['nid'])) {
- $ids[] = $item['nid'];
- }
- else {
- $errors[$field['field_name']][$langcode][$delta][] = array(
- 'error' => 'invalid_nid',
- 'message' => t("%name: invalid input.",
- array('%name' => $instance['label'])),
- );
- }
- }
- }
- // Prevent performance hog if there are no ids to check.
- if ($ids) {
- $options = array(
- 'ids' => $ids,
- );
- $refs = node_reference_potential_references($field, $options);
- foreach ($items as $delta => $item) {
- if (is_array($item)) {
- if (!empty($item['nid']) && !isset($refs[$item['nid']])) {
- $errors[$field['field_name']][$langcode][$delta][] = array(
- 'error' => 'invalid_nid',
- 'message' => t("%name: this post can't be referenced.",
- array('%name' => $instance['label'])),
- );
- }
- }
- }
- }
- }
- /**
- * Implements hook_field_prepare_view().
- */
- function node_reference_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
- $checked_ids = &drupal_static(__FUNCTION__, array());
- // Set an 'access' property on each item (TRUE if the node exists and is
- // accessible by the current user).
- // Extract ids to check.
- $ids = array();
- foreach ($items as $id => $entity_items) {
- foreach ($entity_items as $delta => $item) {
- if (is_array($item)) {
- // Default to 'not accessible'.
- $items[$id][$delta]['access'] = FALSE;
- if (!empty($item['nid']) && is_numeric($item['nid'])) {
- $ids[$item['nid']] = $item['nid'];
- }
- }
- }
- }
- if ($ids) {
- // Load information about ids that we haven't already loaded during this
- // page request.
- $ids_to_check = array_diff($ids, array_keys($checked_ids));
- if (!empty($ids_to_check)) {
- $query = db_select('node', 'n')
- ->addTag('node_access')
- ->addMetaData('id', 'node_reference_field_prepare_view')
- ->addMetaData('field', $field)
- ->fields('n', array('nid'))
- // WHERE n.nid IN (nids to check) AND ...
- ->condition('n.nid', $ids_to_check, 'IN');
- // Unless the user has the right permissions, restrict on the node status.
- // (note: the 'view any unpublished content' permission is provided by the
- // 'view_unpublished' contrib module.)
- if (!user_access('bypass node access') && !user_access('view any unpublished content')) {
- // ... AND n.status = 1
- $status_condition = db_or()
- ->condition('n.status', NODE_PUBLISHED);
- // Take the 'view own unpublished content' permission into account to
- // decide whether some unpublished nodes should still be visible. We
- // only need the items in $ids_to_check because those are the only
- // entries that we are interested in. Any other nodes created by the
- // user are simply ignored so lets only retrieve that subset.
- 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())) {
- // ... AND (n.status = 1 OR n.nid IN (own unpublished))
- $status_condition
- ->condition('n.nid', $own_unpublished, 'IN');
- }
- $query->condition($status_condition);
- }
- $accessible_ids = $query->execute()->fetchAllAssoc('nid');
- // Populate our static list so that we do not query on those ids again.
- foreach ($ids_to_check as $id) {
- $checked_ids[$id] = isset($accessible_ids[$id]);
- }
- }
- foreach ($items as $id => $entity_items) {
- foreach ($entity_items as $delta => $item) {
- if (is_array($item) && !empty($item['nid']) && !empty($checked_ids[$item['nid']])) {
- $items[$id][$delta]['access'] = TRUE;
- }
- }
- }
- }
- }
- /**
- * Implements hook_field_is_empty().
- */
- function node_reference_field_is_empty($item, $field) {
- // nid = 0 is empty too, which is exactly what we want.
- return empty($item['nid']);
- }
- /**
- * Implements hook_field_formatter_info().
- */
- function node_reference_field_formatter_info() {
- $ret = array(
- 'node_reference_default' => array(
- 'label' => t('Title (link)'),
- 'description' => t('Display the title of the referenced node as a link to the node page.'),
- 'field types' => array('node_reference'),
- ),
- 'node_reference_plain' => array(
- 'label' => t('Title (no link)'),
- 'description' => t('Display the title of the referenced node as plain text.'),
- 'field types' => array('node_reference'),
- ),
- 'node_reference_node' => array(
- 'label' => t('Rendered node'),
- 'description' => t('Display the referenced node in a specific view mode'),
- 'field types' => array('node_reference'),
- 'settings' => array('node_reference_view_mode' => 'full'),
- ),
- 'node_reference_nid' => array(
- 'label' => t('Node ID'),
- 'description' => t('Display the referenced node ID'),
- 'field types' => array('node_reference'),
- ),
- 'node_reference_path' => array(
- 'label' => t('URL as plain text'),
- 'description' => t('Display the URL of the referenced node'),
- 'field types' => array('node_reference'),
- 'settings' => array(
- 'alias' => TRUE,
- 'absolute' => FALSE
- ),
- ),
- );
- return $ret;
- }
- /**
- * Implements hook_field_formatter_settings_form().
- */
- function node_reference_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
- $display = $instance['display'][$view_mode];
- $settings = $display['settings'];
- $element = array();
- switch ($display['type']) {
- case 'node_reference_node':
- $entity_info = entity_get_info('node');
- $modes = $entity_info['view modes'];
- $options = array();
- foreach ($modes as $name => $mode) {
- $options[$name] = $mode['label'];
- }
- $element['node_reference_view_mode'] = array(
- '#title' => t('View mode'),
- '#type' => 'select',
- '#options' => $options,
- '#default_value' => $settings['node_reference_view_mode'],
- // Never empty, so no #empty_option
- );
- break;
- case 'node_reference_path':
- $element['alias'] = array(
- '#type' => 'checkbox',
- '#title' => t('Display the aliased path (if exists) instead of the system path'),
- '#default_value' => $settings['alias'],
- );
- $element['absolute'] = array(
- '#type' => 'checkbox',
- '#title' => t('Display an absolute URL'),
- '#default_value' => $settings['absolute'],
- );
- break;
- }
- return $element;
- }
- /**
- * Implements hook_field_formatter_settings_summary().
- */
- function node_reference_field_formatter_settings_summary($field, $instance, $view_mode) {
- $display = $instance['display'][$view_mode];
- $settings = $display['settings'];
- $summary = array();
- switch ($display['type']) {
- case 'node_reference_node':
- $entity_info = entity_get_info('node');
- $modes = $entity_info['view modes'];
- $mode = $modes[$settings['node_reference_view_mode']]['label'];
- $summary[] = t('View mode: %mode', array('%mode' => $mode));
- break;
- case 'node_reference_path':
- $summary[] = t('Aliased path: %yes_no', array('%yes_no' => $settings['alias'] ? t('Yes') : t('No')));
- $summary[] = t('Absolute URL: %yes_no', array('%yes_no' => $settings['absolute'] ? t('Yes') : t('No')));
- break;
- }
- return implode('<br />', $summary);
- }
- /**
- * Implements hook_field_formatter_prepare_view().
- *
- * Preload all nodes referenced by items using 'full entity' formatters.
- */
- function node_reference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
- // Load the referenced nodes, except for the 'node_reference_nid' which does
- // not need full objects.
- // Collect ids to load.
- $ids = array();
- foreach ($displays as $id => $display) {
- if ($display['type'] != 'node_reference_nid') {
- foreach ($items[$id] as $delta => $item) {
- if ($item['access']) {
- $ids[$item['nid']] = $item['nid'];
- }
- }
- }
- }
- $entities = node_load_multiple($ids);
- // Add the loaded nodes to the items.
- foreach ($displays as $id => $display) {
- if ($display['type'] != 'node_reference_nid') {
- foreach ($items[$id] as $delta => $item) {
- if ($item['access']) {
- $items[$id][$delta]['node'] = $entities[$item['nid']];
- }
- }
- }
- }
- }
- /**
- * Implements hook_field_formatter_view().
- */
- function node_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
- $settings = $display['settings'];
- $result = array();
- switch ($display['type']) {
- case 'node_reference_default':
- case 'node_reference_plain':
- foreach ($items as $delta => $item) {
- if ($item['access']) {
- $node = $item['node'];
- $label = entity_label('node', $node);
- if ($display['type'] == 'node_reference_default') {
- $uri = entity_uri('node', $node);
- $result[$delta] = array(
- '#type' => 'link',
- '#title' => $label,
- '#href' => $uri['path'],
- '#options' => $uri['options'],
- );
- }
- else {
- $result[$delta] = array(
- '#markup' => check_plain($label),
- );
- }
- if (!$node->status) {
- $result[$delta]['#prefix'] = '<span class="node-unpublished">';
- $result[$delta]['#suffix'] = '</span>';
- }
- }
- }
- break;
- case 'node_reference_node':
- // To prevent infinite recursion caused by reference cycles, we store
- // diplayed nodes in a recursion queue.
- $recursion_queue = &drupal_static(__FUNCTION__, array());
- // If no 'referencing entity' is set, we are starting a new 'reference
- // thread' and need to reset the queue.
- // @todo Bug: $entity->referencing_entity on nodes referenced in a different
- // thread on the page. E.g: 1 references 1+2 / 2 references 1+2 / visit homepage.
- // We'd need a more accurate way...
- if (!isset($entity->referencing_entity)) {
- $recursion_queue = array();
- }
- // The recursion queue only needs to track nodes.
- if ($entity_type == 'node') {
- list($id) = entity_extract_ids($entity_type, $entity);
- $recursion_queue[$id] = $id;
- }
- // Check the recursion queue to determine which nodes should be fully
- // displayed, and which nodes will only be displayed as a title.
- $nodes_display = array();
- foreach ($items as $delta => $item) {
- if ($item['access'] && !isset($recursion_queue[$item['nid']])) {
- $nodes_display[$item['nid']] = $item['node'];
- }
- }
- // Load and build the fully displayed nodes.
- if ($nodes_display) {
- foreach ($nodes_display as $nid => $node) {
- $nodes_display[$nid]->referencing_entity = $entity;
- $nodes_display[$nid]->referencing_field = $field['field_name'];
- }
- $nodes_built = node_view_multiple($nodes_display, $settings['node_reference_view_mode']);
- }
- // Assemble the render array.
- foreach ($items as $delta => $item) {
- if ($item['access']) {
- if (isset($nodes_display[$item['nid']])) {
- $result[$delta] = $nodes_built['nodes'][$item['nid']];
- }
- else {
- $node = $item['node'];
- $label = entity_label('node', $node);
- $uri = entity_uri('node', $node);
- $result[$delta] = array(
- '#type' => 'link',
- '#title' => $label,
- '#href' => $uri['path'],
- '#options' => $uri['options'],
- );
- if (!$node->status) {
- $result[$delta]['#prefix'] = '<span class="node-unpublished">';
- $result[$delta]['#suffix'] = '</span>';
- }
- }
- }
- }
- break;
- case 'node_reference_nid':
- foreach ($items as $delta => $item) {
- if ($item['access']) {
- $result[$delta] = array(
- '#markup' => $item['nid'],
- );
- }
- }
- break;
- case 'node_reference_path':
- foreach ($items as $delta => $item) {
- if ($item['access']) {
- $uri = entity_uri('node', $item['node']);
- $options = array(
- 'absolute' => $settings['absolute'],
- 'alias' => !$settings['alias'],
- );
- $options += $uri['options'];
- $result[$delta] = array(
- '#markup' => url($uri['path'], $options),
- );
- }
- }
- break;
- }
- return $result;
- }
- /**
- * Implements hook_field_widget_info().
- */
- function node_reference_field_widget_info() {
- return array(
- 'node_reference_autocomplete' => array(
- 'label' => t('Autocomplete text field'),
- 'description' => t('Display the list of referenceable nodes as a textfield with autocomplete behaviour.'),
- 'field types' => array('node_reference'),
- 'settings' => array(
- 'autocomplete_match' => 'contains',
- 'size' => 60,
- 'autocomplete_path' => 'node_reference/autocomplete',
- ),
- ),
- );
- }
- /**
- * Implements hook_field_widget_info_alter().
- */
- function node_reference_field_widget_info_alter(&$info) {
- $info['options_select']['field types'][] = 'node_reference';
- $info['options_buttons']['field types'][] = 'node_reference';
- }
- /**
- * Implements hook_field_widget_settings_form().
- */
- function node_reference_field_widget_settings_form($field, $instance) {
- $widget = $instance['widget'];
- $defaults = field_info_widget_settings($widget['type']);
- $settings = array_merge($defaults, $widget['settings']);
- $form = array();
- if ($widget['type'] == 'node_reference_autocomplete') {
- $form['autocomplete_match'] = array(
- '#type' => 'select',
- '#title' => t('Autocomplete matching'),
- '#default_value' => $settings['autocomplete_match'],
- '#options' => array(
- 'starts_with' => t('Starts with'),
- 'contains' => t('Contains'),
- 'fuzzy' => t('Fuzzy search'),
- ),
- '#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.'),
- );
- $form['size'] = array(
- '#type' => 'textfield',
- '#title' => t('Size of textfield'),
- '#default_value' => $settings['size'],
- '#element_validate' => array('_element_validate_integer_positive'),
- '#required' => TRUE,
- );
- }
- return $form;
- }
- /**
- * Implements hook_field_widget_form().
- */
- function node_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
- switch ($instance['widget']['type']) {
- case 'node_reference_autocomplete':
- $element += array(
- '#type' => 'textfield',
- '#default_value' => isset($items[$delta]['nid']) ? $items[$delta]['nid'] : NULL,
- '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'] . '/' . $field['field_name'],
- '#size' => $instance['widget']['settings']['size'],
- '#maxlength' => NULL,
- '#element_validate' => array('node_reference_autocomplete_validate'),
- '#value_callback' => 'node_reference_autocomplete_value',
- );
- break;
- }
- return array('nid' => $element);
- }
- /**
- * Value callback for a node_reference autocomplete element.
- *
- * Replace the node nid with a node title.
- */
- function node_reference_autocomplete_value($element, $input = FALSE, $form_state) {
- if ($input === FALSE) {
- // We're building the displayed 'default value': expand the raw nid into
- // "node title [nid:n]".
- $nid = $element['#default_value'];
- if (!empty($nid)) {
- $q = db_select('node', 'n');
- $node_title_alias = $q->addField('n', 'title');
- $q->addTag('node_access')
- ->condition('n.nid', $nid)
- ->range(0, 1);
- $result = $q->execute();
- // @todo If no result (node doesn't exist or no access).
- $value = $result->fetchField();
- $value .= ' [nid:' . $nid . ']';
- return $value;
- }
- }
- }
- /**
- * Validation callback for a node_reference autocomplete element.
- */
- function node_reference_autocomplete_validate($element, &$form_state, $form) {
- $field = field_widget_field($element, $form_state);
- $instance = field_widget_instance($element, $form_state);
- $value = $element['#value'];
- $nid = NULL;
- if (!empty($value)) {
- // Check whether we have an explicit "[nid:n]" input.
- preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $value, $matches);
- if (!empty($matches)) {
- // Explicit nid. Check that the 'title' part matches the actual title for
- // the nid.
- list(, $title, $nid) = $matches;
- if (!empty($nid)) {
- $real_title = db_select('node', 'n')
- ->fields('n', array('title'))
- ->condition('n.nid', $nid)
- ->execute()
- ->fetchField();
- if (empty($real_title)) {
- form_error($element, t('%name: No node found. Please check your selection.', array('%name' => $instance['label'])));
- }
- }
- }
- else {
- // No explicit nid (the submitted value was not populated by autocomplete
- // selection). Get the nid of a referencable node from the entered title.
- $options = array(
- 'string' => $value,
- 'match' => 'equals',
- 'limit' => 1,
- );
- $references = node_reference_potential_references($field, $options);
- if ($references) {
- // @todo The best thing would be to present the user with an
- // additional form, allowing the user to choose between valid
- // candidates with the same title. ATM, we pick the first
- // matching candidate...
- $nid = key($references);
- }
- else {
- form_error($element, t('%name: found no valid post with that title.', array('%name' => $instance['label'])));
- }
- }
- }
- // Set the element's value as the node id that was extracted from the entered
- // input.
- form_set_value($element, $nid, $form_state);
- }
- /**
- * Implements hook_field_widget_error().
- */
- function node_reference_field_widget_error($element, $error, $form, &$form_state) {
- form_error($element['nid'], $error['message']);
- }
- /**
- * Builds a list of referenceable nodes suitable for the '#option' FAPI property.
- *
- * Warning: the function does NOT take care of encoding or escaping the node
- * titles. Proper massaging needs to be performed by the caller, according to
- * the destination FAPI '#type' (radios / checkboxes / select).
- *
- * @param $field
- * The field definition.
- * @param $flat
- * Whether optgroups are allowed.
- *
- * @return
- * An array of referenceable node titles, keyed by node id. If the $flat
- * parameter is TRUE, the list might be nested by optgroup first.
- */
- function _node_reference_options($field, $flat = TRUE) {
- $references = node_reference_potential_references($field);
- $options = array();
- foreach ($references as $key => $value) {
- // The label, displayed in selects and checkboxes/radios, should have HTML
- // entities unencoded. The widgets (core's options.module) take care of
- // applying the relevant filters (strip_tags() or filter_xss()).
- $label = html_entity_decode($value['rendered'], ENT_QUOTES);
- if (empty($value['group']) || $flat) {
- $options[$key] = $label;
- }
- else {
- // The group name, displayed in selects, cannot contain tags, and should
- // have HTML entities unencoded.
- $group = html_entity_decode(strip_tags($value['group']), ENT_QUOTES);
- $options[$group][$key] = $label;
- }
- }
- return $options;
- }
- /**
- * Retrieves an array of candidate referenceable nodes.
- *
- * This info is used in various places (allowed values, autocomplete
- * results, input validation...). Some of them only need the nids,
- * others nid + titles, others yet nid + titles + rendered row (for
- * display in widgets).
- *
- * The array we return contains all the potentially needed information,
- * and lets consumers use the parts they actually need.
- *
- * @param $field
- * The field definition.
- * @param $options
- * An array of options to limit the scope of the returned list. The following
- * key/value pairs are accepted:
- * - string: string to filter titles on (used by autocomplete).
- * - match: operator to match the above string against, can be any of:
- * 'contains', 'equals', 'starts_with'. Defaults to 'contains'.
- * - ids: array of specific node ids to lookup.
- * - limit: maximum size of the the result set. Defaults to 0 (no limit).
- *
- * @return
- * An array of valid nodes in the form:
- * array(
- * nid => array(
- * 'title' => The node title,
- * 'rendered' => The text to display in widgets (can be HTML)
- * ),
- * ...
- * )
- */
- function node_reference_potential_references($field, $options = array()) {
- // Fill in default options.
- $options += array(
- 'string' => '',
- 'match' => 'contains',
- 'ids' => array(),
- 'limit' => 0,
- );
- $results = &drupal_static(__FUNCTION__, array());
- // Create unique id for static cache.
- $cid = $field['field_name'] . ':' . $options['match'] . ':'
- . ($options['string'] !== '' ? $options['string'] : implode('-', $options['ids']))
- . ':' . $options['limit'];
- if (!isset($results[$cid])) {
- $references = FALSE;
- if (module_exists('views') && !empty($field['settings']['view']['view_name'])) {
- $references = _node_reference_potential_references_views($field, $options);
- }
- if ($references === FALSE) {
- $references = _node_reference_potential_references_standard($field, $options);
- }
- // Store the results.
- $results[$cid] = !empty($references) ? $references : array();
- }
- return $results[$cid];
- }
- /**
- * Helper function for node_reference_potential_references().
- *
- * Case of Views-defined referenceable nodes.
- */
- function _node_reference_potential_references_views($field, $options) {
- $settings = $field['settings']['view'];
- $options['title_field'] = 'title';
- return references_potential_references_view('node', $settings['view_name'], $settings['display_name'], $settings['args'], $options);
- }
- /**
- * Helper function for node_reference_potential_references().
- *
- * List of referenceable nodes defined by content types.
- */
- function _node_reference_potential_references_standard($field, $options) {
- // Avoid useless work
- if (!count($field['settings']['referenceable_types'])) {
- return array();
- }
- $query = db_select('node', 'n');
- if (!user_access('bypass node access')) {
- // If the user is able to view their own unpublished nodes, allow them to
- // see these in addition to published nodes. Check that they actually have
- // some unpublished nodes to view before adding the condition.
- 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()) {
- $query->condition(db_or()
- ->condition('n.status', NODE_PUBLISHED)
- ->condition('n.nid', $own_unpublished, 'IN')
- );
- }
- else {
- // If not, restrict the query to published nodes.
- $query->condition('n.status', NODE_PUBLISHED);
- }
- $query->addTag('node_access');
- }
- $node_nid_alias = $query->addField('n', 'nid');
- $node_title_alias = $query->addField('n', 'title', 'node_title');
- $node_type_alias = $query->addField('n', 'type', 'node_type');
- $query->addMetaData('id', ' _node_reference_potential_references_standard')
- ->addMetaData('field', $field)
- ->addMetaData('options', $options);
- if (is_array($field['settings']['referenceable_types'])) {
- $query->condition('n.type', $field['settings']['referenceable_types'], 'IN');
- }
- if ($options['string'] !== '') {
- switch ($options['match']) {
- case 'contains':
- $query->condition('n.title', '%' . $options['string'] . '%', 'LIKE');
- break;
- case 'starts_with':
- $query->condition('n.title', $options['string'] . '%', 'LIKE');
- break;
- case 'fuzzy':
- $words = explode(' ', $options['string']);
- foreach ($words as $word) {
- $query->condition('n.title', '%' . $word . '%', 'LIKE');
- }
- break;
- case 'equals':
- default: // no match type or incorrect match type: use "="
- $query->condition('n.title', $options['string']);
- break;
- }
- }
- if ($options['ids']) {
- $query->condition('n.nid', $options['ids'], 'IN');
- }
- if ($options['limit']) {
- $query->range(0, $options['limit']);
- }
- $query
- ->orderBy($node_title_alias)
- ->orderBy($node_type_alias);
- $result = $query->execute()->fetchAll();
- $references = array();
- foreach ($result as $node) {
- $references[$node->nid] = array(
- 'title' => $node->node_title,
- 'rendered' => check_plain($node->node_title),
- );
- }
- return $references;
- }
- /**
- * Menu callback for the autocomplete results.
- */
- function node_reference_autocomplete($entity_type, $bundle, $field_name, $string = '') {
- $instance = field_info_instance($entity_type, $field_name, $bundle);
- $field = field_info_field($field_name);
- $options = array(
- 'string' => $string,
- 'match' => $instance['widget']['settings']['autocomplete_match'],
- 'limit' => 10,
- );
- $references = node_reference_potential_references($field, $options);
- $matches = array();
- foreach ($references as $id => $row) {
- // Markup is fine in autocompletion results (might happen when rendered
- // through Views) but we want to remove hyperlinks.
- $suggestion = preg_replace('/<a href="([^<]*)">([^<]*)<\/a>/', '$2', $row['rendered']);
- // Add a class wrapper for a few required CSS overrides.
- $matches[$row['title'] . " [nid:$id]"] = '<div class="reference-autocomplete">' . $suggestion . '</div>';
- }
- drupal_json_output($matches);
- }
- /**
- * Implements hook_node_type_update().
- *
- * Reflect type name changes to the 'referenceable types' settings: when
- * the name of a type changes, the change needs to be reflected in the
- * "referenceable types" setting for any node_reference field
- * referencing it.
- */
- function node_reference_node_type_update($info) {
- if (!empty($info->old_type) && $info->old_type != $info->type) {
- $fields = field_info_fields();
- foreach ($fields as $field_name => $field) {
- if ($field['type'] == 'node_reference' && isset($field['settings']['referenceable_types'][$info->old_type])) {
- $field['settings']['referenceable_types'][$info->type] = empty($field['settings']['referenceable_types'][$info->old_type]) ? 0 : $info->type;
- unset($field['settings']['referenceable_types'][$info->old_type]);
- field_update_field($field);
- }
- }
- }
- }
- /**
- * Theme preprocess function.
- *
- * Allows specific node templates for nodes displayed as values of a
- * node_reference field with a specific view mode.
- */
- function node_reference_preprocess_node(&$vars) {
- // The 'referencing_field' attribute of the node is added by the
- // node_reference_node mode formatter (display referenced node
- // in a specific view mode).
- if (!empty($vars['node']->referencing_field)) {
- $node = $vars['node'];
- $field_name = $node->referencing_field;
- $vars['theme_hook_suggestions'][] = 'node__node_reference';
- $vars['theme_hook_suggestions'][] = 'node__node_reference__' . $field_name;
- $vars['theme_hook_suggestions'][] = 'node__node_reference__' . $node->type;
- $vars['theme_hook_suggestions'][] = 'node__node_reference__' . $field_name . '__' . $node->type;
- }
- }
- /**
- * Implements hook_field_prepare_translation().
- *
- * When preparing a translation, load any translations of existing
- * references.
- */
- function node_reference_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) {
- if (isset($items) && is_array($items)) {
- // Match each reference with its matching translation, if it exists.
- foreach ($items as $key => $item) {
- $reference_node = node_load($item['nid']);
- $items[$key]['nid'] = node_reference_find_translation($reference_node, $entity->language);
- }
- }
- }
- /**
- * Find a translation for a specific node reference, if it exists.
- *
- * @param $reference_node
- * The untranslated node reference.
- * @param $langcode
- *
- * @return
- * A nid for the translation of the node reference,
- * otherwise the original untranslated nid if no translation exists.
- */
- function node_reference_find_translation($reference_node, $langcode) {
- // Check if the source node translation is set and if translations are supported.
- if (isset($reference_node->tnid) && translation_supported_type($reference_node->type)) {
- // Determine whether an alternative language is being used.
- if (!empty($reference_node->language) && $reference_node->language != $langcode) {
- // Return a corresponding translation nid for the reference (if it exists).
- $translations = translation_node_get_translations($reference_node->tnid);
- if (isset($translations[$langcode])) {
- return $translations[$langcode]->nid;
- }
- }
- }
- // Return the untranslated reference nid, no matching translations found.
- return $reference_node->nid;
- }
- /**
- * Implements hook_options_list().
- */
- function node_reference_options_list($field) {
- return _node_reference_options($field, FALSE);
- }
- /**
- * Implements hook_content_migrate_field_alter().
- *
- * Use this to tweak the conversion of field settings from the D6 style to the
- * D7 style for specific situations not handled by basic conversion, as when
- * field types or settings are changed.
- *
- * $field_value['widget_type'] is available to
- * see what widget type was originally used.
- */
- function node_reference_content_migrate_field_alter(&$field_value, $instance_value) {
- switch ($field_value['module']) {
- case 'nodereference':
- $field_value['module'] = 'node_reference';
- $field_value['type'] = 'node_reference';
- // Translate 'view' settings.
- $view_name = isset($field_value['settings']['advanced_view']) ? $field_value['settings']['advanced_view'] : '';
- $view_args = isset($field_value['settings']['advanced_view_args']) ? $field_value['settings']['advanced_view_args'] : '';
- $view_args = array_map('trim', explode(',', $view_args));
- $field_value['settings']['view'] = array(
- 'view_name' => $view_name,
- 'display_name' => 'default',
- 'args' => $view_args,
- );
- if ($view_name) {
- $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));
- }
- unset($field_value['settings']['advanced_view']);
- unset($field_value['settings']['advanced_view_args']);
- break;
- }
- }
- /**
- * Implements hook_content_migrate_instance_alter().
- *
- * Use this to tweak the conversion of instance or widget settings from the D6
- * style to the D7 style for specific situations not handled by basic
- * conversion, as when formatter or widget names or settings are changed.
- */
- function node_reference_content_migrate_instance_alter(&$instance_value, $field_value) {
- switch ($field_value['type']) {
- case 'nodereference':
- // Massage formatters.
- foreach ($instance_value['display'] as $context => &$display) {
- switch ($display['type']) {
- case 'full':
- case 'teaser':
- // Those two formatters have been merged into
- // 'node_reference_view_mode', with a formatter setting.
- $display['type'] = 'node_reference_node';
- $display['settings']['node_reference_view_mode'] = $display['type'];
- break;
- default:
- // The formatter names changed, all are prefixed with
- // 'node_reference_'.
- $display['type'] = 'node_reference_' . $display['type'];
- break;
- }
- }
- // Massage the widget.
- switch ($instance_value['widget']['type']) {
- case 'nodereference_autocomplete':
- $instance_value['widget']['type'] = 'node_reference_autocomplete';
- $instance_value['widget']['module'] = 'node_reference';
- break;
- case 'nodereference_select':
- $instance_value['widget']['type'] = 'options_select';
- $instance_value['widget']['module'] = 'options';
- break;
- case 'nodereference_buttons':
- $instance_value['widget']['type'] = 'options_buttons';
- $instance_value['widget']['module'] = 'options';
- }
- break;
- }
- }
- /**
- * Implements hook_field_views_data().
- *
- * In addition to the default field information we add the relationship for
- * views to connect back to the node table.
- */
- function node_reference_field_views_data($field) {
- // No module_load_include(): this hook is invoked from
- // views/modules/field.views.inc, which is where that function is defined.
- $data = field_views_field_default_views_data($field);
- $storage = $field['storage']['details']['sql'];
- foreach ($storage as $age => $table_data) {
- $table = key($table_data);
- $columns = current($table_data);
- $id_column = $columns['nid'];
- if (isset($data[$table])) {
- // Filter: swap the handler to the 'in' operator. The callback receives
- // the field name instead of the whole $field structure to keep views
- // data to a reasonable size.
- $data[$table][$id_column]['filter']['handler'] = 'views_handler_filter_in_operator';
- $data[$table][$id_column]['filter']['options callback'] = 'node_reference_views_filter_options';
- $data[$table][$id_column]['filter']['options arguments'] = array($field['field_name']);
- // Argument: display node.title in argument titles (handled in our custom
- // handler) and summary lists (handled by the base views_handler_argument
- // handler).
- // Both mechanisms rely on the 'name table' and 'name field' information
- // below, by joining to a separate copy of the base table from the field
- // data table.
- $data[$table][$id_column]['argument']['handler'] = 'references_handler_argument';
- $data[$table][$id_column]['argument']['name table'] = $table . '_reference';
- $data[$table][$id_column]['argument']['name field'] = 'title';
- $data[$table . '_reference']['table']['join'][$table] = array(
- 'left_field' => $id_column,
- 'table' => 'node',
- 'field' => 'nid',
- );
- // Relationship.
- $data[$table][$id_column]['relationship'] = array(
- 'handler' => 'references_handler_relationship',
- 'base' => 'node',
- 'base field' => 'nid',
- 'field' => $id_column,
- 'label' => $field['field_name'],
- 'field_name' => $field['field_name'],
- );
- }
- }
- return $data;
- }
- /**
- * Implements hook_field_views_data_views_data_alter().
- */
- function node_reference_field_views_data_views_data_alter(&$data, $field) {
- foreach ($field['bundles'] as $entity_type => $bundles) {
- $entity_info = entity_get_info($entity_type);
- $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type;
- list($label, $all_labels) = field_views_field_label($field['field_name']);
- $entity = $entity_info['label'];
- if ($entity == t('Node')) {
- $entity = t('Content');
- }
- // Only specify target entity type if the field is used in more than one.
- if (count($field['bundles']) > 1) {
- $title = t('@field (@field_name) - reverse (to @entity)', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name']));
- }
- else {
- $title = t('@field (@field_name) - reverse', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name']));
- }
- $data['node'][$pseudo_field_name]['relationship'] = array(
- 'title' => $title,
- 'help' => t('Relate each @entity referencing the node through @field.', array('@entity' => $entity, '@field' => $label)),
- 'handler' => 'views_handler_relationship_entity_reverse',
- 'field_name' => $field['field_name'],
- 'field table' => _field_sql_storage_tablename($field),
- 'field field' => $field['field_name'] . '_nid',
- 'base' => $entity_info['base table'],
- 'base field' => $entity_info['entity keys']['id'],
- 'label' => t('!field_name', array('!field_name' => $field['field_name'])),
- );
- }
- }
- /**
- * 'options callback' for the views_handler_filter_in_operator filter.
- *
- * @param $field_name
- * The field name.
- */
- function node_reference_views_filter_options($field_name) {
- $options = array();
- if ($field = field_info_field($field_name)) {
- $options = _node_reference_options($field, TRUE);
- // The options are displayed in checkboxes within the filter admin form, and
- // in a select within an exposed filter. Checkboxes accept HTML, other
- // entities should be encoded; selects require the exact opposite: no HTML,
- // no encoding. We go for a middle ground: strip tags, leave entities
- // unencoded.
- foreach ($options as $key => $value) {
- $options[$key] = strip_tags($value);
- }
- }
- return $options;
- }
|