You are here

community_tags.module in Community Tags 7

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

File

community_tags.module
View source
<?php

/**
 * @file
 * Implements community tagging of nodes using a specific vocabulary for Drupal v7.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);

/**
 * Implements hook_help().
 */
function community_tags_help($path, $arg) {
  switch ($path) {
    case 'admin/config/content/community-tags':
      return t('To set up community tagging, first add a term reference field to a content type, then enable community tagging here. Set where the community tags form is displayed (tab, inline, or block) on the content type settings page under "Community tags settings". <strong>Hint:</strong> Be sure to set the "number of values" setting on your term reference fields!');
      break;
  }
}

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

/**
 * Implements hook_menu().
 */
function community_tags_menu() {
  $items = array();
  $items['admin/config/content/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/config/content/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/config/content/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',
      6,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'community_tags.admin.inc',
  );
  $items['admin/config/content/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',
      6,
    ),
    '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;
}

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

  // 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' => DRUPAL_NO_CACHE,
  );
  return $block;
}

/**
 * Implements hook_block_view().
 */
function community_tags_block_view($delta) {
  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;
      }
    }
  }
}

/**
 * Implements hook_permission().
 */
function community_tags_permission() {
  return array(
    'tag content' => array(
      'title' => t('tag content'),
      'description' => t('Tag content'),
    ),
    'edit own tags' => array(
      'title' => t('edit own tags'),
      'description' => t('Add tags after initial tagging and delete own tags'),
    ),
  );
}

/*****************************************************************************
 * Community tag node hooks should be called after taxonomy module hooks - see
 * system weight in community_tags.install.
 *****************************************************************************/

/**
 * Implements hook_node_load().
 */
function community_tags_node_load($nodes, $types) {
  foreach ($nodes as $node) {
    $node->community_tags_form = _community_tags_is_tagging_view_visible($node, COMMUNITY_TAGS_MODE_INLINE);
  }
}

/**
 * Implements hook_node_insert().
 */
function community_tags_node_insert($node) {
  _community_tags_node_insert($node);
}

/**
 * Implements hook_node_update().
 */
function community_tags_node_update($node) {
  if (!isset($node->ct_user_tags)) {

    // only process if not comming from community_tags_taxonomy_node_save() or batch
    _community_tags_node_update($node);
  }
}

/**
 * Implements hook_node_delete().
 */
function community_tags_node_delete($node) {
  _community_tags_node_delete($node);
}

/**
 * Implements hook_node_view().
 */
function community_tags_node_view($node, $view_mode = 'full') {
  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.
  // NODE_BUILD_SEARCH_INDEX test no longer required - search has view_mode of search_index (or search_result)
  if ($view_mode == 'full' && isset($node->community_tags_form)) {
    $node->content['community_tags'] = array(
      '#markup' => _community_tags_node_view($node, TRUE),
      '#weight' => 50,
    );
  }
}

/**
 * Implements 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;
    }
  }
}

/**
 * Implements hook_user_cancel().
 */
function community_tags_user_cancel($edit, $account, $method) {

  // 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
  // maybe something to add with anonymous user support if method is appropriate.
  _community_tags_user_delete($user);
}

/**
 * Implements hook_user_delete().
 */
function community_tags_user_delete($account) {

  // if user is deleted then remove all ctags for the user.
  _community_tags_user_delete($user);
}

/**
 * Implements 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;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function community_tags_form_node_type_form_alter(&$form, $form_state) {

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

    // only show if content type is mapped to tagging vocabulary
    $supported_vmnames = _community_tags_vids_for_node_type($form['#node_type']->type);
    if (!empty($supported_vmnames)) {
      $modes = array(
        COMMUNITY_TAGS_MODE_BLOCK => t('Block'),
        COMMUNITY_TAGS_MODE_TAB => t('Tab'),
        COMMUNITY_TAGS_MODE_INLINE => t('Inline'),
      );
      $form['community_tags'] = array(
        '#type' => 'fieldset',
        '#title' => t('Community tags settings'),
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#group' => 'additional_settings',
      );
      $form['community_tags']['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.
 *
 * @todo D7 Make sure we're handling field instances correctly and multiple value behaviour - see devel_generate for examples
 */
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();

  // get the term fields
  $fields_by_vid = _community_tags_get_term_reference_fields(NULL, $node->type);
  $language = $node->language;

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

    // fields for this node and vocabulary
    $fields = $fields_by_vid[$vid];

    // 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)) {

        // add to all fields that don't already have it
        if (_community_tags_add_term_to_node($node, $tid, $vid) > 0) {
          $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) {

        // remove from all fields that have it
        if (_community_tags_remove_term_from_node($node, $tid, $vid) > 0) {
          $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) {
  $query = db_select('taxonomy_term_data', 't');

  // add fields
  $count_alias = $query
    ->addExpression('COUNT(t.tid)', 'count');
  $tid_alias = $query
    ->addField('t', 'tid', 'tid');
  $name_alias = $query
    ->addField('t', 'name', 'name');
  $vid_alias = $query
    ->addField('t', 'vid', 'vid');
  $description_alias = $query
    ->addField('t', 'description', 'description');

  // join common to all access types
  $query
    ->join('community_tags', 'c', 'c.tid = t.tid');

  // common group and order by
  $query
    ->groupBy($tid_alias)
    ->groupBy($name_alias)
    ->groupBy($vid_alias);
  $query
    ->orderBy($count_alias, 'DESC');
  switch ($type) {
    case 'node':
      $query
        ->condition('c.nid', (int) $arg1);
      if ($arg2) {
        $query
          ->condition('t.vid', (int) $arg2);
      }
      break;
    case 'type':
      $query
        ->join('node', 'n', 'n.nid = c.nid AND n.type = :type', array(
        ':type' => (string) $arg1,
      ));
      break;
    case 'user':
      $query
        ->condition('c.uid', (int) $arg1);
      break;
    case 'user_node':
      $query
        ->condition('c.nid', (int) $arg1)
        ->condition('c.uid', (int) $arg1);
    default:
  }
  if ($limit) {
    $query
      ->range(0, (int) $limit);
  }
  return $query
    ->execute();
}
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) {
  $tags = _community_tags_get_tag_result($type, $limit, $arg1, $arg2);
  $tags = $tags
    ->fetchAllAssoc('tid');
  return theme('community_tags_links', array(
    'tags' => $tags,
  ));
}

/**
 * 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) {
  $tags = _community_tags_get_tag_result($type, $limit, $arg1, $arg2);
  $tags = $tags
    ->fetchAllAssoc('tid');
  $weighted_tags = tagadelic_build_weighted_tags($tags);
  $sorted_tags = tagadelic_sort_tags($weighted_tags);
  return theme('community_tags', array(
    '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);
  }
  if (!$inline) {
    drupal_set_title($node->title);
  }
  module_load_include('inc', 'community_tags', 'community_tags.pages');
  $output = '';
  $vids = community_tags_vids_for_node($node);
  foreach ($vids as $vid) {

    // get fields for vid/type combination
    $fields = _community_tags_get_term_reference_fields($vid, $node->type);

    // if more than 1 doesn't matter which we use
    $field = reset($fields);
    $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);
    if (!count($tags)) {

      // User has not yet added tags to this node yet. Show form.
      $form = drupal_get_form('community_tags_form', array(
        'node' => $node,
        'cloud' => $cloud,
        'nid' => $node->nid,
        'vid' => $vid,
        'tags' => NULL,
        'inline' => $inline,
        'field' => $field,
        'names' => array(),
        'multiple' => count($vids),
      ));
      $output .= drupal_render($form);

      // $output .= $form_output;
    }
    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);
      $form = drupal_get_form('community_tags_form', array(
        'node' => $node,
        'cloud' => $cloud,
        'nid' => $node->nid,
        'vid' => $vid,
        'tags' => $tags,
        'inline' => $inline,
        'field' => $field,
        'names' => $names,
        'multiple' => count($vids),
      ));
      $output .= drupal_render($form);
    }
    else {

      // Sorry, no more adding tags for you!
      $output .= '<p>' . t('You have already tagged this post. Your tags: ') . theme('community_tags', array(
        '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'))))), array('type' => 'setting', 'scope' => JS_DEFAULT));
  }
  return $output;
}

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

/**
 * Theme function to display a list of community tags as simple links.
 *
 * @ingroup themeable
 */
function theme_community_tags_links($variables) {
  $tags = $variables['tags'];
  $links = array();
  $terms = taxonomy_term_load_multiple(array_keys($tags));
  foreach ($terms as $term) {

    // $term = taxonomy_get_term($tag->tid);
    // $uri = entity_uri('taxonomy_term', $term);
    // dpm($uri);
    // $variables['term_url']  = url($uri['path'], $uri['options']);
    $link = array(
      'title' => $term->name,
      'href' => drupal_get_path_alias('taxonomy/term/' . $term->tid),
      'attributes' => array(
        'rel' => 'tag',
        'title' => $term->description,
      ),
    );
    $links[] = $link;
  }
  return theme('links', array(
    'links' => $links,
    'attributes' => array(
      'class' => array(
        'links',
        'inline',
      ),
    ),
  ));
}

/**
 * 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;
}

/**
 * Implements 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_terms = _community_tags_node_process_term_fields($node, $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_terms = _community_tags_node_process_term_fields($node, $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);
}

/*****************************************************************************
 * Vocabulary hook changes
 ****************************************************************************/

/**
 * Implements hook_taxonomy_vocabulary_update().
 */
function community_tags_taxonomy_vocabulary_update($vocabulary) {

  // Reflect machine name changes in community_tags settings
  if (!empty($vocabulary->old_machine_name) && $vocabulary->old_machine_name != $vocabulary->machine_name) {
    $settings = variable_get('community_tags_vocabularies', array());
    if (!empty($settings[$vocabulary->old_machine_name])) {
      $settings[$vocabulary->machine_name] = $settings[$vocabulary->old_machine_name];
      unset($settings[$vocabulary->old_machine_name]);
      variable_set('community_tags_vocabularies', $settings);
    }
  }
}

/*****************************************************************************
 * 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;
    }
    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);
      if (_community_tags_remove_mixed_terms_from_node($node, $terms_to_remove) > 0) {

        // 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
  // @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);
  }
}

/*****************************************************************************
 * Helpers for adding and removing terms from nodes.
 ****************************************************************************/

/**
 * Helper to add terms to a node - ready for saving with node_save.
 *
 * @param $terms_to_add
 *  An array of terms to add. Keys must be tids, values must be an 'term' array
 *  which must include at least the vid - e.g. array(12 => array('vid' => 1),...)
 */
function _community_tags_add_mixed_terms_to_node(&$node, $terms_to_add) {
  $count_of_terms_added = FALSE;

  // add to all fields that don't have it
  foreach ($terms_to_add as $tid => $tag) {
    $count_of_terms_added += _community_tags_add_term_to_node($node, $tid, $tag['vid']);
  }
  return $count_of_terms_added;
}

/**
 * Helper to add terms to a node - ready for saving with node_save.
 *
 * @param $tids_to_add
 *  An array of tids to add.
 */
function _community_tags_add_terms_to_node(&$node, $tids_to_add, $vid) {
  $count_of_terms_added = 0;

  // add to all fields that don't have it
  foreach ($terms_to_add as $tid) {
    $count_of_terms_added += _community_tags_add_term_to_node($node, $tid, $vid);
  }
  return $count_of_terms_added;
}

/**
 * Add single term to node. Ready for node_save().
 */
function _community_tags_add_term_to_node(&$node, $tid, $vid) {
  $count_of_terms_added = 0;

  // get the term fields for this node and vid
  $fields = _community_tags_get_term_reference_fields($vid, $node->type);

  // language doesn't appear to be used for the field
  // $language = $node->language;
  $language = 'und';

  // add to all fields that don't have it
  // only add if node has one or more term reference fields for the term's vocabulary
  if (!empty($fields)) {

    // check terms present in each term reference field and add if not already there
    foreach ($fields as $field_name => $field) {
      $term_field =& $node->{$field_name};

      // if it's the first tag create a new array
      if (empty($term_field) || !array_key_exists($language, $term_field)) {
        $term_field[$language] = array(
          array(
            'tid' => $tid,
          ),
        );
        $count_of_terms_added += 1;
      }
      else {
        if (FALSE === ($index = _community_tags_term_reference_field_search($term_field, $language, $tid))) {
          $term_field[$language][] = array(
            'tid' => $tid,
          );
          $count_of_terms_added += 1;
        }
      }
    }
  }
  return $count_of_terms_added;
}

/**
 * Utility to search for tid in term reference field.
 */
function _community_tags_term_reference_field_search($term_field, $language, $tid) {
  foreach ($term_field[$language] as $index => $field_term) {
    if ($field_term['tid'] == $tid) {
      return $index;
    }
  }
  return FALSE;
}

/**
 * Helper to remove terms from a node - ready for saving with node_save.
 *
 * @param $terms_to_remove
 *  An array of terms to remove. Keys must be tids, values must be an 'term' array
 *  which must include at least the vid - e.g. array(12 => array('vid' => 1),...)
 */
function _community_tags_remove_mixed_terms_from_node(&$node, $terms_to_remove) {
  $count_of_terms_removed = 0;

  // remove from all fields that have it
  foreach ($terms_to_remove as $tid => $tag) {
    $count_of_terms_removed += _community_tags_remove_term_from_node($node, $tid, $tag['vid']);
  }
  return $count_of_terms_removed;
}

/**
 * Helper to remove terms from a node - ready for saving with node_save.
 *
 * @param $tids_to_remove
 */
function _community_tags_remove_terms_from_node(&$node, $terms_to_remove, $vid) {
  $count_of_terms_removed = 0;

  // remove from all fields that have it
  foreach ($terms_to_remove as $tid) {
    $count_of_terms_removed += _community_tags_remove_term_from_node($node, $tid, $vid);
  }
  return $count_of_terms_removed;
}

/**
 * Helper to remove terms from a node - ready for saving with node_save.
 *
 * @param $terms_to_remove
 *  An array of terms to remove. Keys must be tids, values must be an 'term' array
 *  which must include at least the vid - e.g. array(12 => array('vid' => 1),...)
 */
function _community_tags_remove_term_from_node(&$node, $tid, $vid) {
  $count_of_terms_removed = 0;

  // get the term fields for this node and vid
  $fields = _community_tags_get_term_reference_fields($vid, $node->type);

  // $language = $node->language;
  $language = 'und';

  // remove from all fields that have it
  if (!empty($fields)) {
    foreach ($fields as $field_name => $field) {
      $term_field =& $node->{$field_name};
      if (FALSE !== ($index = _community_tags_term_reference_field_search($term_field, $language, $tid))) {
        unset($term_field[$language][$index]);
        $count_of_terms_removed += 1;
      }
    }
  }
  return $count_of_terms_removed;
}

/*****************************************************************************
 * 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 = REQUEST_TIME;
  $id = db_insert('community_tags')
    ->fields(array(
    'tid' => $tid,
    'nid' => $nid,
    'uid' => $uid,
    'date' => $time,
  ))
    ->execute();
}

/**
 * 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_delete('community_tags')
    ->condition('nid', $nid)
    ->condition('tid', $tid)
    ->condition('uid', $uid)
    ->execute();
}

/**
 * Delete all community tags for given node and term.
 */
function _community_tags_delete_tags($nid, $tid) {
  db_delete('community_tags')
    ->condition('nid', $nid)
    ->condition('tid', $tid)
    ->execute();
}

/**
 * Delete all community tags for a given node.
 */
function _community_tags_delete_tags_for_node($nid) {
  db_delete('community_tags')
    ->condition('nid', $nid)
    ->execute();
}

/**
 * Delete all community tags for a given term.
 */
function _community_tags_delete_tags_for_term($tid) {
  db_delete('community_tags')
    ->condition('tid', $tid)
    ->execute();
}

/**
 * Delete all community tags for a given user.
 */
function _community_tags_delete_tags_for_user($uid) {
  db_delete('community_tags')
    ->condition('uid', $uid)
    ->execute();
}

/**
 * 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, and has no children
    // TODO Please convert this statement to the D7 database API syntax.
    $results = db_query("SELECT td.* FROM {taxonomy_term_data} td\n       LEFT JOIN {taxonomy_term_hierarchy} th ON th.parent = td.tid\n       LEFT JOIN {community_tags} ct ON ct.tid = td.tid\n       WHERE td.tid IN (:tids)\n       AND ct.tid IS NULL\n       AND th.parent IS NULL", array(
      ':tids' => $tids,
    ));
    foreach ($results as $row) {
      _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_term_delete($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);
}

/**
 * Utility function to get a vocabuary by name.
 */
function _community_tags_get_vocabularies_by_name() {
  static $vocabularies_by_name;
  if (!isset($vocabularies_by_name)) {
    $vocabularies_by_name = array();
    foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) {
      $vocabularies_by_name[$vocabulary->machine_name] = $vocabulary;
    }
  }
  return $vocabularies_by_name;
}

/**
 * Get term reference fields
 */
function _community_tags_get_term_reference_fields($vid = NULL, $type = NULL) {
  static $fields_by_vid, $fields_by_type;
  if (!isset($fields_by_vid)) {
    $fields_by_vid = array();
    $fields_by_type = array();
    $fields = field_info_fields();
    $vocabularies = _community_tags_get_vocabularies_by_name();
    foreach ($fields as $field_name => $field) {

      // $field['bundles'] contains names of bundles and entities associated with this field.
      // keys are entity types, values are arrays of bundle names.
      if ($field['type'] == 'taxonomy_term_reference' && !empty($field['bundles']['node'])) {
        foreach ($field['bundles']['node'] as $node_type) {
          foreach ($field['settings']['allowed_values'] as $allowed_values) {
            if (isset($vocabularies[$allowed_values['vocabulary']])) {
              $vocabulary = $vocabularies[$allowed_values['vocabulary']];
              $fields_by_vid[$vocabulary->vid][$node_type][$field['field_name']] = $field;
              $fields_by_type[$node_type][$vocabulary->vid][$field['field_name']] = $field;
            }
          }
        }
      }
    }
  }
  if (isset($vid) && isset($type)) {
    return !empty($fields_by_vid[$vid][$type]) ? $fields_by_vid[$vid][$type] : array();
  }
  elseif (isset($vid)) {
    return !empty($fields_by_vid[$vid]) ? $fields_by_vid[$vid] : array();
  }
  elseif (isset($type)) {
    return !empty($fields_by_type[$type]) ? $fields_by_type[$type] : array();
  }
  else {
    return $fields_by_vid;
  }
}

/**
 * Check whether given node type has one or more community tagged vocabularies associated with it.
 * @return
 *  Array of vocabulary vids.
 */
function _community_tags_vids($type = NULL) {
  $community_tagged = variable_get('community_tags_vocabularies', array(
    'tags' => 'tags',
  ));
  $term_reference_fields = _community_tags_get_term_reference_fields(NULL, $type);

  // convert vocabulary machine names used in settings and field info to vids.
  // and get enabled valid vocabularies (for the given type).
  $vocabularies = taxonomy_get_vocabularies();
  $vids = array();
  foreach ($vocabularies as $vid => $vocabulary) {
    if (isset($community_tagged[$vocabulary->machine_name]) && isset($term_reference_fields[$vid])) {
      $vids[$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.
 *
 * @return
 *  Array of settings keyed on vid.
 */
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());

    // all vocabularies that are assigned to node (entity) types
    $term_reference_fields_by_vid = _community_tags_get_term_reference_fields();
    $settings = array();
    $valid_settings = array();
    foreach (taxonomy_get_vocabularies() as $_vid => $vocabulary) {
      $vname = $vocabulary->machine_name;
      $settings[$_vid] = array(
        'name' => $vocabulary->name,
        'machine_name' => $vname,
        'tagging' => TRUE,
        'types' => array(),
      );
      $settings[$_vid]['CT_enabled'] = isset($valid_CT_vocabularies[$vname]);
      if (!empty($term_reference_fields_by_vid[$_vid])) {
        foreach ($term_reference_fields_by_vid[$_vid] as $type => $fields) {
          if ($type_info = node_type_get_type($type)) {

            // create structure grouped on vocabulary
            foreach ($fields as $field_name => $field) {
              $settings[$_vid]['types'][$type]['fields'][$field_name] = array(
                'field_name' => $field_name,
                'CT_enabled' => FALSE,
                'opmode' => COMMUNITY_TAGS_OPMODE_SYNC,
                'display_handler' => $default_display_handler,
              );
            }
            if (isset($valid_CT_vocabularies[$vname]['types'][$type])) {
              $settings[$_vid]['types'][$type] = $valid_CT_vocabularies[$vname]['types'][$type];
              $settings[$_vid]['types'][$type]['type_name'] = $type_info->name;
              $settings[$_vid]['types'][$type]['assigned'] = TRUE;
            }
            else {
              $settings[$_vid]['types'][$type] = array(
                'type_name' => $type_info->name,
                'assigned' => TRUE,
                'opmode' => COMMUNITY_TAGS_OPMODE_SYNC,
                'display_handler' => $default_display_handler,
              );
            }
            if (isset($valid_CT_vocabularies[$vname])) {
              $valid_settings[$_vid] = $settings[$_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) {
    $result = db_query("SELECT t.tid, t.vid, t.name, count(t.tid) tag_count FROM {taxonomy_term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = :nid AND t.vid IN (:vids) GROUP BY t.tid", array(
      ':nid' => $nid,
      ':vids' => $vids,
    ));
  }
  else {
    $result = db_query("SELECT t.tid, t.vid, t.name, count(t.tid) tag_count FROM {taxonomy_term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = :nid GROUP BY t.tid", array(
      ':nid' => $nid,
    ));
  }
  foreach ($result as $term) {
    $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 = :nid AND uid = :uid) AS ct\n     INNER JOIN {taxonomy_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 = :vid\n     GROUP BY t.tid", array(
    ':nid' => $nid,
    ':uid' => $uid,
    ':vid' => $vid,
  ));
  foreach ($result as $term) {
    $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 = :uid) AS ct\n     INNER JOIN {taxonomy_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", array(
    ':uid' => $uid,
  ));
  foreach ($result as $term) {
    $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();
  $records = db_query("SELECT t.tid, t.name, c.uid, c.nid FROM {taxonomy_term_data} t INNER JOIN {community_tags} c ON c.tid = t.tid WHERE c.nid = :nid AND c.uid = :uid AND t.vid = :vid ORDER BY t.name", array(
    ':nid' => $nid,
    ':uid' => $uid,
    ':vid' => $vid,
  ));
  foreach ($records as $term) {
    $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.
 *
 * Get all terms referenced by term_reference_fields attached to the node
 * for the given vocabularies.
 */
function _community_tags_node_process_term_fields($node, $vids) {
  $processed_terms = array();
  $fields_by_vid = _community_tags_get_term_reference_fields(NULL, $node->type);
  foreach ($fields_by_vid as $vid => $fields) {

    // only collect terms for selected vocabularies
    if (isset($vids[$vid])) {
      foreach ($fields as $field_name => $field) {
        $items = field_get_items('node', $node, $field_name);
        if (!empty($items)) {
          foreach ($items as $item) {
            $processed_terms[$vid][$item['tid']] = $item['tid'];
          }
        }
      }
    }
  }
  return $processed_terms;
}

/**
 * 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);
              $match = FALSE;

              // tid match if any.
              foreach ($matching_terms as $matching_term) {
                if ($matching_term->vid == $vid) {
                  $match = TRUE;
                  break;
                }
              }
              if (!$match) {
                $processed_terms[$vid]['new tags'][] = $tag;
              }
              else {
                $processed_terms[$vid]['terms'][$matching_term->tid] = $matching_term;
              }
            }
          }
        }
      }
      else {
        if (is_array($term)) {
          foreach ($term as $tid) {
            if ($tid) {
              $term_object = taxonomy_term_load($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_term_load($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.
        $new_term = (object) array(
          'vid' => $vid,
          'name' => $tag_name,
        );

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

Functions

Namesort descending Description
community_tags_block_info Implements hook_block_info().
community_tags_block_view Implements hook_block_view().
community_tags_content_extra_fields Implements hook_content_extra_fields().
community_tags_flatten Helper function for the JS tagger.
community_tags_form_node_type_form_alter Implements hook_form_FORM_ID_alter().
community_tags_get_user_node_tags Retrieve list of tags for a given node that belong to a user.
community_tags_help Implements hook_help().
community_tags_menu Implements hook_menu().
community_tags_node_delete Implements hook_node_delete().
community_tags_node_insert Implements hook_node_insert().
community_tags_node_load Implements hook_node_load().
community_tags_node_update Implements hook_node_update().
community_tags_node_view Implements hook_node_view().
community_tags_permission Implements hook_permission().
community_tags_taxonomy Implements 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_taxonomy_vocabulary_update Implements hook_taxonomy_vocabulary_update().
community_tags_theme Implements hook_theme().
community_tags_user_cancel Implements hook_user_cancel().
community_tags_user_delete Implements hook_user_delete().
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 Implements 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_mixed_terms_to_node Helper to add terms to a node - ready for saving with node_save.
_community_tags_add_tag Add a community tag. Nid and vid and user should be valid
_community_tags_add_terms_to_node Helper to add terms to a node - ready for saving with node_save.
_community_tags_add_term_to_node Add single term to node. Ready for node_save().
_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_term_reference_fields Get term reference fields
_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_get_vocabularies_by_name Utility function to get a vocabuary by name.
_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_process_term_fields 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_node_view Community tags callback for node view.
_community_tags_remove_mixed_terms_from_node Helper to remove terms from a node - ready for saving with node_save.
_community_tags_remove_terms_from_node Helper to remove terms from a node - ready for saving with node_save.
_community_tags_remove_term_from_node Helper to remove terms from a node - ready for saving with node_save.
_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_term_reference_field_search Utility to search for tid in term reference field.
_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