You are here

similar.module in Similar Entries 5

Same filename and directory in other branches
  1. 6.2 similar.module
  2. 6 similar.module
  3. 7.2 similar.module
  4. 7 similar.module

Module that shows a block listing similar entries. NOTE: Uses MySQL's FULLTEXT indexing for MyISAM tables.

Caching feature sponsored by http://xomba.com/

@author David Kent Norman http://deekayen.net/ @author Arnab Nandi http://arnab.org/

File

similar.module
View source
<?php

/**
 * @file
 * Module that shows a block listing similar entries.
 * NOTE: Uses MySQL's FULLTEXT indexing for MyISAM tables.
 *
 * Caching feature sponsored by http://xomba.com/
 *
 * @author David Kent Norman http://deekayen.net/
 * @author Arnab Nandi http://arnab.org/
 */
define('SIMILAR_CACHE_LIFETIME', 0);
define('SIMILAR_CACHE_DISABLED', 0);
define('SIMILAR_CACHE_ENABLED', 1);

/**
 * Implementation of hook_help().
 *
 * @param string $section
 */
function similar_help($section = 'admin/help#similar') {
  switch ($section) {
    case 'admin/help#similar':
      return t('<p>Lists the most similar nodes to the current node.</p>');
      break;
  }
}
function similar_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/similar',
      'title' => t('Similar Entries'),
      'description' => t('A module that displays a block with the most similar nodes to the currently viewed one, based on the title and body fields.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'similar_admin_settings',
      ),
      'access' => user_access('administer site configuration'),
    );
  }
  return $items;
}
function similar_admin_settings() {
  $form = array();
  $form['similar_cache'] = array(
    '#type' => 'fieldset',
    '#title' => t('Cache settings'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['similar_cache']['similar_cache'] = array(
    '#type' => 'radios',
    '#title' => t('Cache'),
    '#default_value' => variable_get('similar_cache', SIMILAR_CACHE_DISABLED),
    '#options' => array(
      SIMILAR_CACHE_DISABLED => t('Disabled'),
      SIMILAR_CACHE_ENABLED => t('Enabled'),
    ),
  );
  $period = drupal_map_assoc(array(
    0,
    60,
    180,
    300,
    600,
    900,
    1800,
    2700,
    3600,
    10800,
    21600,
    32400,
    43200,
    86400,
  ), 'format_interval');
  $period[0] = t('none');
  $form['similar_cache']['similar_cache_lifetime'] = array(
    '#type' => 'select',
    '#title' => t('Minimum cache lifetime'),
    '#default_value' => variable_get('similar_cache_lifetime', SIMILAR_CACHE_LIFETIME),
    '#options' => $period,
    '#description' => t('The cached block items will be forced to remain in cache at least this long before they are refreshed. Otherwise, cron jobs and node updates will clear the similar entries block cache at each occurrence. The retention policy below can be used to override this setting.'),
  );
  $form['similar_cache']['retention'] = array(
    '#type' => 'fieldset',
    '#title' => 'Retention policy',
    '#collapsible' => true,
    '#collapsed' => true,
    '#description' => t('Adjust whether or not to clear the similar entries cache when node content is updated. Remember, not setting delete to force clear cache may result in broken links for cached similar entries. This policy overrides the cache lifetime. Disabling every setting in this group will give full expiration control to the cache lifetime setting.'),
  );
  $form['similar_cache']['retention']['similar_clear_on_insert'] = array(
    '#type' => 'radios',
    '#title' => t('Force clear on insert'),
    '#default_value' => variable_get('similar_clear_on_insert', 0),
    '#options' => array(
      0 => t('Disabled'),
      1 => t('Enabled'),
    ),
  );
  $form['similar_cache']['retention']['update'] = array(
    '#type' => 'fieldset',
    '#collapsible' => false,
  );
  $form['similar_cache']['retention']['update']['similar_clear_on_update'] = array(
    '#type' => 'radios',
    '#title' => t('Force clear on update'),
    '#default_value' => variable_get('similar_clear_on_update', 0),
    '#options' => array(
      0 => t('Disabled'),
      1 => t('Enabled'),
    ),
  );
  $form['similar_cache']['retention']['update']['similar_clear_node_only'] = array(
    '#type' => 'radios',
    '#title' => t('Clear node only'),
    '#default_value' => variable_get('similar_clear_node_only', 1),
    '#options' => array(
      0 => t('Disabled'),
      1 => t('Enabled'),
    ),
    '#description' => t('Disabled will clear all the cached similar blocks on the system. Enabled will only clear the similar entries block cache shown on the updated node.'),
  );
  $form['similar_cache']['retention']['similar_clear_on_delete'] = array(
    '#type' => 'radios',
    '#title' => t('Force clear on deletion'),
    '#default_value' => variable_get('similar_clear_on_delete', 1),
    '#options' => array(
      0 => t('Disabled'),
      1 => t('Enabled'),
    ),
  );
  return system_settings_form($form);
}

/**
 * Perform forced cache clearing
 */
function similar_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'insert':
      if (variable_get('similar_clear_on_insert', 0)) {
        cache_clear_all('similar_block_0:', 'cache', TRUE);
      }
      break;
    case 'update':
      if (variable_get('similar_clear_on_update', 0)) {
        if (variable_get('similar_clear_node_only', 1)) {
          cache_clear_all("similar_block_0:{$node->nid}", 'cache');
        }
        else {
          cache_clear_all('similar_block_0:', 'cache', TRUE);
        }
      }
      break;
    case 'delete':
      if (variable_get('similar_clear_on_delete', 1)) {
        cache_clear_all('similar_block_0:', 'cache', TRUE);
      }
      else {
        cache_clear_all("similar_block_0:{$node->nid}", 'cache');
      }
      break;
  }
}

/**
 * Implementation of hook_block().
 *
 * This hook both declares to Drupal what blocks are provided by the module, and
 * generates the contents of the blocks themselves.
 *
 * @param string $op
 * @param integer $delta
 * @param array $edit
 */
function similar_block($op = 'list', $delta = 0, $edit = array()) {
  switch ($op) {
    case 'list':
      $blocks[0]['info'] = t('Similar entries');
      return $blocks;
    case 'configure':
      $form = array();
      if ($delta == 0) {
        $form['similar_teaser_enabled'] = array(
          '#type' => 'radios',
          '#title' => t('Include teaser text'),
          '#default_value' => variable_get('similar_teaser_enabled', 0),
          '#options' => array(
            t('No'),
            t('Yes'),
          ),
        );
        $form['similar_rel_nofollow'] = array(
          '#type' => 'radios',
          '#title' => t('Block search engines'),
          '#description' => t('Adds rel="nofollow" to the HTML source of similar links so search engines won\'t count similar links in their ranking calculations.'),
          '#default_value' => variable_get('similar_rel_nofollow', 0),
          '#options' => array(
            t('No'),
            t('Yes'),
          ),
        );
        for ($i = 1, $options = array(); $i < 101; $options[$i] = $i, $i += 1) {
        }
        $form['similar_num_display'] = array(
          '#type' => 'select',
          '#title' => t('Number of similar entries to find'),
          '#default_value' => variable_get('similar_num_display', 5),
          '#options' => $options,
        );
        $types = _similar_published_node_types();
        $form['similar_node_types'] = array(
          '#type' => 'checkboxes',
          '#multiple' => true,
          '#title' => t('Node types to display'),
          '#default_value' => variable_get('similar_node_types', $types),
          '#options' => $types,
        );
        if (module_exists('taxonomy')) {
          $names = _similar_taxonomy_names();
          $form['similar_taxonomy'] = array(
            '#type' => 'fieldset',
            '#title' => t('Taxonomy category filter'),
            '#collapsible' => TRUE,
            '#collapsed' => TRUE,
          );
          $form['similar_taxonomy']['similar_taxonomy_filter'] = array(
            '#type' => 'radios',
            '#title' => t('Filter by taxonomy categories'),
            '#default_value' => variable_get('similar_taxonomy_filter', 0),
            '#options' => array(
              t('No category filtering'),
              t('Only show the similar nodes in the same category as the original node'),
              t('Use global category filtering'),
            ),
            '#description' => t('By selecting global filtering, only nodes assigned to the following selected categories will display as similar nodes, regardless of the categories the original node is or is not assigned to.'),
          );
          $form['similar_taxonomy']['similar_taxonomy_select'] = array(
            '#type' => 'fieldset',
            '#title' => t('Taxonomy categories to display'),
            '#collapsible' => TRUE,
            '#collapsed' => TRUE,
          );
          $form['similar_taxonomy']['similar_taxonomy_select']['similar_taxonomy_tids'] = array(
            '#type' => 'select',
            '#default_value' => variable_get('similar_taxonomy_tids', array_keys($names)),
            '#description' => t('Hold the CTRL key to (de)select multiple options.'),
            '#options' => $names,
            '#multiple' => TRUE,
          );
          $vocs = _similar_taxonomy_get_vocabularies();
          $form['similar_taxonomy']['similar_taxonomy_exclude'] = array(
            '#type' => 'select',
            '#multiple' => TRUE,
            '#title' => t('Exclude vocabularies'),
            '#default_value' => variable_get('similar_taxonomy_exclude', ''),
            '#options' => $vocs,
          );
        }
      }
      return $form;
    case 'save':
      if ($delta == 0) {
        variable_set('similar_teaser_enabled', $edit['similar_teaser_enabled']);
        variable_set('similar_rel_nofollow', $edit['similar_rel_nofollow']);
        variable_set('similar_num_display', $edit['similar_num_display']);
        variable_set('similar_node_types', $edit['similar_node_types']);
        if (module_exists('taxonomy')) {
          variable_set('similar_taxonomy_filter', $edit['similar_taxonomy_filter']);
          variable_set('similar_taxonomy_tids', $edit['similar_taxonomy_tids']);
          variable_set('similar_taxonomy_exclude', $edit['similar_taxonomy_exclude']);
        }
      }
      return;
    case 'view':
    default:
      if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) != 'edit') {
        $node = node_load(array(
          'nid' => arg(1),
        ));
      }
      else {
        return;
      }
      $similar_node_types = variable_get('similar_node_types', _similar_published_node_types());
      if ($node->nid > 0 && !empty($similar_node_types[$node->type])) {
        unset($similar_node_types);
        switch ($delta) {
          case 0:

            // The subject is displayed at the top of the block. Note that it should
            // be passed through t() for translation.
            $block['subject'] = t('Similar entries');
            $cache_blocks = variable_get('similar_cache', SIMILAR_CACHE_DISABLED);
            if ($cache_blocks) {
              $cached_content = cache_get("similar_block_0:{$node->nid}", 'cache');
              if ($cached_content == 0) {

                // cache_get() only returns 0 when data is not returned for the key
                $block['content'] = theme('similar_content', $node);
                $lifetime = variable_get('similar_cache_lifetime', SIMILAR_CACHE_LIFETIME);
                cache_set("similar_block_0:{$node->nid}", 'cache', $block['content'], $lifetime ? time() + $lifetime : CACHE_TEMPORARY);
              }
              else {
                $block['content'] = $cached_content->data;
              }
            }
            else {
              $block['content'] = theme('similar_content', $node);
            }
        }
      }
      return empty($block['content']) ? '' : $block;
      break;
  }
}

/**
 * Query for published node types
 *
 * @see http://drupal.org/node/33444
 * @return array
 */
function _similar_published_node_types() {
  $types = array();
  $result = db_query('SELECT DISTINCT(n.type) FROM {node} n WHERE n.status <> 0 ORDER BY n.type ASC');
  while ($type = db_fetch_object($result)) {
    $types[$type->type] = $type->type;
  }
  return $types;
}

/**
 * Query for taxonomy names
 *
 * @see http://drupal.org/node/51041
 * @return array
 */
function _similar_taxonomy_names() {
  $names = array();
  $result = db_query('SELECT d.tid, v.vid, v.name AS vocab_name, d.name AS data_name FROM {term_data} d, {vocabulary} v WHERE v.vid = d.vid ORDER BY v.name, d.name ASC');
  while ($data = db_fetch_object($result)) {
    $names[$data->tid] = $data->vocab_name . ': ' . $data->data_name;
  }
  return $names;
}

/**
 * Query for taxonomies a node belongs to
 *
 * @see http://drupal.org/node/51041
 * @return array
 */
function _similar_taxonomy_membership($nid) {
  $tids_exclude = array();
  $vid = variable_get('similar_taxonomy_exclude', '');
  if (count($vid) > 0) {
    $result = db_query('SELECT t.tid FROM {term_data} t WHERE t.vid IN (%s)', implode(',', $vid));
    while ($data = db_fetch_object($result)) {
      $tids_exclude[$data->tid] = $data->tid;
    }
  }
  $tids = array();
  $result = db_query('SELECT t.tid FROM {term_node} t WHERE t.nid = %d', $nid);
  while ($data = db_fetch_object($result)) {
    if (!in_array($data->tid, $tidsExclude)) {
      $tids[$data->tid] = $data->tid;
    }
  }
  return $tids;
}

/**
 * Some characters just shouldn't be in node type names
 */
function _similar_content_type_escape(&$item) {
  $item = str_replace(array(
    "\0",
    "\n",
    "\r",
    "\\",
    "'",
    "\"",
    "\32",
  ), '', $item);
}

/**
 * SQL injection prevention
 */
function _similar_force_int(&$item) {
  $item = (int) $item;
}

/**
 * Queries the database for similar entries and puts them in a HTML list
 *
 * @param object $node
 * @return string
 */
function theme_similar_content($node) {
  $items = array();
  $text = $node->title . ' ' . $node->body;
  $teaser = variable_get('similar_teaser_enabled', 0);
  $types = _similar_published_node_types();
  $types = variable_get('similar_node_types', $types);
  array_walk($types, '_similar_content_type_escape');
  if (sizeof($types) > 1) {
    $types = implode("','", $types);
  }
  else {
    list(, $types) = each($types);
  }
  $types = "'{$types}'";
  if (module_exists('taxonomy') && (variable_get('similar_taxonomy_filter', 0) == 2 && ($taxonomy_tids = variable_get('similar_taxonomy_tids', array()))) || variable_get('similar_taxonomy_filter', 0) == 1 && ($taxonomy_tids = _similar_taxonomy_membership($node->nid))) {
    array_walk($taxonomy_tids, '_similar_force_int');
    if (sizeof($taxonomy_tids) > 1) {
      $taxonomy_tids = implode(',', $taxonomy_tids);
    }
    else {
      list(, $taxonomy_tids) = each($taxonomy_tids);
      $taxonomy_tids = (int) $taxonomy_tids;
    }
    $query = "SELECT r.nid, MATCH(r.body, r.title) AGAINST ('%s') AS score FROM {node_revisions} r INNER JOIN {node} n ON r.nid = n.nid AND r.vid = n.vid INNER JOIN {term_node} t ON n.nid = t.nid AND t.tid IN ({$taxonomy_tids}) WHERE MATCH(r.body, r.title) AGAINST ('%s') AND n.status <> 0 AND r.nid <> %d AND n.type IN ({$types}) GROUP BY n.nid ORDER BY score DESC, r.vid DESC";
  }
  else {
    $query = "SELECT r.nid, MATCH(r.body, r.title) AGAINST ('%s') AS score FROM {node_revisions} r INNER JOIN {node} n ON r.nid = n.nid AND r.vid = n.vid WHERE MATCH(r.body, r.title) AGAINST ('%s') AND n.status <> 0 AND r.nid <> %d AND n.type IN ({$types}) GROUP BY n.nid ORDER BY score DESC, r.vid DESC";
  }
  $query = db_rewrite_sql($query, 'n', 'nid');
  $result = db_query_range($query, $text, $text, $node->nid, 0, variable_get('similar_num_display', 5));
  while ($node = db_fetch_object($result)) {
    $content = node_load($node->nid);
    if ($teaser) {
      $items[] = '<div class="similar-title">' . l($content->title, 'node/' . $node->nid, variable_get('similar_rel_nofollow', 0) ? array(
        'rel' => 'nofollow',
      ) : NULL, NULL, NULL, NULL, TRUE) . '</div><div class="similar-teaser">' . check_markup($content->teaser, $content->format, FALSE) . '</div>';
    }
    else {
      $items[] = l($content->title, 'node/' . $node->nid, variable_get('similar_rel_nofollow', 0) ? array(
        'rel' => 'nofollow',
      ) : NULL);
    }
  }
  return sizeof($items) > 0 ? theme('item_list', $items) : '';
}

/**
 * Get all vocabularies
 *
 * @return array
 */
function _similar_taxonomy_get_vocabularies() {
  $v = array();
  $vocs = taxonomy_get_vocabularies();
  $result = db_query("SELECT DISTINCT(n.type) FROM {node} n WHERE n.status <> 0 ORDER BY n.type ASC");
  foreach ($vocs as $voc) {
    $v[$voc->vid] = $voc->name;
  }
  return $v;
}

Functions

Namesort descending Description
similar_admin_settings
similar_block Implementation of hook_block().
similar_help Implementation of hook_help().
similar_menu
similar_nodeapi Perform forced cache clearing
theme_similar_content Queries the database for similar entries and puts them in a HTML list
_similar_content_type_escape Some characters just shouldn't be in node type names
_similar_force_int SQL injection prevention
_similar_published_node_types Query for published node types
_similar_taxonomy_get_vocabularies Get all vocabularies
_similar_taxonomy_membership Query for taxonomies a node belongs to
_similar_taxonomy_names Query for taxonomy names

Constants

Namesort descending Description
SIMILAR_CACHE_DISABLED
SIMILAR_CACHE_ENABLED
SIMILAR_CACHE_LIFETIME @file Module that shows a block listing similar entries. NOTE: Uses MySQL's FULLTEXT indexing for MyISAM tables.