You are here

node_reference.module in References 7.2

Defines a field type for referencing one node from another.

File

node_reference/node_reference.module
View source
<?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()),
      // node_reference_autocomplete',
      'default_widget' => 'options_select',
      '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();
  if (is_array($items)) {
    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',
      ))
        ->condition('n.nid', $ids_to_check, 'IN');

      // Unless the user has the right permissions, restrict on the node status.
      // 'view_unpublished' contrib module and the
      // 'view all unpublished content'
      // permission is provided by the 'workbench_moderation' contrib module.)
      if (!user_access('bypass node access') && !user_access('view any unpublished content') && !user_access('view all 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'],
      );
      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) {

        // Checking item 'access' before the use.
        if (!empty($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) {

        // Checking item 'access' before the use.
        if (!empty($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 (!empty($item['access'])) {

          // Checking if exists the node. If not exists use node_load to load.
          if (!empty($item['node'])) {
            $node = $item['node'];
          }
          else {
            $node = node_load($item['nid']);
          }
          $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 (!empty($item['access']) && !isset($recursion_queue[$item['nid']])) {
          if (!empty($item['node'])) {
            $nodes_display[$item['nid']] = $item['node'];
          }
          else {
            $nodes_display[$item['nid']] = node_load($item['nid']);
          }
        }
      }

      // 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 (!empty($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 (!empty($item['access'])) {
          $result[$delta] = array(
            '#markup' => $item['nid'],
          );
        }
      }
      break;
    case 'node_reference_path':
      foreach ($items as $delta => $item) {
        if (!empty($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',
        'limit' => 10,
        '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['limit'] = array(
      '#type' => 'select',
      '#title' => t('Dropdown limit'),
      '#options' => drupal_map_assoc(array(
        5,
        10,
        15,
        20,
        25,
        30,
        35,
        40,
        45,
        50,
      )),
      '#default_value' => $settings['limit'],
      '#required' => TRUE,
      '#description' => t('Maximum number of matched dropdown items displayed on the form. Overrides limit setting of the view if used.'),
    );
    $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.
 *
 * @codingStandardsIgnoreStart
 */
function node_reference_autocomplete_value($element, $input = FALSE, $form_state) {

  // @codingStandardsIgnoreEnd
  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');
      $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(, , $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']);
}

/**
 * Node Reference Options.
 *
 * 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 array $field
 *   The field definition.
 * @param bool $flat
 *   Whether optgroups are allowed.
 *
 * @return array
 *   An array of referenceable node titles, keyed by node id. If the $flat
 *   parameter is TRUE, the list might be nested by optgroup first.
 *
 * @codingStandardsIgnoreStart
 */
function _node_reference_options($field, $flat = TRUE) {

  // @codingStandardsIgnoreEnd
  $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 array $field
 *   The field definition.
 * @param array $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 array
 *   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)
 *     ),
 *     ...
 *   )
 *
 * @codingStandardsIgnoreStart
 */
function node_reference_potential_references($field, $options = array()) {

  // @codingStandardsIgnoreEnd
  // Fill in default options.
  $options += array(
    'string' => '',
    'match' => 'contains',
    'ids' => array(),
    'limit' => 25,
  );
  $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');
  }
  $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':

      // No match type or incorrect match type: use "=".
      default:
        $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' => $instance['widget']['settings']['limit'],
  );
  $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) {
      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 object $reference_node
 *   The untranslated node reference.
 * @param string $langcode
 *   String with lang code.
 *
 * @return int
 *   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 &$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 = array();
  if (isset($field['storage']['details']['sql'])) {
    $storage = $field['storage']['details']['sql'];
  }
  foreach ($storage as $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) = 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'],
      )),
      'join_extra' => array(
        0 => array(
          'field' => 'entity_type',
          'value' => $entity_type,
        ),
        1 => array(
          'field' => 'deleted',
          'value' => 0,
          'numeric' => TRUE,
        ),
      ),
    );
  }
}

/**
 * Options: 'options callback' for the views_handler_filter_in_operator filter.
 *
 * @param string $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;
}

/**
 * Returns nodes of the given type(s) that are referred to in a given node.
 *
 * @param object $node
 *   The node object from which the references should be retrieved.
 * @param array $types
 *   An array listing the desired content types. If omitted all referenced nodes
 *   are returned, regardless of type.
 *
 * @return array
 *   An associative array of nodes of the given types that are referred to in
 *   the given node, keyed by nid.
 */
function node_reference_get_referred_nodes($node, array $types = array()) {
  $nodes = array();
  if (!isset($node->type)) {
    return $nodes;
  }
  foreach (field_info_fields() as $field_name => $field_info) {
    if ($field_info['type'] == 'node_reference' && (empty($types) || array_intersect($types, $field_info['settings']['referenceable_types'])) && array_intersect(array(
      $node->type,
    ), $field_info['bundles']['node'])) {
      $field_items = field_get_items('node', $node, $field_name);
      if (is_array($field_items) && !empty($field_items)) {
        foreach ($field_items as $field_item) {
          if (($referred_node = node_load($field_item['nid'])) && (empty($types) || in_array($referred_node->type, $types))) {
            $nodes[$field_item['nid']] = $referred_node;
          }
        }
      }
    }
  }
  return $nodes;
}

Functions

Namesort descending Description
node_reference_autocomplete Menu callback for the autocomplete results.
node_reference_autocomplete_validate Validation callback for a node_reference autocomplete element.
node_reference_autocomplete_value Value callback for a node_reference autocomplete element.
node_reference_content_migrate_field_alter Implements hook_content_migrate_field_alter().
node_reference_content_migrate_instance_alter Implements hook_content_migrate_instance_alter().
node_reference_field_formatter_info Implements hook_field_formatter_info().
node_reference_field_formatter_prepare_view Implements hook_field_formatter_prepare_view().
node_reference_field_formatter_settings_form Implements hook_field_formatter_settings_form().
node_reference_field_formatter_settings_summary Implements hook_field_formatter_settings_summary().
node_reference_field_formatter_view Implements hook_field_formatter_view().
node_reference_field_info Implements hook_field_info().
node_reference_field_is_empty Implements hook_field_is_empty().
node_reference_field_prepare_translation Implements hook_field_prepare_translation().
node_reference_field_prepare_view Implements hook_field_prepare_view().
node_reference_field_settings_form Implements hook_field_settings_form().
node_reference_field_validate Implements hook_field_validate().
node_reference_field_views_data Implements hook_field_views_data().
node_reference_field_views_data_views_data_alter Implements hook_field_views_data_views_data_alter().
node_reference_field_widget_error Implements hook_field_widget_error().
node_reference_field_widget_form Implements hook_field_widget_form().
node_reference_field_widget_info Implements hook_field_widget_info().
node_reference_field_widget_info_alter Implements hook_field_widget_info_alter().
node_reference_field_widget_settings_form Implements hook_field_widget_settings_form().
node_reference_find_translation Find a translation for a specific node reference, if it exists.
node_reference_get_referred_nodes Returns nodes of the given type(s) that are referred to in a given node.
node_reference_menu Implements hook_menu().
node_reference_node_type_update Implements hook_node_type_update().
node_reference_options_list Implements hook_options_list().
node_reference_potential_references Retrieves an array of candidate referenceable nodes.
node_reference_preprocess_node Theme preprocess function.
node_reference_views_filter_options Options: 'options callback' for the views_handler_filter_in_operator filter.
_node_reference_options Node Reference Options.
_node_reference_potential_references_standard Helper function for node_reference_potential_references().
_node_reference_potential_references_views Helper function for node_reference_potential_references().
_node_reference_view_settings_validate Validate callback for the 'view settings' fieldset.