You are here

community_tags.module in Community Tags 6

Implements community tagging of nodes using a specific vocabulary for Drupal v6.x

File

community_tags.module
View source
<?php

/**
 * @file
 * Implements community tagging of nodes using a specific vocabulary for Drupal v6.x
 */

/**
 * Display modes.
 */
define('COMMUNITY_TAGS_MODE_BLOCK', 0);
define('COMMUNITY_TAGS_MODE_TAB', 1);
define('COMMUNITY_TAGS_MODE_INLINE', 2);

/**
 * Operation modes.
 */
define('COMMUNITY_TAGS_OPMODE_NOSYNC', 0x0);
define('COMMUNITY_TAGS_OPMODE_SYNC', 0x1);
define('COMMUNITY_TAGS_OPMODE_DELETE_ORPHAN_TERMS', 0x2);

/**
 * Implementation of hook_help().
 */
function community_tags_help($path, $arg) {
  switch ($path) {
    case 'admin/settings/community-tags':
      return t('To set up community tagging, you must first <a href="@taxonomy">create a normal free tagged vocabulary</a>. Then activate community tagging on such a vocabulary below, and set the <a href="@workflow">workflow options</a> for node types to control how users can tag nodes.', array(
        '@taxonomy' => url('admin/content/taxonomy'),
        '@workflow' => url('admin/content/types'),
      ));
      break;
  }
}

/**
 * Implementation of hook_theme().
 */
function community_tags_theme() {
  return array(
    'community_tags_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'file' => 'community_tags.pages.inc',
    ),
    'community_tags' => array(
      'arguments' => array(
        'tags' => NULL,
      ),
    ),
    'community_tags_links' => array(
      'arguments' => array(
        'tags' => NULL,
      ),
    ),
    'community_tags_settings' => array(
      'arguments' => array(
        'element' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_menu().
 */
function community_tags_menu() {
  $items = array();
  $items['admin/settings/community-tags'] = array(
    'title' => 'Community tags',
    'description' => 'Configure community tagging.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'community_tags_settings',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'community_tags.admin.inc',
  );
  $items['admin/settings/community-tags/ops/broken'] = array(
    'title' => 'Delete broken community tags',
    'description' => 'Delete broken community tags.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'community_tags_delete_broken_tags_form',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'community_tags.admin.inc',
  );
  $items['admin/settings/community-tags/ops/rebuild/%taxonomy_vocabulary'] = array(
    'title' => 'Rebuild community tags',
    'description' => 'Rebuild community tags.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'community_tags_rebuild_form',
      5,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'community_tags.admin.inc',
  );
  $items['admin/settings/community-tags/ops/purge/%taxonomy_vocabulary'] = array(
    'title' => 'Delete community tags',
    'description' => 'Delete community tags.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'community_tags_delete_all_form',
      5,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'community_tags.admin.inc',
  );
  $items['community-tags/js/%node'] = array(
    'page callback' => 'community_tags_from_js',
    'page arguments' => array(
      2,
    ),
    'access callback' => '_community_tags_menu_access',
    'type' => MENU_CALLBACK,
    'file' => 'community_tags.ajax.inc',
  );
  $items['community-tags/user'] = array(
    'page callback' => 'community_tags_by_user',
    'access callback' => '_community_tags_menu_access',
    'type' => MENU_CALLBACK,
  );
  $items['node/%node/tag'] = array(
    'title' => 'Tags',
    'page callback' => 'community_tags_node_view',
    'page arguments' => array(
      1,
      FALSE,
    ),
    'access callback' => '_community_tags_tab_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
    'file' => 'community_tags.pages.inc',
  );
  return $items;
}

/**
 * Implementation of hook_block().
 */
function community_tags_block($op = 'list', $delta = 0, $edit = array()) {
  global $user;
  switch ($op) {
    case 'list':

      // tagging form block should not be cached as block uses JS settings in community_tags_node_view()
      $block[0] = array(
        'info' => t('Community tagging form'),
        'cache' => BLOCK_NO_CACHE,
      );
      return $block;
    case 'view':
      if (user_access('access content') && user_access('tag content')) {
        if (arg(0) == 'node' && is_numeric(arg(1)) && (arg(2) == '' || arg(2) == 'view')) {
          $node = menu_get_object();
          if (_community_tags_is_tagging_view_visible($node, COMMUNITY_TAGS_MODE_BLOCK)) {
            $block['subject'] = t('Tag this');
            $block['content'] = community_tags_node_view($node, TRUE);
            return $block;
          }
        }
      }
      break;
  }
}

/**
 * Implementation of hook_perm().
 */
function community_tags_perm() {
  return array(
    'tag content',
    'edit own tags',
  );
}

/**
 * Implementation of hook_nodeapi().
 * Community tags hooks should be called after taxonomy module hooks - see system
 * weight in community_tags.install.
 */
function community_tags_nodeapi(&$node, $op, $teaser) {
  switch ($op) {
    case 'load':
      $node->community_tags_form = _community_tags_is_tagging_view_visible($node, COMMUNITY_TAGS_MODE_INLINE);
      break;
    case 'insert':
      _community_tags_node_insert($node);
      break;
    case 'update':
      if (!isset($node->ct_user_tags)) {

        // only process if not comming from community_tags_taxonomy_node_save() or batch
        _community_tags_node_update($node);
      }
      break;
    case 'delete':
      _community_tags_node_delete($node);
    case 'view':
      global $user;

      // Show quick tag form for this node if we're on a node page view and the
      // form is enabled for this node and the default quick tag vocab is set and it's not a search build.
      if (!$teaser && $node->build_mode != NODE_BUILD_SEARCH_INDEX && $node->community_tags_form) {
        $node->content['community_tags'] = array(
          '#value' => community_tags_node_view($node, TRUE),
          '#weight' => 50,
        );
      }
      break;
  }
}

/**
 * Implementation of hook_taxonomy().
 * Handle term deletion. No need to handle vocabulary deletion term/delete
 * hook is called for every term in the vocabulary before vocabulary/delete hook.
 */
function community_tags_taxonomy($op = NULL, $type = NULL, $term = NULL) {
  if ($type == 'term' && $term['tid']) {
    switch ($op) {
      case 'delete':

        // if term is deleted then remove all ctags for the term
        $term = (object) $term;
        _community_tags_term_delete($term);
        break;
    }
  }
}

/**
 * Implementation of hook_user().
 *
 * Handle user deletion.
 */
function community_tags_user($op, &$edit, &$user) {
  if ($op == 'delete') {

    // if user is deleted then remove all ctags for the user.
    // @todo consider option of moving all tags to a "dead" user so tags are not lost
    _community_tags_user_delete($user);
  }
}

/**
 *  Implement CCK's hook_content_extra_fields().
 */
function community_tags_content_extra_fields($type_name) {
  $extra = array();
  if (variable_get('community_tags_display_' . $type_name, COMMUNITY_TAGS_MODE_TAB) == COMMUNITY_TAGS_MODE_INLINE) {
    $extra['community_tags'] = array(
      'label' => t('Community Tags'),
      'description' => t('Community Tags Form'),
      'weight' => 100,
    );
  }
  return $extra;
}

/**
 * Implementation of hook_form_alter().
 */
function community_tags_form_alter(&$form, &$form_state, $form_id) {

  // Provide option to enable Community Tags per node type.
  if ($form_id == 'node_type_form' && isset($form['#node_type']->type)) {

    // only show if content type is mapped to tagging vocabulary
    $supported_vids = _community_tags_vids_for_node_type($form['#node_type']->type);
    if (!empty($supported_vids)) {
      $modes = array(
        COMMUNITY_TAGS_MODE_BLOCK => t('Block'),
        COMMUNITY_TAGS_MODE_TAB => t('Tab'),
        COMMUNITY_TAGS_MODE_INLINE => t('Inline'),
      );
      $form['workflow']['community_tags_display'] = array(
        '#type' => 'radios',
        '#title' => t('Community tagging form'),
        '#default_value' => variable_get('community_tags_display_' . $form['#node_type']->type, COMMUNITY_TAGS_MODE_TAB),
        '#options' => $modes,
        '#description' => t('How should users be allowed to tag content?'),
      );
    }
  }
}

/**
 * Save community_tags term associations and counts for a given node.
 *
 * Do user ctags processing. If new tags added or tags deleted and synchronisation required,
 * call node_save() so that other modules get to act including taxonomy.module which will create
 * or destroy term node records.
 *
 * @param $tags_and_terms
 *  All the users' terms - array('tags' => array(vid1 => array($tagname1, $tagname2...), vid2 => array(...)))
 *  NB: may have more than 1 vocabulary.
 */
function community_tags_taxonomy_node_save($node, $tags_and_terms, $is_owner, $uid) {

  // get permitted CT vocabularies
  $vids = community_tags_vids_for_node($node);

  // find existing terms and identify new tags
  $processed_tags_and_terms = _community_tags_node_process_tags_and_terms($tags_and_terms, $vids);

  // create new terms for new tags
  $processed_terms = _community_tags_convert_new_tags_to_terms($processed_tags_and_terms);

  // flag will be set if new terms for the node are found and sync mode is set
  $node_save_required = FALSE;

  // keep track of terms that we may remove if tag deleted
  $possible_redundant_term_tids = array();

  // for each vocabulary supplied
  foreach ($processed_terms as $vid => $processed_terms_for_vocabulary) {

    // compare existing node terms to processed terms - add or delete as required.
    $existing_tags = _community_tags_get_node_user_vid_tags($node->nid, $uid, $vid);
    $new_tags = array_diff_key($processed_terms_for_vocabulary, $existing_tags);
    $removed_tags = array_diff_key($existing_tags, $processed_terms_for_vocabulary);

    // add new tags attribute to the current user
    foreach ($new_tags as $tid => $value) {

      // add new tag
      _community_tags_add_tag($node->nid, $tid, $uid);

      // if tags are synched with node terms and this tag isn't a node term - then add it from node terms
      if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_SYNC, $vid, $node->type) && !isset($node->taxonomy[$tid])) {
        $node->taxonomy[$tid] = $processed_terms_for_vocabulary[$tid];
        $node_save_required = TRUE;
      }
    }

    // remove old tags for this user
    foreach ($removed_tags as $tid => $value) {
      _community_tags_delete_tag($node->nid, $tid, $uid);

      // if tags are synched with node terms and this tag is a node term and tag count is down to 1 (i.e. last tag)
      // then remove it from node terms
      if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_SYNC, $vid, $node->type) && $existing_tags[$tid]->tag_count <= 1 && isset($node->taxonomy[$tid])) {
        unset($node->taxonomy[$tid]);
        $node_save_required = TRUE;
      }
      if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_DELETE_ORPHAN_TERMS, $vid, $node->type)) {
        $possible_redundant_term_tids[] = $tid;
      }
    }
  }
  if ($node_save_required) {

    // setting this will prevent full CT node update processing
    $node->ct_user_tags = $tags_and_terms;

    // invoke full node save pipeline - term nodes will be updated, and good stuff like search (including the Apache SOLR Integration module) will know about it.
    node_save($node);
  }

  // still valid to check for orphaned terms
  // @todo make sure this isn't too onerous - we're probably in an AJAX call here...
  if (!empty($possible_redundant_term_tids)) {
    _community_tags_cleanup_orphaned_tags_by_tids($possible_redundant_term_tids);
  }
  return;
}

/**
 * Helper function for retrieving a query result to pass along to the Tagadelic
 * functions prior to theming.
 *
 * @param $type
 *   The type of query to perform. Possible values:
 *   - node: get tag count for a given node.
 *   - type: get tag count for a given node type.
 *   - user: get tag count for a given user.
 *   - user_node: get tag count for a given user on a given node.
 *   - global: get tag count across entire site (default).
 * @param $args
 *   An array of arguments that correspond to the result type:
 *   - If type is 'node', $arg1 is a node ID, $arg2 (optional) is vocabulary ID.
 *   - If type is 'type', $arg1 is a node type.
 *   - If type is 'user', $arg1 is a user ID.
 *   - If type is 'user_node', $arg1 is a user ID, and $arg2 is a node ID.
 *   - If type is 'global', neither $args are used.
 * @param $limit
 *   Only display a certain number of tags.
 * @return $result
 *  A database result set.
 */
function _community_tags_get_tag_result($type = 'global', $limit = NULL, $arg1 = NULL, $arg2 = NULL) {
  $sql = '';
  switch ($type) {
    case 'node':
      $arg1 = (int) $arg1;
      if ($arg2) {
        $arg2 = (int) $arg2;
        $sql = "SELECT COUNT(t.tid) AS count, t.tid, t.name, t.vid FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = %d AND t.vid = %d GROUP BY t.tid, t.name, t.vid ORDER BY count DESC";
      }
      else {
        $arg2 = NULL;
        $sql = "SELECT COUNT(t.tid) AS count, t.tid, t.name, t.vid FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = %d GROUP BY t.tid, t.name, t.vid ORDER BY count DESC";
      }
      break;
    case 'type':
      $arg1 = (string) $arg1;
      $arg2 = NULL;
      $sql = "SELECT COUNT(t.tid) AS count, t.tid, t.name, t.vid FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid INNER JOIN {node} n ON n.nid = c.nid WHERE n.type = '%s' GROUP BY t.tid, t.name, t.vid ORDER BY count DESC";
      break;
    case 'user':
      $arg1 = (int) $arg1;
      $arg2 = NULL;
      $sql = "SELECT COUNT(t.tid) AS count, t.tid, t.name, t.vid FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.uid = %d GROUP BY t.tid, t.name, t.vid ORDER BY count DESC";
      break;
    case 'user_node':
      $arg1 = (int) $arg1;
      $arg2 = (int) $arg2;
      $sql = "SELECT COUNT(t.tid) AS count, t.tid, t.name, t.vid FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = %d AND c.uid = %d GROUP BY t.tid, t.name, t.vid ORDER BY count DESC";
    default:
      $sql = "SELECT COUNT(t.tid) AS count, t.tid, t.name, t.vid FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid GROUP BY t.tid, t.name, t.vid ORDER BY count DESC";
  }
  if ($limit) {
    $limit = (int) $limit;
    return db_query_range($sql, $arg1, $arg2, 0, $limit);
  }
  else {
    return db_query($sql, $arg1, $arg2);
  }
}
function _community_tags_get_display_handlers() {
  static $handlers;
  if (!$handlers) {
    $handlers = array(
      'none' => array(
        'id' => 'none',
        'title' => t('None'),
        'fn' => '_community_tags_display_handler_none',
      ),
      'links' => array(
        'id' => 'links',
        'title' => t('Links'),
        'fn' => '_community_tags_display_handler_links',
      ),
    );
    if (module_exists('tagadelic')) {
      $handlers['tagadelic'] = array(
        'id' => 'tagadelic',
        'title' => t('Tagadelic'),
        'fn' => '_community_tags_display_handler_tagadelic',
      );
    }
  }
  return $handlers;
}

/**
 * Get handler options for admin form. Interim measure pending pluggable display handlers.
 */
function _community_tags_get_display_handler_options() {
  $options = array();
  foreach (_community_tags_get_display_handlers() as $key => $handler) {
    $options[$key] = $handler['title'];
  }
  return $options;
}

/**
 * Perhaps extend with ctools.
 * Return configured display handler or default to 'links' if configured handler not available.
 */
function _community_tags_get_display_handler($vid, $content_type, $inline) {

  // get settings
  $settings = _community_tags_get_settings($vid, $content_type);

  // get all handlers
  $handlers = _community_tags_get_display_handlers();
  return isset($handlers[$settings['display_handler']]) ? $handlers[$settings['display_handler']] : $handlers['links'];
}

/**
 * No all tag display.
 */
function _community_tags_display_handler_none() {
  return;
}

/**
 * Display all tags as simple links.
 */
function _community_tags_display_handler_links($type = 'global', $limit = NULL, $arg1 = NULL, $arg2 = NULL) {
  $result = _community_tags_get_tag_result($type, $limit, $arg1, $arg2);
  $items = array();
  while ($row = db_fetch_object($result)) {
    $items[] = $row;
  }
  return theme('community_tags_links', $items);
}

/**
 * Display all tags using tagadelic. Only called if tagadelic module is enabled. See _community_tags_get_tag_result() for definitions
 * of $type and the arguments.
 */
function _community_tags_display_handler_tagadelic($type = 'global', $limit = NULL, $arg1 = NULL, $arg2 = NULL) {
  $result = _community_tags_get_tag_result($type, $limit, $arg1, $arg2);
  $weighted_tags = tagadelic_build_weighted_tags($result);
  $sorted_tags = tagadelic_sort_tags($weighted_tags);
  return theme('community_tags', $sorted_tags);
}

/**
 * Community tags callback for node view.
 *
 * chaps2 - implemented multiple vocabularies base on patch at #199936.
 * @todo refactor to allow use of block cache
 */
function community_tags_node_view($node, $inline = TRUE) {
  global $user;
  if (is_numeric($node)) {
    $node = node_load($node);
  }

  // Guard against duff nids and nodes. Added in response to http://drupal.org/node/331819.
  if (!$node || !is_object($node)) {
    return;
  }
  if (!$inline) {
    drupal_set_title(check_plain($node->title));
  }
  module_load_include('inc', 'community_tags', 'community_tags.pages');
  $output = '';
  $vids = community_tags_vids_for_node($node);
  foreach ($vids as $vid) {
    $tags = community_tags_get_user_node_tags($user->uid, $node->nid, $vid);
    $display_handler = _community_tags_get_display_handler($vid, $node->type, $inline);
    $cloud = call_user_func($display_handler['fn'], 'node', NULL, $node->nid, $vid);
    $names = array();
    if (!count($tags)) {

      // User has not yet added tags to this node yet. Show form.
      $output .= drupal_get_form('community_tags_form', array(
        'node' => $node,
        'cloud' => $cloud,
        'nid' => $node->nid,
        'vid' => $vid,
        'tags' => NULL,
        'inline' => $inline,
        'multiple' => count($vids),
      ));
    }
    elseif (user_access('edit own tags')) {

      // User has already tagged this node, but can edit their tags. Show form
      // with the user's tags pre-populated.
      $names = community_tags_flatten($tags);
      $tags = taxonomy_implode_tags($tags);
      $output .= drupal_get_form('community_tags_form', array(
        'node' => $node,
        'cloud' => $cloud,
        'nid' => $node->nid,
        'vid' => $vid,
        'tags' => $tags,
        'inline' => $inline,
        'multiple' => count($vids),
      ));
    }
    else {

      // Sorry, no more adding tags for you!
      $output .= '<p>' . t('You have already tagged this post. Your tags: ') . theme('community_tags', $tags) . '</p>';
    }

    // TODO might want to optimise this call
    drupal_add_js(array(
      'communityTags' => array(
        'n_' . $node->nid => array(
          'v_' . $vid => array(
            'tags' => $names,
            'url' => url('community-tags/js/' . $node->nid . '/' . $vid),
            'add' => t('Add'),
            'token' => drupal_get_token('community_tags_form'),
          ),
        ),
      ),
    ), 'setting');
  }
  return $output;
}

/**
 * Theme function to display a list of community tags via tagadelic.
 *
 * @ingroup themeable
 */
function theme_community_tags($tags) {
  return '<div class="cloud">' . (count($tags) ? theme('tagadelic_weighted', $tags) : t('None')) . '</div>';
}

/**
 * Theme function to display a list of community tags as simple links.
 *
 * @ingroup themeable
 */
function theme_community_tags_links($tags) {
  foreach ($tags as $tag) {
    $links[] = array(
      'title' => $tag->name,
      'href' => taxonomy_term_path($tag),
    );
  }
  return theme('links', $links);
}

/**
 * Menu access callback; Common access check for tag operations.
 */
function _community_tags_menu_access() {
  return user_access('access content') && user_access('tag content');
}

/**
 * Menu access callback; Check if the user can access the 'Tags' local task on
 * node pages.
 */
function _community_tags_tab_access($node) {
  return _community_tags_is_tagging_view_visible($node, COMMUNITY_TAGS_MODE_TAB) && _community_tags_menu_access();
}

/**
 * Helper function for the JS tagger.
 */
function community_tags_flatten($tags) {
  $names = array();
  foreach ($tags as $tag) {
    $names[] = $tag->name;
  }
  return $names;
}

/**
 * Implementation of hook_views_api
 * See community_tags.views.inc for the actual views integration
 */
function community_tags_views_api() {
  return array(
    'api' => 2,
  );
}

/*****************************************************************************
 * Node (hook_nodeapi) handlers for CT.
 *
 * Permissions - user editing a node may cause community tags to be created
 * or deleted without having explicit permission to do so.
 *****************************************************************************/

/**
 * Node has been inserted. All node terms are added to ctags attributed to the node editor.
 */
function _community_tags_node_insert($node) {
  global $user;

  // get CT vocabularies for this node
  $vids = community_tags_vids_for_node($node);

  // filter out non CT vocabulary terms, convert tag names to terms, and identify new tags
  $processed_tags_and_terms = _community_tags_node_process_tags_and_terms($node->taxonomy, $vids);

  // new tags should have been created as new terms by taxonomy.module but in
  // case system weights have been altered...
  $processed_terms = _community_tags_convert_new_tags_to_terms($processed_tags_and_terms);

  // add all to community_tags
  foreach ($processed_terms as $vid => $terms) {
    foreach ($terms as $tid => $term) {

      // add term to community tags for current user (default)
      _community_tags_add_tag($node->nid, $tid, $user->uid);
    }
  }
}

/**
 * Node has been updated. All terms that are not ctags are added to ctags attributed to the current user. Removed
 * terms are removed from ctags either for all users (sync mode) or just the current user.
 */
function _community_tags_node_update($node, $vids = NULL) {
  global $user;

  // get CT vocabularies for this node
  $vids = $vids ? $vids : community_tags_vids_for_node($node);

  // filter out non CT vocabulary terms, convert tag names to terms, and identify new tags
  $processed_tags_and_terms = _community_tags_node_process_tags_and_terms($node->taxonomy, $vids);

  // new tags should have been created as new terms by taxonomy.module but in
  // case system weights have been altered...
  $processed_terms = _community_tags_convert_new_tags_to_terms($processed_tags_and_terms);

  // combine processed terms into 1 array
  $all_processed_terms = array();
  foreach ($processed_terms as $vid => $terms) {
    $all_processed_terms += $terms;
  }

  // compare existing node terms to processed terms - add or delete as required.
  $existing_tags = _community_tags_get_node_tags($node->nid, $vids);
  $new_tags = array_diff_key($all_processed_terms, $existing_tags);
  $removed_tags = array_diff_key($existing_tags, $all_processed_terms);
  $possible_redundant_term_tids = array();

  // add new tags attribute to the current user
  // always add irrespective of SYNC mode
  foreach ($new_tags as $tid => $value) {
    _community_tags_add_tag($node->nid, $tid, $user->uid);
  }

  // remove old tags for all users
  foreach ($removed_tags as $tid => $value) {
    $removed_node_term = $existing_tags[$tid];
    if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_SYNC, $removed_node_term->vid, $node->type)) {

      // if in SYNC mode - delete all ctags for removed node term
      _community_tags_delete_tags($node->nid, $tid);
      if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_DELETE_ORPHAN_TERMS, $removed_node_term->vid, $node->type)) {
        $possible_redundant_term_tids[] = $tid;
      }
    }
    else {

      // if not in SYNC mode - only delete the current user's tag for the removed node term
      _community_tags_delete_tag($node->nid, $tid, $user->uid);
      if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_DELETE_ORPHAN_TERMS, $removed_node_term->vid, $node->type) && $removed_node_term->tag_count <= 1) {
        $possible_redundant_term_tids[] = $tid;
      }
    }
  }
  _community_tags_cleanup_orphaned_tags_by_tids($possible_redundant_term_tids);
}

/**
 * Node has been deleted. Delete all community tags for the deleted node.
 * Node terms will have been removed. After ctags have been removed check
 * for redundant terms.
 */
function _community_tags_node_delete($node) {

  // delete all tags for this node
  $existing_tags = _community_tags_get_node_tags($node->nid);
  _community_tags_delete_tags_for_node($node->nid);
  $possible_redundant_term_tids = array();
  foreach ($existing_tags as $tid => $tag) {
    if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_DELETE_ORPHAN_TERMS, $tag->vid, $node->type) && $tag->tag_count <= 1) {
      $possible_redundant_term_tids[] = $tid;
    }
  }
  _community_tags_cleanup_orphaned_tags_by_tids($possible_redundant_term_tids);
}

/*****************************************************************************
 * Taxonomy hook handlers
 ****************************************************************************/

/**
 * Node has been deleted. Delete all community tags for the deleted node. No
 * Synchronisation issues. When it's gone it's gone.
 */
function _community_tags_term_delete($term) {

  // delete all tags for this term
  _community_tags_delete_tags_for_term($term->tid);
}

/*****************************************************************************
 * User hook handlers
 ****************************************************************************/

/**
 * User has been deleted. Delete all user tags for the deleted user. Apply node term
 * deletion logic for all deleted tags.
 */
function _community_tags_user_delete($user) {

  // get all user tags with tag counts (needed for term node deletion logic)
  $user_tags = _community_tags_get_user_tags($user->uid);

  // delete all tags for this user
  _community_tags_delete_tags_for_user($user->uid);

  // compile list of node terms to remove from affected nodes
  $node_terms_to_remove = array();
  $possible_redundant_term_tids = array();
  foreach ($user_tags as $ctag) {
    if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_SYNC, $ctag->vid, $ctag->type) && $ctag->tag_count <= 1) {

      // last tag - need to delete the node term as well
      $node_terms_to_remove[$ctag->nid][$ctag->tid] = $ctag->name;
    }
    if (_community_tags_is_opmode(COMMUNITY_TAGS_OPMODE_DELETE_ORPHAN_TERMS, $ctag->vid, $ctag->type)) {
      $possible_redundant_term_tids[] = $ctag->tid;
    }
  }
  if (!empty($node_terms_to_remove)) {

    // for all affected nodes - remove node terms
    foreach ($node_terms_to_remove as $nid => $terms_to_remove) {
      $node = node_load($nid);
      $node->taxonomy = array_diff_key($node->taxonomy, $terms_to_remove);

      // identify node_save call as coming from CT processing - not node edit
      $node->ct_user_tags = array();
      node_save($node);
    }
  }

  // still valid to check for orphaned terms
  if (!empty($possible_redundant_term_tids)) {
    _community_tags_cleanup_orphaned_tags_by_tids($possible_redundant_term_tids);
  }
}

/*****************************************************************************
 * Low level community tag operations. Keep cruft out of these. For an API, wrap
 * these in higher level functions that can include hook invocation, permission
 * checking, configuration checks, bulk operations etc.
 ****************************************************************************/

/**
 * Add a community tag. Nid and vid and user should be valid
 */
function _community_tags_add_tag($nid, $tid, $uid) {
  $time = time();
  db_query('INSERT INTO {community_tags} (tid, nid, uid, date) VALUES (%d, %d, %d, %d)', $tid, $nid, $uid, $time);
}

/**
 * Delete a community tag. Nid and vid should be valid. If user is supplied tag is only removed for that user.
 */
function _community_tags_delete_tag($nid, $tid, $uid) {
  db_query('DELETE FROM {community_tags} WHERE nid = %d AND tid = %d AND uid = %d', $nid, $tid, $uid);
}

/**
 * Delete all community tags for given node and term.
 */
function _community_tags_delete_tags($nid, $tid) {
  db_query('DELETE FROM {community_tags} WHERE nid = %d AND tid = %d', $nid, $tid);
}

/**
 * Delete all community tags for a given node.
 */
function _community_tags_delete_tags_for_node($nid) {
  db_query('DELETE FROM {community_tags} WHERE nid = %d', $nid);
}

/**
 * Delete all community tags for a given term.
 */
function _community_tags_delete_tags_for_term($tid) {
  db_query('DELETE FROM {community_tags} WHERE tid = %d', $tid);
}

/**
 * Delete all community tags for a given user.
 */
function _community_tags_delete_tags_for_user($uid) {
  db_query('DELETE FROM {community_tags} WHERE uid = %d', $uid);
}

/**
 * Check for orphaned node terms and delete if required - by default doesn't.
 * Provides fix for [#984462] - "When a tag is no longer attached to any nodes, (provide option to) automatically remove it from its taxonomy vocabulary"
 *
 * @param $tids
 *  Doesn't check settings.
 */
function _community_tags_cleanup_orphaned_tags_by_tids($tids) {
  $count = 0;
  if (!empty($tids)) {

    // only delete if not ctag, not node term, not involved in relation, has no synonyms, and has no children
    $results = db_query("SELECT td.* FROM {term_data} td\n      LEFT JOIN {term_hierarchy} th ON th.parent = td.tid\n      LEFT JOIN {term_relation} tr ON tr.tid1 = td.tid OR tr.tid2 = td.tid\n      LEFT JOIN {term_synonym} ts ON ts.tid = td.tid\n      LEFT JOIN {term_node} tn ON tn.tid = td.tid\n      LEFT JOIN {community_tags} ct ON ct.tid = td.tid\n      WHERE td.tid IN (" . db_placeholders($tids) . ")\n      AND tn.tid IS NULL\n      AND ct.tid IS NULL\n      AND th.parent IS NULL\n      AND (tr.tid1 IS NULL AND tr.tid2 IS NULL)\n      AND ts.tid IS NULL", $tids);
    while ($row = db_fetch_object($results)) {
      _community_tags_delete_redundant_term($row->tid);
    }
  }
  return $count;
}

/**
 * @todo set flag to skip tag delete attempt in community_tags_taxonomy() invocation
 */
function _community_tags_delete_redundant_term($tid) {

  // Be careful of other dependencies on taxonomy terms
  // Hook community_tags_taxonomy() will be invoked which will attempt to delete
  // tags for the deleted term. There will be none so a pointless step - potential to set a flag to skip.
  taxonomy_del_term($tid);
}

/*****************************************************************************
 * Visibility and access helpers
 ****************************************************************************/

/**
 * Check that tagging form is configured for display for given node in given context. Does not check user access.
 *
 * @param $context
 *  Either COMMUNITY_TAGS_MODE_BLOCK, COMMUNITY_TAGS_MODE_BLOCK, or COMMUNITY_TAGS_MODE_INLINE.
 */
function _community_tags_is_tagging_view_visible($node, $context) {
  if ($node && variable_get('community_tags_display_' . $node->type, COMMUNITY_TAGS_MODE_TAB) == $context) {
    $vids = community_tags_vids_for_node($node);
    if (!empty($vids)) {
      return TRUE;
    }
  }
}

/**
 * Check whether a given node has one or more community tagged vocabularies associated with its type.
 */
function community_tags_vids_for_node($node) {

  // Allow both nids and nodes
  if (is_numeric($node)) {
    $node = node_load($node);
  }
  return _community_tags_vids_for_node_type($node->type);
}

/**
 * Check whether given node type has one or more community tagged vocabularies associated with it.
 */
function _community_tags_vids_for_node_type($type) {
  return _community_tags_vids($type);
}

/**
 * Check whether given node type has one or more community tagged vocabularies associated with it.
 */
function _community_tags_vids($type = NULL) {
  $community_tagged = variable_get('community_tags_vocabularies', array());
  if ($type) {
    $result = db_query("SELECT vnt.vid FROM {vocabulary_node_types} vnt JOIN {vocabulary} v ON v.vid = vnt.vid AND v.tags = 1 WHERE vnt.type = '%s'", $type);
  }
  else {
    $result = db_query("SELECT vnt.vid FROM {vocabulary_node_types} vnt JOIN {vocabulary} v ON v.vid = vnt.vid AND v.tags = 1");
  }
  $vids = array();
  while ($vid = db_fetch_object($result)) {
    if (isset($community_tagged[$vid->vid])) {
      $vids[$vid->vid] = $vid->vid;
    }
  }
  return $vids;
}

/**
 * Determine whether such and such a CT operation mode is set for tagging in given vocabulary. Returns
 * true if any of the modes is set.
 *
 * @param $modes
 *  A bitwise OR of the operation modes to test.
 *
 * @todo Add settings to admin screen. Is it necessary to have settings per vid / per type?
 */
function _community_tags_is_opmode($modes, $vid, $content_type) {
  $settings = _community_tags_get_settings($vid, $content_type);
  if ($settings) {
    return $settings['opmode'] & $modes;
  }

  // default to keeping node terms and community tags in sync
  return COMMUNITY_TAGS_OPMODE_SYNC & $modes;
}

/**
 * Get CT settings.
 */
function _community_tags_get_settings($vid = NULL, $content_type = NULL, $valid = FALSE) {
  static $settings, $valid_settings;
  $handlers = _community_tags_get_display_handlers();
  $default_display_handler = isset($handlers['tagadelic']) ? 'tagadelic' : 'links';
  if (!$settings) {

    // Build list of available free-tagging vocabularies
    // $valid_CT_vocabularies = _community_tags_vids();
    $valid_CT_vocabularies = variable_get('community_tags_vocabularies', array());
    $result = db_query('SELECT v.vid, v.name, v.tags, nt.name type_name, nt.type
       FROM {vocabulary} v
       LEFT JOIN {vocabulary_node_types} vnt ON vnt.vid = v.vid
       LEFT JOIN {node_type} nt ON nt.type = vnt.type
       ORDER BY v.weight, v.name, nt.name');
    $settings = array();
    $valid_settings = array();
    while ($row = db_fetch_object($result)) {

      // create structure grouped on vocabulary
      if (!isset($settings[$row->vid])) {
        $settings[$row->vid] = array(
          'name' => $row->name,
          'tagging' => $row->tags,
          'types' => array(),
        );
        $settings[$row->vid]['CT_enabled'] = isset($valid_CT_vocabularies[$row->vid]);
      }
      if (!empty($row->type)) {
        if (isset($valid_CT_vocabularies[$row->vid]['types'][$row->type])) {
          $settings[$row->vid]['types'][$row->type] = $valid_CT_vocabularies[$row->vid]['types'][$row->type];
          $settings[$row->vid]['types'][$row->type]['type_name'] = $row->type_name;
          $settings[$row->vid]['types'][$row->type]['assigned'] = TRUE;
        }
        else {
          $settings[$row->vid]['types'][$row->type] = array(
            'type_name' => $row->type_name,
            'assigned' => TRUE,
            'opmode' => COMMUNITY_TAGS_OPMODE_SYNC,
            'display_handler' => $default_display_handler,
          );
        }
        if ($settings[$row->vid]['tagging'] && isset($valid_CT_vocabularies[$row->vid])) {
          $valid_settings[$row->vid] = $settings[$row->vid];
        }
      }
    }
  }

  // either return from valid settings only or from all settings
  $rt = $valid ? $valid_settings : $settings;
  if ($vid && $content_type) {
    $return = !empty($rt[$vid]['types'][$content_type]) ? $rt[$vid]['types'][$content_type] : FALSE;
    return $return;
  }
  elseif ($vid) {
    return !empty($rt[$vid]) ? $rt[$vid] : FALSE;
  }
  else {
    return $rt;
  }
}

/*****************************************************************************
 * ctag queries
 ****************************************************************************/

/**
 * Retrieve list of tags for a given node.
 *
 * @return
 *  Array of objects {tid, name, tag_count} keyed on tid.
 */
function _community_tags_get_node_tags($nid, $vids = NULL) {
  $tags = array();
  if ($vids) {
    $args = $vids;
    array_unshift($args, $nid);
    $result = db_query("SELECT t.tid, t.vid, t.name, count(t.tid) tag_count FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = %d AND t.vid IN (" . db_placeholders($vids, 'varchar') . ") GROUP BY t.tid", $args);
  }
  else {
    $result = db_query("SELECT t.tid, t.vid, t.name, count(t.tid) tag_count FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = %d GROUP BY t.tid", $nid);
  }
  while ($term = db_fetch_object($result)) {
    $tags[$term->tid] = $term;
  }
  return $tags;
}

/**
 * Retrieve list of tags for a given node and user. Includes a count of the number of users who have tagged it.
 *
 * @return
 *  Array of objects {tid, name, tag_count} keyed on tid. The tag_count is the number of users who share the tag.
 */
function _community_tags_get_node_user_vid_tags($nid, $uid, $vid) {
  $tags = array();
  $result = db_query("SELECT t.tid, t.name, count(ct2.uid) tag_count\n     FROM (SELECT tid, nid FROM {community_tags} WHERE nid = %d AND uid = %d) AS ct\n     INNER JOIN {term_data} t ON t.tid = ct.tid\n     INNER JOIN {community_tags} ct2 ON ct2.tid = ct.tid AND ct2.nid = ct.nid\n     WHERE t.vid = %d\n     GROUP BY t.tid", $nid, $uid, $vid);
  while ($term = db_fetch_object($result)) {
    $tags[$term->tid] = $term;
  }
  return $tags;
}

/**
 * Retrieve list of tags for a given user. Includes a count of the number of users who have tagged it.
 *
 * @return
 *  Array of objects {nid, tid, (term)name, vid, tag_count}. The tag_count is the number of users who share the tag.
 */
function _community_tags_get_user_tags($uid) {
  $tags = array();
  $result = db_query("SELECT ct2.nid, t.tid, t.name, t.vid, n.type, count(ct2.uid) tag_count\n     FROM (SELECT tid, nid FROM {community_tags} WHERE uid = %d) AS ct\n     INNER JOIN {term_data} t ON t.tid = ct.tid\n     INNER JOIN {node} n ON n.nid = ct.nid\n     INNER JOIN {community_tags} ct2 ON ct2.tid = ct.tid AND ct2.nid = ct.nid\n     GROUP BY ct2.nid, t.tid", $uid);
  while ($term = db_fetch_object($result)) {
    $tags[] = $term;
  }
  return $tags;
}

/**
 * If user supplied - assume has permission - otherwise use current user if has permission.
 *
 * If the user is anonymous
 */
function _community_tags_check_user($user = NULL) {
  if (!$user) {
    if (!$GLOBALS['user'] && !variable_get('community_tags_allow_anonymous_attribution', 1)) {
      return FALSE;
    }
    else {
      $user = $GLOBALS['user'];
    }
  }
  return $user;
}

/**
 * Retrieve list of tags for a given node that belong to a user.
 */
function community_tags_get_user_node_tags($uid, $nid, $vid) {
  $tags = array();
  $result = db_query("SELECT t.tid, t.name, c.uid, c.nid FROM {term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = %d AND c.uid = %d AND t.vid = %d ORDER BY t.name", $nid, $uid, $vid);
  while ($term = db_fetch_object($result)) {
    $tags[$term->tid] = $term;
  }
  return $tags;
}

/*****************************************************************************
 * Tag/term input processors.
 *****************************************************************************/

/**
 * Process tags and terms - resolve tags (supplied as the tag name) to existing terms and
 * identify new tags - but don't create.
 *
 * @param $terms
 *  A data structure as processed by taxonomy_save_node. Maybe tags, terms, tids etc.
 *
 * @param $vids
 *  The valid vocabulary vids - ignore all other terms and tags
 *
 * @return
 *  An array of terms and new tags grouped by vid. Each element has the following structure:
 *    'terms' => array of term objects
 *    'new tags' => array of new tag names
 */
function _community_tags_node_process_tags_and_terms($tags_and_terms, $vids) {
  $processed_terms = array();
  if (is_array($tags_and_terms)) {
    foreach ($tags_and_terms as $key => $term) {
      if (!is_numeric($key) && $key == 'tags') {

        // tags are grouped by vid
        foreach ($term as $vid => $vid_value) {

          // only process ctag vocabulary tags
          if (isset($vids[$vid])) {

            // make sure we pass back at least an empty array for the provided vid
            $processed_terms[$vid] = array();

            // handle array of tags or comma seperated list of tags
            $vid_tags = is_array($vid_value) ? $vid_value : drupal_explode_tags($vid_value);
            foreach ($vid_tags as $tag) {

              // See if the term exists in the chosen vocabulary
              // and return the tid, otherwise, add a new record.
              $matching_terms = taxonomy_get_term_by_name($tag);
              $matching_vocabulary_term = NULL;

              // tid match if any.
              foreach ($matching_terms as $matching_term) {
                if ($matching_term->vid == $vid) {
                  $matching_vocabulary_term = $matching_term;
                  break;
                }
              }
              if (!$matching_vocabulary_term) {
                $processed_terms[$vid]['new tags'][] = $tag;
              }
              else {
                $processed_terms[$vid]['terms'][$matching_vocabulary_term->tid] = $matching_vocabulary_term;
              }
            }
          }
        }
      }
      else {
        if (is_array($term)) {
          foreach ($term as $tid) {
            if ($tid) {
              $term_object = taxonomy_get_term($tid);
              if ($term_object && isset($vids[$term_object->vid])) {
                $processed_terms[$term_object->vid]['terms'][$tid] = $term_object;
              }
            }
          }
        }
        else {
          if ($term) {
            $term_object = !is_object($term) ? taxonomy_get_term($term) : $term;
            if ($term_object && isset($vids[$term_object->vid])) {
              $processed_terms[$term_object->vid]['terms'][$term_object->tid] = $term_object;
            }
          }
        }
      }
    }
  }
  return $processed_terms;
}

/**
 * Create terms for new tags and add return a simpler structure of term arrays
 * grouped by vid.
 *
 * @param $processed_tags
 *  Data structure as returned by _community_tags_node_process_terms().
 *
 * @return
 *  An array of term arrays keyed on vid.
 */
function _community_tags_convert_new_tags_to_terms($processed_tags_and_terms) {
  $processed_terms = array();
  foreach ($processed_tags_and_terms as $vid => $tags_and_terms) {
    if (!empty($tags_and_terms['terms'])) {
      $processed_terms[$vid] = $tags_and_terms['terms'];
    }
    else {
      $processed_terms[$vid] = array();
    }
    if (!empty($tags_and_terms['new tags'])) {
      foreach ($tags_and_terms['new tags'] as $tag_name) {

        // create term.
        $edit = array(
          'vid' => $vid,
          'name' => $tag_name,
        );

        // the following call may result in contrib hook_invocations
        $status = taxonomy_save_term($edit);
        $new_term = taxonomy_get_term($edit['tid']);
        $processed_terms[$vid][$new_term->tid] = $new_term;
      }
    }
  }
  return $processed_terms;
}

Functions

Namesort descending Description
community_tags_block Implementation of hook_block().
community_tags_content_extra_fields Implement CCK's hook_content_extra_fields().
community_tags_flatten Helper function for the JS tagger.
community_tags_form_alter Implementation of hook_form_alter().
community_tags_get_user_node_tags Retrieve list of tags for a given node that belong to a user.
community_tags_help Implementation of hook_help().
community_tags_menu Implementation of hook_menu().
community_tags_nodeapi Implementation of hook_nodeapi(). Community tags hooks should be called after taxonomy module hooks - see system weight in community_tags.install.
community_tags_node_view Community tags callback for node view.
community_tags_perm Implementation of hook_perm().
community_tags_taxonomy Implementation of hook_taxonomy(). Handle term deletion. No need to handle vocabulary deletion term/delete hook is called for every term in the vocabulary before vocabulary/delete hook.
community_tags_taxonomy_node_save Save community_tags term associations and counts for a given node.
community_tags_theme Implementation of hook_theme().
community_tags_user Implementation of hook_user().
community_tags_vids_for_node Check whether a given node has one or more community tagged vocabularies associated with its type.
community_tags_views_api Implementation of hook_views_api See community_tags.views.inc for the actual views integration
theme_community_tags Theme function to display a list of community tags via tagadelic.
theme_community_tags_links Theme function to display a list of community tags as simple links.
_community_tags_add_tag Add a community tag. Nid and vid and user should be valid
_community_tags_check_user If user supplied - assume has permission - otherwise use current user if has permission.
_community_tags_cleanup_orphaned_tags_by_tids Check for orphaned node terms and delete if required - by default doesn't. Provides fix for [#984462] - "When a tag is no longer attached to any nodes, (provide option to) automatically remove it from its taxonomy vocabulary"
_community_tags_convert_new_tags_to_terms Create terms for new tags and add return a simpler structure of term arrays grouped by vid.
_community_tags_delete_redundant_term @todo set flag to skip tag delete attempt in community_tags_taxonomy() invocation
_community_tags_delete_tag Delete a community tag. Nid and vid should be valid. If user is supplied tag is only removed for that user.
_community_tags_delete_tags Delete all community tags for given node and term.
_community_tags_delete_tags_for_node Delete all community tags for a given node.
_community_tags_delete_tags_for_term Delete all community tags for a given term.
_community_tags_delete_tags_for_user Delete all community tags for a given user.
_community_tags_display_handler_links Display all tags as simple links.
_community_tags_display_handler_none No all tag display.
_community_tags_display_handler_tagadelic Display all tags using tagadelic. Only called if tagadelic module is enabled. See _community_tags_get_tag_result() for definitions of $type and the arguments.
_community_tags_get_display_handler Perhaps extend with ctools. Return configured display handler or default to 'links' if configured handler not available.
_community_tags_get_display_handlers
_community_tags_get_display_handler_options Get handler options for admin form. Interim measure pending pluggable display handlers.
_community_tags_get_node_tags Retrieve list of tags for a given node.
_community_tags_get_node_user_vid_tags Retrieve list of tags for a given node and user. Includes a count of the number of users who have tagged it.
_community_tags_get_settings Get CT settings.
_community_tags_get_tag_result Helper function for retrieving a query result to pass along to the Tagadelic functions prior to theming.
_community_tags_get_user_tags Retrieve list of tags for a given user. Includes a count of the number of users who have tagged it.
_community_tags_is_opmode Determine whether such and such a CT operation mode is set for tagging in given vocabulary. Returns true if any of the modes is set.
_community_tags_is_tagging_view_visible Check that tagging form is configured for display for given node in given context. Does not check user access.
_community_tags_menu_access Menu access callback; Common access check for tag operations.
_community_tags_node_delete Node has been deleted. Delete all community tags for the deleted node. Node terms will have been removed. After ctags have been removed check for redundant terms.
_community_tags_node_insert Node has been inserted. All node terms are added to ctags attributed to the node editor.
_community_tags_node_process_tags_and_terms Process tags and terms - resolve tags (supplied as the tag name) to existing terms and identify new tags - but don't create.
_community_tags_node_update Node has been updated. All terms that are not ctags are added to ctags attributed to the current user. Removed terms are removed from ctags either for all users (sync mode) or just the current user.
_community_tags_tab_access Menu access callback; Check if the user can access the 'Tags' local task on node pages.
_community_tags_term_delete Node has been deleted. Delete all community tags for the deleted node. No Synchronisation issues. When it's gone it's gone.
_community_tags_user_delete User has been deleted. Delete all user tags for the deleted user. Apply node term deletion logic for all deleted tags.
_community_tags_vids Check whether given node type has one or more community tagged vocabularies associated with it.
_community_tags_vids_for_node_type Check whether given node type has one or more community tagged vocabularies associated with it.

Constants