You are here

uniqueness.module in Uniqueness 7

Same filename and directory in other branches
  1. 6 uniqueness.module

File

uniqueness.module
View source
<?php

/**
 * @file uniqueness.module
 */
define('UNIQUENESS_WIDGET_INLINE', 0x1);
define('UNIQUENESS_WIDGET_BLOCK', 0x2);
define('UNIQUENESS_SEARCH_MODE_NODETITLE', 0x1);
define('UNIQUENESS_SEARCH_MODE_DRUPAL', 0x2);
define('UNIQUENESS_SEARCH_MODE_SOLR', 0x3);
define('UNIQUENESS_ADD_NODE', 0x1);
define('UNIQUENESS_EDIT_NODE', 0x2);
define('UNIQUENESS_SCOPE_ALL', 0x1);
define('UNIQUENESS_SCOPE_CONTENT_TYPE', 0x2);

/**
 * Implements hook_menu().
 */
function uniqueness_menu() {
  $items['uniqueness-search'] = array(
    'title' => 'Uniqueness search',
    'page callback' => 'uniqueness_dynamic_callback',
    'access arguments' => array(
      'use uniqueness widget',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/content/uniqueness'] = array(
    'title' => 'Uniqueness settings',
    'description' => 'Configure the behaviour and appearance of the uniqueness widget.',
    'file' => 'uniqueness.admin.inc',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uniqueness_settings',
    ),
    'access arguments' => array(
      'administer uniqueness',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function uniqueness_permission() {
  return array(
    'use uniqueness widget' => array(
      'title' => t('Use uniqueness widget'),
      'description' => t('Display the uniqueness widget during content authoring.'),
    ),
    'administer uniqueness' => array(
      'title' => t('Administer uniqueness settings'),
      'description' => t('Configure the uniqueness module.'),
    ),
  );
}

/**
 * Implements hook_help().
 */
function uniqueness_help($path, $arg) {
  $output = '<p>' . t('The Uniqueness module helps you avoid duplicate content on your site by informing users of similar or related content as they create <em>new</em> or edit <em>existing</em> content.');
  switch ($path) {
    case 'admin/config/content/uniqueness':
      $output .= ' ' . l(t('Learn more'), 'admin/help/uniqueness');
      $output .= '</p>';
      return $output;
    case 'admin/help#uniqueness':
      $output .= '</p>';
      $output .= '<p>' . t('A block and/or in-line user interface element is added to the content adding and/or editing form.') . ' ' . t('As the user types, Uniqueness searches on the title or vocabulary fields and displays a list of similar content.') . ' ' . t('To configure overall options such as search and appearance, visit the <a href="@uniqueness-settings-page">uniqueness settings</a> page. To use the in-line user interface element, enable it on each desired <a href="@content-types-page">content type</a> configuration page.', array(
        '@content-types-page' => url('admin/structure/types'),
        '@uniqueness-settings-page' => url('admin/config/content/uniqueness'),
      )) . '</p>';
      $output .= '<h3>' . t('Search modes') . '</h3>';
      $output .= '<p>' . t('The module can find related content using one of three possible methods:') . '</p>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Simple node title search (default)') . '</dt>' . '<dd>' . t('Matches the title of a new node by comparing the new title with the title of existing nodes.') . '</dd>';
      $output .= '<dt>' . t('Drupal search') . '</dt>' . '<dd>' . t('Searches for content using the standard search module. Requires the core search module to be enabled.') . '</dd>';
      $output .= '<dt>' . t('Apache Solr search') . '</dt>' . '<dd>' . t('Searches using the <a href="@apachesolr-project">Apache Solr</a> module. Apache Solr must be to be installed, enabled and configured.', array(
        '@apachesolr-project' => url('http://drupal.org/project/apachesolr'),
      )) . '</dd>';
      $output .= '</dl>';
      return $output;
  }
}

/**
 * Implements hook_field_extra_fields().
 *
 * Allows the inline uniqueness field to be sorted on the manage fields page for
 * any content type for which it is enabled.
 */
function uniqueness_field_extra_fields() {
  $extras = array();
  if (in_array(UNIQUENESS_WIDGET_INLINE, variable_get('uniqueness_widgets', array(
    UNIQUENESS_WIDGET_INLINE,
  )))) {

    // The inline widget is enabled
    foreach (array_keys(node_type_get_types()) as $type) {
      $v = variable_get('uniqueness_type_' . $type, array());
      if (!empty($v)) {
        $extras['node'][$type] = array(
          'form' => array(
            'uniqueness' => array(
              'label' => filter_xss_admin(variable_get('uniqueness_default_title', t('Related content'))),
              'description' => t('Uniqueness inline widget.'),
              'weight' => 0,
            ),
          ),
        );
      }
    }
  }
  return $extras;
}

/**
 * Implements hook_form_FORMID_alter().
 *
 * Adds two configuration options to each content type configuration form.
 */
function uniqueness_form_node_type_form_alter(&$form, $form_state, $form_id) {
  $form['uniqueness'] = array(
    '#type' => 'fieldset',
    '#title' => t('Uniqueness settings'),
    '#group' => 'additional_settings',
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['uniqueness']['uniqueness_type'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Provide uniqueness search'),
    '#options' => array(
      UNIQUENESS_ADD_NODE => t('When adding new content of this content type'),
      UNIQUENESS_EDIT_NODE => t('When editing an exist content of this content type'),
    ),
    '#description' => t('Shows similar content when adding or editing content to help avoid duplication.'),
    '#default_value' => variable_get('uniqueness_type_' . $form['#node_type']->type, array()),
  );
}

/**
 * Implements hook_form_alter().
 *
 * Adds the in-line widget to the node add/edit form.
 */
function uniqueness_form_alter(&$form, $form_state, $form_id) {
  if (!empty($form['#node_edit_form'])) {
    $type = $form['type']['#value'];
    $op = empty($form['#node']->nid) ? UNIQUENESS_ADD_NODE : UNIQUENESS_EDIT_NODE;

    // Save the content type and operation for later use
    _uniqueness_widget_store(array(
      'type' => $type,
      'op' => $op,
    ));
    if (user_access('use uniqueness widget') && in_array($op, variable_get('uniqueness_type_' . $type, array()))) {

      // Add our javascript.
      $form['uniqueness']['#attached']['js'] = array(
        drupal_get_path('module', 'uniqueness') . '/uniqueness.js',
        array(
          'data' => array(
            'uniqueness' => _uniqueness_get_js_settings($type, $form['nid']['#value']),
          ),
          'type' => 'setting',
        ),
      );

      // Save relevant form values in a temporary store, so that we can generate
      // the list of related content right away for node previews or when
      // editing existing nodes. (The store is needed because we cannot pass
      // data directly to a block from within this function.)
      $values = array();

      // Store the node id.
      if (!empty($form['nid']['#value'])) {
        $values['nid'] = $form['nid']['#value'];
      }

      // Store the title.
      if (!empty($form['title'])) {
        $values['title'] = strip_tags($form['title']['#default_value']);
      }

      // Store the tags.
      if (!empty($values['taxonomy']['tags'][1])) {
        $values['tags'] = strip_tags($form['taxonomy']['tags'][1]['#default_value']);
      }
      _uniqueness_store($values);

      // Embed inline widget if enabled.
      if (uniqueness_widget_enabled(UNIQUENESS_WIDGET_INLINE)) {

        // Get rendered list of results.
        $count = 0;
        $content = uniqueness_widget_content($count);

        // Only add the inline uniqueness widget if it has not been defined
        // already. This makes it possible for other modules to customize the
        // form element in their own hook_form_alter() implementation.
        $form['uniqueness'] += array(
          '#type' => 'fieldset',
          '#title' => filter_xss_admin(variable_get('uniqueness_default_title', t('Related content'))),
          '#collapsible' => 1,
          '#collapsed' => $count == 0,
          '#weight' => $form['title']['#weight'] + 1,
          // Place underneath title.
          'uniqueness_type' => array(),
        );
        $form['uniqueness']['uniqueness_type'] += array(
          '#type' => 'item',
          '#title' => '',
          '#markup' => $content,
        );
      }
    }
  }
}

/**
 * Helper function checks whether a widget is enabled.
 *
 * @param $widget
 *   Widget identifier, either UNIQUENESS_WIDGET_INLINE or
 *   UNIQUENESS_WIDGET_BLOCK.
 *
 * @return
 *   TRUE if the widget is enabled, FALSE otherwise.
 */
function uniqueness_widget_enabled($widget) {
  $widget_types = variable_get('uniqueness_widgets', array(
    UNIQUENESS_WIDGET_INLINE,
  ));
  return in_array($widget, $widget_types);
}

/**
 * Helper function saves and returns the current content type and operation.
 *
 * Needed because both the content type and nid of the node are not known when
 * the block is built.
 *
 * @param $options
 *   If set, saves this array.
 *
 * @return
 *   The previously saved array, or NULL.
 */
function _uniqueness_widget_store($options = NULL) {
  static $saved_options = NULL;
  if ($options !== NULL) {
    $saved_options = $options;
  }
  return $saved_options;
}

/**
 * Returns the jQuery settings required for the uniqueness search.
 */
function _uniqueness_get_js_settings($type, $nid) {
  $minCharacters = variable_get('uniqueness_query_min', 3);
  if (variable_get('uniqueness_search_mode', UNIQUENESS_SEARCH_MODE_NODETITLE) == UNIQUENESS_SEARCH_MODE_DRUPAL && $minCharacters < variable_get('minimum_word_size', 3)) {
    $minCharacters = variable_get('minimum_word_size', 3);
  }
  $settings = array(
    'URL' => base_path() . 'uniqueness-search/' . $type,
    'prependResults' => variable_get('uniqueness_results_prepend', 0) == 1 ? TRUE : FALSE,
    'minCharacters' => $minCharacters,
    'searchingString' => filter_xss_admin(variable_get('uniqueness_searching_string', t('Searching&hellip;'))),
    'noResultsString' => filter_xss_admin(variable_get('uniqueness_no_result_string', t('Success! No related content found.'))),
  );
  if (variable_get('uniqueness_scope', UNIQUENESS_SCOPE_CONTENT_TYPE) == UNIQUENESS_SCOPE_CONTENT_TYPE) {
    $settings['type'] = $type;
  }
  if (is_numeric($nid) && $nid != 0) {
    $settings['nid'] = $nid;
  }
  return $settings;
}

/**
 * Helper function solves FAPI deficiency by storing some data statically for
 * retrieval in our block.
 */
function _uniqueness_store($values = array()) {
  static $uniqueness_store = array();
  if (!empty($values)) {
    $uniqueness_store = $values;
  }
  return $uniqueness_store;
}

/**
 * Implements hook_block_info().
 */
function uniqueness_block_info() {

  // @todo I think we can cache the block actually -- explore.
  $blocks['uniqueness'] = array(
    'info' => t('Uniqueness search'),
    'cache' => DRUPAL_NO_CACHE,
  );
  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function uniqueness_block_view($delta) {
  if (uniqueness_widget_enabled(UNIQUENESS_WIDGET_BLOCK)) {

    // We only operate on the node form for a type we search.
    $options = _uniqueness_widget_store();
    if ($options !== NULL && user_access('use uniqueness widget') && $delta == 'uniqueness' && in_array($options['op'], variable_get('uniqueness_type_' . $options['type'], array()))) {
      $block['subject'] = filter_xss_admin(variable_get('uniqueness_default_title', t('Related content')));
      $block['content'] = uniqueness_widget_content($count);
      return $block;
    }
  }
}

/**
 * Returns the rendered related content.
 *
 * @param $count
 *   Returns the number of related content items that were found.
 * @return
 *   The rendered HTML with content as specified by the data in
 *   _uniqueness_store().
 */
function uniqueness_widget_content(&$count) {
  $results = array();
  $items = array();
  $description = filter_xss_admin(variable_get('uniqueness_default_description', t("Help us avoid duplicate content! If we find content that's related or similar to what you're posting it will be listed here.")));
  $values = _uniqueness_store();
  if (!empty($values)) {
    $content = uniqueness_content($values);
    foreach ($content as $nid => $item) {

      // Avoid duplicates.
      if (!in_array($nid, array_keys($items))) {
        $items[$nid] = $item;
        $options = array(
          'attributes' => array(
            'target' => '_blank',
          ),
        );
        $results[] = l($item['title'], 'node/' . $item['nid'], $options);
      }
    }
  }
  $count = count($items);

  // Pass the description and any initial results through the theme system.
  return theme('uniqueness_widget', array(
    'description' => $description,
    'results' => $results,
  ));
}

/**
 * Callback for uniqueness-search returns HTML stubs for related content.
 */
function uniqueness_dynamic_callback() {

  // Build $values from $string.
  $values = array();

  // @todo refer to tags as 'terms'
  foreach (array(
    'tags',
    'title',
    'nid',
    'type',
  ) as $key) {
    if (isset($_GET[$key])) {
      $values[$key] = strip_tags($_GET[$key]);
    }
  }
  if (!empty($values)) {
    $related_content = uniqueness_content($values);
  }
  if (!empty($related_content)) {
    $items = array();
    $i = 0;
    $limit = variable_get('uniqueness_results_max', 10);
    foreach ($related_content as $nid => $item) {

      // Build items and avoid duplicates.
      if (!in_array($nid, array_keys($items))) {
        $items[$nid] = $item;
        $items[$nid]['href'] = url('node/' . $nid);
      }
      if (++$i > $limit) {

        // At least one more than the results_max limit were found
        // -> communicate "and more" to the widget
        $items[$nid]['more'] = TRUE;
        break;

        // LOOP EXIT
      }
    }
    drupal_json_output($items);
  }
  else {
    drupal_json_output('false');
  }
}

/**
 * Perform lookup of related or similar content.
 */
function uniqueness_content($values) {
  $search_mode = variable_get('uniqueness_search_mode', UNIQUENESS_SEARCH_MODE_NODETITLE);
  switch ($search_mode) {
    case UNIQUENESS_SEARCH_MODE_NODETITLE:
      return _uniqueness_content_nodetitle($values);
    case UNIQUENESS_SEARCH_MODE_DRUPAL:
      return _uniqueness_content_drupalsearch($values);
    case UNIQUENESS_SEARCH_MODE_SOLR:
      return _uniqueness_content_solr($values);
  }
}

/**
 * Searches for related content by comparing the node title with the title of
 * existing nodes.
 */
function _uniqueness_content_nodetitle($values) {
  if (empty($values['title'])) {
    return array();
  }

  // Query node table.
  $q = db_select('node', 'n')
    ->fields('n', array(
    'nid',
    'title',
    'status',
  ))
    ->condition('n.title', '%' . db_like($values['title']) . '%', 'LIKE');
  if (isset($values['type'])) {
    $q
      ->condition('n.type', $values['type']);
  }
  if (isset($values['nid']) && is_numeric($values['nid'])) {
    $q
      ->condition('n.nid', $values['nid'], '<>');
  }

  // If user is not allowed to bypass node access, restrict access to
  // unpublished content.
  if (!user_access('bypass node access')) {
    if (user_access('view own unpublished content')) {
      $q
        ->condition(db_or()
        ->condition('n.status', NODE_PUBLISHED)
        ->condition('n.uid', $GLOBALS['user']->uid));
    }
    else {
      $q
        ->condition('n.status', NODE_PUBLISHED);
    }
  }

  // Range is +1 for "... and more".
  $q
    ->range(0, variable_get('uniqueness_results_max', 10) + 1);

  // Respect node access.
  $q
    ->addTag('node_access');
  return $q
    ->execute()
    ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
}

/**
 * Searches for related content using the drupal core search module.
 */
function _uniqueness_content_drupalsearch($values) {

  // Check that the core search module is available
  if (!module_exists('search')) {
    drupal_set_message(t('Search module not found. Please enable the search module or select a different search mode on the uniqueness configuration page.'), 'warning');
    return array();
  }

  // build search string
  $searchstring = array_key_exists('type', $values) ? ' type:' . $values['type'] . ' ' : '';
  $searchstring .= join(' OR ', explode(' ', $values['title']));
  $search_results = node_search_execute($searchstring);

  // node_search is automatically limited to max. 10 results, but in case we
  // want even less we need to further reduce the number of results.
  $limit = variable_get('uniqueness_results_max', 10);
  if (count($search_results) > $limit + 1) {

    // +1 for "... and more"
    array_slice($search_results, 0, $limit + 1, TRUE);
  }
  $related_content = array();
  $nid = array_key_exists('nid', $values) ? $values['nid'] : 0;
  foreach ($search_results as $result) {
    $item = array();

    // Title has already been filtered.
    $item['html'] = TRUE;
    $item['nid'] = $result['node']->nid;
    $item['title'] = $result['title'];
    if ($nid != $item['nid']) {
      $related_content["{$result['node']->nid}"] = $item;
    }
  }
  return $related_content;
}

/**
 * Searches for realted content using the apachesolr module.
 */
function _uniqueness_content_solr($values) {

  // Check that the apachesolr module is available.
  if (!module_exists('apachesolr_search')) {
    drupal_set_message(t('Solr search module not found. Please select a different search mode on the uniqueness configuration page.'), 'warning');
    return array();
  }
  $related_content = array();
  $filter = array_key_exists('type', $values) ? 'type:' . $values['type'] : '';
  $nid = array_key_exists('nid', $values) ? $values['nid'] : 0;

  // Search title.
  if ($values['title']) {
    $title_content = uniqueness_solr($values['title'], $filter, $nid);
    if (!empty($title_content)) {
      $related_content = $related_content + $title_content;
    }
  }

  // Search tags.
  if (isset($values['tags'])) {
    $tags = explode(',', $values['tags']);
    foreach ($tags as $tag) {
      $results = uniqueness_solr(trim($tag), $filter, $nid);
      if (!empty($results)) {
        $related_content = $related_content + $results;
      }
    }
  }
  $limit = variable_get('uniqueness_results_max', 10);
  if (count($related_content) > $limit + 1) {

    // +1 for "... and more"
    array_slice($related_content, 0, $limit + 1, TRUE);
  }
  return $related_content;
}

/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
function uniqueness_solr($string, $filter, $nid) {
  $related_content = array();
  try {
    $solr_results = apachesolr_search_execute($string, $filter, '', '', 0, 'uniqueness');
    foreach ($solr_results as $result) {
      $item = array();

      // Title has already been filtered.
      $item['html'] = TRUE;
      $item['nid'] = $result['node']->nid;
      $item['title'] = $result['title'];
      if ($item['nid'] != $nid) {
        $related_content["{$result['node']->nid}"] = $item;
      }
    }
  } catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e
      ->getMessage())), NULL, WATCHDOG_ERROR);
  }
  return $related_content;
}

/**
 * Implements hook_apachesolr_prepare_query().
 */
function uniqueness_apachesolr_prepare_query(&$query, &$params, $caller) {
  if ($caller == 'uniqueness') {

    // Tell Solr we only have to match on one word.
    $params['mm'] = 1;
  }
}

/**
 * Implements hook_theme().
 */
function uniqueness_theme($existing, $type, $theme, $path) {
  return array(
    'uniqueness_widget' => array(
      'variables' => array(
        'description' => NULL,
        'results' => NULL,
      ),
    ),
  );
}

/**
 * Theme function for the Uniqueness widget.
 *
 * Note, some classes are required in the widget markup for Uniqueness to work.
 * These classes begin with the appropriate word, 'uniquness'. Do not remove, or
 * be sure to add them to your own theme override function.
 */
function theme_uniqueness_widget($variables) {
  $description = $variables['description'];
  $results = $variables['results'];

  // The widget title is populated by the block or in the form, if enabled.
  $output = '';

  // Add the Uniqueness description, if set.
  if (!empty($description)) {
    $output .= '<p class="uniqueness-description">' . $description . '</p>';
  }

  // A parent block element with class uniqueness-dyn is required.
  $output .= '<div class="uniqueness-dyn">';

  // An element with class uniqueness-search-notifier is also required but feel
  // free to alter the element or add additional markup. The element's content
  // is dynamically filled by Javascript.
  $output .= '<span class="uniqueness-search-notifier"></span>';

  // If $results is populated than either an existing page is being edited or
  // the page is being previewed.
  if (empty($results)) {

    // Same markup as theme_item_list, if it were to return a <ul></ul> for an
    // empty array
    $output .= '<div class="item-list"><ul></ul></div>';
  }
  else {

    // Run through theme_item_list() or overrides.
    $output .= theme('item_list', array(
      'items' => $results,
    ));
  }

  // Close parent block element.
  $output .= '</div>';
  return $output;
}

Functions

Namesort descending Description
theme_uniqueness_widget Theme function for the Uniqueness widget.
uniqueness_apachesolr_prepare_query Implements hook_apachesolr_prepare_query().
uniqueness_block_info Implements hook_block_info().
uniqueness_block_view Implements hook_block_view().
uniqueness_content Perform lookup of related or similar content.
uniqueness_dynamic_callback Callback for uniqueness-search returns HTML stubs for related content.
uniqueness_field_extra_fields Implements hook_field_extra_fields().
uniqueness_form_alter Implements hook_form_alter().
uniqueness_form_node_type_form_alter Implements hook_form_FORMID_alter().
uniqueness_help Implements hook_help().
uniqueness_menu Implements hook_menu().
uniqueness_permission Implements hook_permission().
uniqueness_solr @todo Please document this function.
uniqueness_theme Implements hook_theme().
uniqueness_widget_content Returns the rendered related content.
uniqueness_widget_enabled Helper function checks whether a widget is enabled.
_uniqueness_content_drupalsearch Searches for related content using the drupal core search module.
_uniqueness_content_nodetitle Searches for related content by comparing the node title with the title of existing nodes.
_uniqueness_content_solr Searches for realted content using the apachesolr module.
_uniqueness_get_js_settings Returns the jQuery settings required for the uniqueness search.
_uniqueness_store Helper function solves FAPI deficiency by storing some data statically for retrieval in our block.
_uniqueness_widget_store Helper function saves and returns the current content type and operation.

Constants