' . t('') . '
'; $output = '' . t('Taxonomy Access Control Lite allows you to restrict access to site content. It uses a simple scheme based on Taxonomy, Users and Roles.') . '
'; $output .= '' . t('This module leverages Drupal\'s node_access table allows this module to grant permission to view, update, and delete nodes. To control which users can create new nodes, use Drupal\'s role based permissions.') . '
'; $output .= '' . t('It is important to understand that this module grants privileges, as opposed to revoking privileges. So, use Drupal\'s built-in permissions to hide content from certain roles, then use this module to show the content. This module cannot hide content that the user is allowed to see based on their existing privileges.') . '
'; $output .= '' . t('There are several steps required to set up Taxonomy Access Control Lite.') . '
'; $output .= '' . t('Troubleshooting:.') . '
', '#suffix' => '
', ); $form['helptext2'] = array( '#type' => 'markup', '#markup' => t('To grant by role, select the terms below.'), '#prefix' => '', '#suffix' => '
', ); $vocabularies = taxonomy_get_vocabularies(); $all_defaults = variable_get('tac_lite_grants_scheme_' . $i, array()); $form['tac_lite_grants_scheme_' . $i] = array('#tree' => TRUE); foreach ($roles as $rid => $role_name) { $form['tac_lite_grants_scheme_' . $i][$rid] = array( '#type' => 'fieldset', '#tree' => TRUE, '#title' => check_plain(t('Grant permission by role: !role', array('!role' => $role_name))), '#description' => t(''), '#collapsible' => TRUE, ); $defaults = isset($all_defaults[$rid]) ? $all_defaults[$rid] : NULL; foreach ($vids as $vid) { // Build a taxonomy select form element for this vocab $v = $vocabularies[$vid]; $tree = taxonomy_get_tree($v->vid); $options = array(0 => '<' . t('none') . '>'); if ($tree) { foreach ($tree as $term) { $choice = new stdClass(); $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name); $options[] = $choice; } } $default_values = isset($defaults[$vid]) ? $defaults[$vid] : NULL; $form['tac_lite_grants_scheme_' . $i][$rid][$vid] = _tac_lite_term_select($v, $default_values); } } $form['tac_lite_rebuild'] = array( '#type' => 'checkbox', '#title' => t('Rebuild content permissions now'), '#default_value' => FALSE, // default false because usually only needed after scheme has been changed. '#description' => t('Do this once, after you have fully configured access by taxonomy.'), '#weight' => 9, ); $form['#submit'][] = 'tac_lite_admin_scheme_form_submit'; return system_settings_form($form); } else { $form['tac_lite_help'] = array( '#type' => 'markup', '#prefix' => '', '#suffix' => '
', '#markup' => t('First, select one or more vocabularies on the settings tab. Then, return to this page to complete configuration.', array('!url' => url('admin/config/people/tac_lite/settings')))); return $form; } } /** * Submit function for admin settings form to rebuild the menu. */ function tac_lite_admin_scheme_form_submit($form, &$form_state) { variable_set('menu_rebuild_needed', TRUE); // Rebuild the node_access table. if ($form_state['values']['tac_lite_rebuild']) { node_access_rebuild(TRUE); } else { drupal_set_message(t('Do not forget to rebuild node access permissions after you have configured taxonomy-based access.', array( '!url' => url('admin/reports/status/rebuild'), )), 'warning'); } variable_del('tac_lite_rebuild'); // We don't need to store this as a system variable. } /** * Implementation of hook_user_categories * * Creates the user edit category form for tac_lite's user-specific permissions under user/edit */ function tac_lite_user_categories() { return array( array( 'name' => 'tac_lite', 'title' => t('Access by taxonomy'), 'weight' => 5, 'access callback' => 'user_access', 'access arguments' => array('administer users'), ), ); } /** * Implementation of hook_form_alter(). * * @param $form * Nested array of form elements that comprise the form. * @param $form_state * A keyed array containing the current state of the form. The arguments that drupal_get_form() was originally called with are available in the array $form_state['build_info']['args']. * @param $form_id * String representing the name of the form itself. Typically this is the name of the function that generated the form. * */ function tac_lite_form_alter(&$form, &$form_state, $form_id){ // Catch for the tac_lite category on the user edit form. if ($form_id == 'user_profile_form') { if ($form['#user_category'] == 'tac_lite') { $vocabularies = taxonomy_get_vocabularies(); $vids = variable_get('tac_lite_categories', NULL); if (count($vids)) { for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) { $config = _tac_lite_config($i); if ($config['name']) { $perms = $config['perms']; if ($config['term_visibility']) { $perms[] = t('term visibility'); } $form['tac_lite'][$config['realm']] = array( '#type' => 'fieldset', '#title' => $config['name'], '#description' => t('This scheme controls %perms.', array('%perms' => implode(' and ', $perms))), '#tree' => TRUE, ); // Create a form element for each vocabulary foreach ($vids as $vid) { $v = $vocabularies[$vid]; // TODO: Should we be looking in form_state also for the default value? // (Might only be necessary if we are adding in custom validation?) $default_values = array(); if (!empty($form['#user']->data[$config['realm']])) { if (isset($form['#user']->data[$config['realm']][$vid])) { $default_values = $form['#user']->data[$config['realm']][$vid]; } } $form['tac_lite'][$config['realm']][$vid] = _tac_lite_term_select($v, $default_values); $form['tac_lite'][$config['realm']][$vid]['#description'] = t('Grant permission to this user by selecting terms. Note that permissions are in addition to those granted based on user roles.'); } } } $form['tac_lite'][0] = array( '#type' => 'markup', '#markup' => '' . t('You may grant this user access based on the schemes and terms below. These permissions are in addition to role based grants on scheme settings pages.', array('!url' => url('admin/config/people/tac_lite/scheme_1'))) . "
\n", '#weight' => -1, ); } else { // TODO: Do we need to handle the situation where no vocabularies have been set up yet / none have been assigned to tac_lite? } return $form; } } } /** * Implementation of hook_user_presave(). * * Move the tac_lite data into the data object * @param $edit * The array of form values submitted by the user. * @param $account * The user object on which the operation is performed. * @param $category * The active category of user information being edited. */ function tac_lite_user_presave(&$edit, $account, $category) { // Only proceed if we are in the tac_lite category. if ($category == 'tac_lite'){ // Go through each scheme and copy the form value into the data element for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) { $config = _tac_lite_config($i); if ($config['name']) { $edit['data'][$config['realm']] = $edit[$config['realm']]; } } } } /** * Implements hook_node_access_records(). * * We are given a node and we return records for the node_access table. In * our case, we inpect the node's taxonomy and grant permissions based on the * terms. */ function tac_lite_node_access_records($node) { // Get the tids we care about that are assigned to this node $tids = _tac_lite_get_terms($node); if (!count($tids)) { // no relevant terms found. // in drupal 4-7 we had to write a row into the database. In drupal 5 and later, it should be safe to do nothing. } else { // if we're here, the node has terms associated with it which restrict // access to the node. $grants = array(); for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) { $config = _tac_lite_config($i); // Only apply grants to published nodes, or unpublished nodes if requested in the scheme if ($node->status || $config['unpublished']) { foreach ($tids as $tid) { $grant = array( 'realm' => $config['realm'], 'gid' => $tid, // use term id as grant id 'grant_view' => 0, 'grant_update' => 0, 'grant_delete' => 0, 'priority' => 0, ); foreach ($config['perms'] as $perm) { $grant[$perm] = TRUE; } $grants[] = $grant; } } } return $grants; } } /** * Gets terms from a node that belong to vocabularies selected for use by tac_lite * * @param $node * A node object * @return * An array of term ids */ function _tac_lite_get_terms($node) { $tids = array(); // Get the vids that tac_lite cares about. $vids = variable_get('tac_lite_categories', NULL); if ($vids) { // Load all terms found in term reference fields. // This logic should work for all nodes (published or not). $terms_by_vid = tac_lite_node_get_terms($node); if (!empty($terms_by_vid)) { foreach ($vids as $vid) { if (!empty($terms_by_vid[$vid])) { foreach ($terms_by_vid[$vid] as $tid => $term) { $tids[$tid] = $tid; } } } } // The logic above should have all terms already, but just in case we use // the "original" logic below. The taxonomy module stopped writing to the // taxonomy_index for unpublished nodes, so this works only for published // nodes. $query = db_select('taxonomy_index', 'r'); $t_alias = $query->join('taxonomy_term_data', 't', 'r.tid = t.tid'); $v_alias = $query->join('taxonomy_vocabulary', 'v', 't.vid = v.vid'); $query->fields( $t_alias ); $query->condition("r.nid", $node->nid); $query->condition("t.vid", $vids, 'IN'); $result = $query->execute(); foreach ($result as $term) { if (empty($tids[$term->tid])) { watchdog('tac_lite', 'Unexpected term id %tid associated with !node. Please report this to !url.', array( '%tid' => $term->tid, '!node' => l($node->title, 'node/' . $node->nid), '!url' => 'https://drupal.org/node/1918272', ), WATCHDOG_DEBUG); } $tids[$term->tid] = $term->tid; } } elseif (user_access('administer tac_lite')) { drupal_set_message(t('tac_lite.module enabled, but not configured. No tac_lite terms associated with %title.', array( '!admin_url' => url('admin/config/people/tac_lite'), '%title' => $node->title, ))); } return $tids; } /** * In Drupal 6.x, there was taxonomy_node_get_terms(). Drupal 7.x should * provide the same feature, but doesn't. Here is our workaround, based on * https://drupal.org/comment/5573176#comment-5573176. * * We organize our data structure by vid and tid. */ function tac_lite_node_get_terms($node) { $terms = &drupal_static(__FUNCTION__); if (!isset($terms[$node->nid])) { // Get tids from all taxonomy_term_reference fields. $fields = field_info_fields(); foreach ($fields as $field_name => $field) { // Our goal is to get all terms, regardless of language, associated with the node. Does the code below do that? if ($field['type'] == 'taxonomy_term_reference' && field_info_instance('node', $field_name, $node->type)) { if (($items = field_get_items('node', $node, $field_name)) && is_array($items)) { foreach ($items as $item) { // Sometimes $item contains only tid, sometimes entire term. Thanks Drupal for remaining mysterious! // We need to term to determine the vocabulary id. if (!empty($item['taxonomy_term'])) { $term = $item['taxonomy_term']; } else { $term = taxonomy_term_load($item['tid']); } if ($term) { $terms[$node->nid][$term->vid][$term->tid] = $term; } } } } } } return isset($terms[$node->nid]) ? $terms[$node->nid] : FALSE; } /** * Helper function to build a taxonomy term select element for a form. * * @param $v * A vocabulary object containing a vid and name. * @param $default_values * An array of values to use for the default_value argument for this form element. */ function _tac_lite_term_select($v, $default_values = array()) { $tree = taxonomy_get_tree($v->vid); $options = array(0 => '<' . t('none') . '>'); if ($tree) { foreach ($tree as $term) { $choice = new stdClass(); $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name); $options[] = $choice; } } $field_array = array( '#type' => 'select', '#title' => $v->name, '#default_value' => $default_values, '#options' => $options, '#multiple' => TRUE, '#description' => $v->description, ); return $field_array; } /** * Return the term ids of terms this user is allowed to access. * * Users are granted access to terms either because of who they are, * or because of the roles they have. */ function _tac_lite_user_tids($account, $scheme) { // grant id 0 is reserved for nodes which were not given a grant id when they were created. By adding 0 to the grant id, we let the user view those nodes. $grants = array(0); $config = _tac_lite_config($scheme); $realm = $config['realm']; if (isset($account->data[$realm]) && count($account->data[$realm])) { // $account->$realm is array. Keys are vids, values are array of tids within that vocabulary, to which the user has access foreach ($account->data[$realm] as $tids) { if (count($tids)) { $grants = array_merge($grants, $tids); } } } // add per-role grants in addition to per-user grants $defaults = variable_get('tac_lite_grants_scheme_' . $scheme, array()); foreach ($account->roles as $rid => $role_name) { if (isset($defaults[$rid]) && count($defaults[$rid])) { foreach ($defaults[$rid] as $tids) { if (count($tids)) { $grants = array_merge($grants, $tids); } } } } // Because of some flakyness in the form API and the form we insert under // user settings, we may have a bogus entry with vid set // to ''. Here we make sure not to return that. unset($grants['']); return $grants; } /** * Implementation of hook_node_grants(). * * Returns any grants which may give the user permission to perform the * requested op. */ function tac_lite_node_grants($account, $op) { $grants = array(); for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) { $config = _tac_lite_config($i); if (in_array('grant_' . $op, $config['perms'])) { $grants[$config['realm']] = _tac_lite_user_tids($account, $i); } } if (count($grants)) { return $grants; } } /** * Implements hook_query_TAG_alter(). * * Acts on queries that list terms (generally these should be tagged with 'term_access') * to remove any terms that this user should not be able to see. */ function tac_lite_query_term_access_alter(QueryAlterableInterface $query) { global $user; // If this user has administer rights, don't filter if (user_access('administer tac_lite')) { return; } // Get our vocabularies and schemes from variables. Return if we have none. $vids = variable_get('tac_lite_categories', NULL); $schemes = variable_get('tac_lite_schemes', 1); if (!$vids || !count($vids) || !$schemes) { return; } // the terms this user is allowed to see $term_visibility = FALSE; $tids = array(); for ($i = 1; $i <= $schemes; $i++) { $config = _tac_lite_config($i); if ($config['term_visibility']) { $tids = array_merge($tids, _tac_lite_user_tids($user, $i)); $term_visibility = TRUE; } } if ($term_visibility) { // HELP: What is the proper way to find the alias of the primary table here? $primary_table = ''; $t = $query->getTables(); foreach($t as $key => $info) { if (!$info['join type']) { $primary_table = $info['alias']; } } // Prevent query from finding terms the current user does not have permission to see. $query->leftJoin('taxonomy_term_data', 'tac_td', $primary_table . '.tid = tac_td.tid'); $or = db_or(); $or->condition($primary_table . '.tid', $tids, 'IN'); $or->condition('tac_td.vid', $vids, 'NOT IN'); $query->condition($or); } }