' . 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 .= '
    '; $output .= '
  1. ' . t('Define one or more vocabularies whose terms will control which users have access. For example, you could define a vocabulary called \'Privacy\' with terms \'Public\' and \'Private\'.') . '
  2. '; $output .= '
  3. ' . t('Tell this module which vocabularies control privacy. (!link)', array('!link' => l(t('administer -> people -> access control by taxonomy'), 'admin/people/access/tac_lite'))) . '
  4. '; $output .= '
  5. ' . t('Configure one or more schemes. simple site may need only one scheme which grants view permission. A more complex site might require additional schemes for update and delete. Each scheme associates roles and terms. Users will be granted priviliges based on their role and the terms with which nodes are tagged.') . '
  6. '; $output .= '
  7. ' . t('When settings are correct, rebuild node_access permissions.', array( '!url' => url('admin/reports/status/rebuild'), )) . '
  8. '; $output .= '
  9. ' . t('Optionally, grant access to individual users. (See the access by taxonomy tab, under user -> edit.)') . '
  10. '; $output .= '
'; $output .= '

' . t('Troubleshooting:.') . '

'; return $output; break; } } /** * Implementation of hook_perm(). */ function tac_lite_permission() { return array( 'administer tac_lite' => array( 'title' => t('administer tac_lite'), 'description' => t('TODO Add a description for \'administer tac_lite\''), ), ); } /** * Implementation of hook_menu(). */ function tac_lite_menu() { global $user; $items = array(); $items['admin/config/people/tac_lite'] = array( 'title' => 'Access by Taxonomy', 'description' => "taxonomy-based permissions by tac_lite", 'page callback' => 'drupal_get_form', 'page arguments' => array('tac_lite_admin_settings'), 'weight' => 1, // after 'roles' tab 'access arguments' => array('administer tac_lite'), ); $items['admin/config/people/tac_lite/settings'] = array( 'title' => 'Settings', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -1, 'access arguments' => array('administer tac_lite'), ); $schemes = variable_get('tac_lite_schemes', 1); for ($i = 1; $i <= $schemes; $i++) { $scheme = variable_get('tac_lite_config_scheme_'. $i, FALSE); if ($scheme) { $title = $scheme['name']; } else { $title = "Scheme $i"; } $items['admin/config/people/tac_lite/scheme_' . $i] = array( 'title' => $title, 'page callback' => 'tac_lite_admin_settings_scheme', 'page arguments' => array((string)$i), 'type' => MENU_LOCAL_TASK, 'weight' => $i, 'access arguments' => array('administer tac_lite'), ); } return $items; } /** * Returns the settings form */ function tac_lite_admin_settings($form, &$form_state) { $vocabularies = taxonomy_get_vocabularies(); if (!count($vocabularies)) { $form['body'] = array( '#type' => 'markup', '#markup' => t('You must create a vocabulary before you can use tac_lite.', array('!url' => url('admin/structure/taxonomy/add/vocabulary'))), ); return $form; } else { $options = array(); foreach ($vocabularies as $vid => $vocab) { $options[$vid] = $vocab->name; } $form['tac_lite_categories'] = array( '#type' => 'select', '#title' => t('Vocabularies'), '#default_value' => variable_get('tac_lite_categories', NULL), '#options' => $options, '#description' => t('Select one or more vocabularies to control privacy.
Use caution with hierarchical (nested) taxonomies as visibility settings may cause problems on node edit forms.
Do not select free tagging vocabularies, they are not supported.'), '#multiple' => TRUE, '#required' => TRUE, ); $scheme_options = array(); // Currently only view, edit, delete permissions possible, so 7 // permutations will be more than enough. for ($i = 1; $i < 8; $i++) $scheme_options[$i] = $i; $form['tac_lite_schemes'] = array( '#type' => 'select', '#title' => t('Number of Schemes'), '#description' => t('Each scheme allows for a different set of permissions. For example, use scheme 1 for read-only permission; scheme 2 for read and update; scheme 3 for delete; etc. Additional schemes increase the size of your node_access table, so use no more than you need.'), '#default_value' => variable_get('tac_lite_schemes', 1), '#options' => $scheme_options, '#required' => TRUE, ); $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, ); $ret = system_settings_form($form); // Special handling is required when this form is submitted. $ret['#submit'][] = '_tac_lite_admin_settings_submit'; return $ret; } } /** * This form submit callback ensures that the form values are saved, and also * the node access database table is rebuilt. * 2008 : Modified by Paulo to be compliant with drupal 6 */ function _tac_lite_admin_settings_submit($form, &$form_state) { $rebuild = $form_state['values']['tac_lite_rebuild']; // Rebuild the node_access table. if ($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'); } // And rebuild menus, in case the number of schemes has changed. menu_rebuild(); variable_del('tac_lite_rebuild'); // We don't need to store this as a system variable. } /** * Menu callback to create a form for each scheme. * @param $i * The index of the scheme that we will be creating a form for. Passed in as a page argument from the menu. */ function tac_lite_admin_settings_scheme($i) { return drupal_get_form('tac_lite_admin_scheme_form', $i); } /** * helper function */ function _tac_lite_config($scheme) { // different defaults for scheme 1 if ($scheme === 1) { $config = variable_get('tac_lite_config_scheme_' . $scheme, array( 'name' => t('read'), 'perms' => array('grant_view'), )); } else { $config = variable_get('tac_lite_config_scheme_' . $scheme, array( 'name' => NULL, 'perms' => array(), )); } // Merge defaults, for backward compatibility. $config += array( 'term_visibility' => (isset($config['perms']['grant_view']) && $config['perms']['grant_view']), 'unpublished' => FALSE, ); // For backward compatability, use naming convention for scheme 1 if ($scheme == 1) { $config['realm'] = 'tac_lite'; } else { $config['realm'] = 'tac_lite_scheme_' . $scheme; } return $config; } /** * Returns the form for role-based privileges. */ function tac_lite_admin_scheme_form($form, $form_state, $i) { $vids = variable_get('tac_lite_categories', NULL); $roles = user_roles(); $config = _tac_lite_config($i); $form['#tac_lite_config'] = $config; if (count($vids)) { $form['tac_lite_config_scheme_' . $i] = array('#tree' => TRUE); $form['tac_lite_config_scheme_' . $i]['name'] = array( '#type' => 'textfield', '#title' => t('Scheme name'), '#description' => t('A human-readable name for administrators to see. For example, \'read\' or \'read and write\'.'), '#default_value' => $config['name'], '#required' => TRUE, ); // Currently, only view, update and delete are supported by node_access $options = array( 'grant_view' => 'view', 'grant_update' => 'update', 'grant_delete' => 'delete', ); $form['tac_lite_config_scheme_' . $i]['perms'] = array( '#type' => 'select', '#title' => t('Permissions'), '#multiple' => TRUE, '#options' => $options, '#default_value' => $config['perms'], '#description' => t('Select which permissions are granted by this scheme.
Note when granting update, it is best to enable visibility on all terms. Otherwise a user may unknowingly remove invisible terms while editing a node.'), '#required' => FALSE, ); $form['tac_lite_config_scheme_' . $i]['unpublished'] = array( '#type' => 'checkbox', '#title' => t('Apply to unpublished content'), '#description' => t('If checked, permissions in this scheme will apply to unpublished content. If this scheme includes the view permission, then unpublished nodes will be visible to users whose roles would grant them access to the published node.'), '#default_value' => $config['unpublished'], ); $form['tac_lite_config_scheme_' . $i]['term_visibility'] = array( '#type' => 'checkbox', '#title' => t('Visibility'), '#description' => t('If checked, this scheme determines whether a user can view terms. Note the view permission in the select field above refers to node visibility. This checkbox refers to term visibility, for example in a content edit form or tag cloud.'), '#default_value' => $config['term_visibility'], ); $form['helptext'] = array( '#type' => 'markup', '#markup' => t('To grant to an individual user, visit the access by taxonomy tab on the account edit page.'), '#prefix' => '

', '#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); } }