Module Grants project page', array('@module_grants' => url('http://drupal.org/project/module_grants'))); break; } return empty($s) ? '' : '

'. $s .'

'; } /** * Implementation of hook_menu(). */ function module_grants_menu() { $items = array(); $items['admin/settings/module_grants'] = array( 'title' => 'Module grants', 'description' => 'Configure how node access modules interact and customise the Accessible-content page.', 'page callback' => 'drupal_get_form', 'page arguments' => array('module_grants_admin_configure'), 'access arguments' => array('administer site configuration'), 'file' => 'module_grants.admin.inc' ); return $items; } /** * Implementation of hook_menu_alter(). * * Modify menu items defined in other modules, in particular the node module. */ function module_grants_menu_alter(&$items) { // As module_grants_node_access() fixes the problem of grants not being // checked when a node isn't published, all node access menu links are // altered to use function module_grants_node_access(). // For normal view/edit/delete operations module_grant_node_access() is // called directly, for the revision-specific operations the function is // called via module_grants_node_revision_access(). // ---- Node-related access callbacks $items['node/%node']['access callback'] = 'module_grants_node_access'; $items['node/%node']['access arguments'] = array('view', 1); // [#828262] $items['node/%node/view']['access callback'] = 'module_grants_node_access'; $items['node/%node/view']['access arguments'] = array('view', 1); // @todo see http://drupal.org/node/756100 $items['node/%node/edit']['access callback'] = 'module_grants_node_access'; // Need to override delete because node.module's node_delete() calls // node_access() directly when module_grants_node_access() should be used. $items['node/%node/delete']['page arguments'] = array('module_grants_node_delete_confirm', 1); $items['node/%node/delete']['access callback'] = 'module_grants_node_access'; $items['node/%node/delete']['module'] = 'module_grants'; $items['node/%node/delete']['file'] = 'module_grants.pages.inc'; $items['node/%node/delete']['file path'] = drupal_get_path('module', 'module_grants'); // ---- Revision-related access callbacks $items['node/%node/revisions']['access callback'] = 'module_grants_node_revision_access'; $items['node/%node/revisions']['access arguments'] = array('view revision list', 1); // Point /%node/revisions/%/view page to same callback as /%node/view (see // node.module) for a consistent view of current, pending, archived revisions $items['node/%node/revisions/%/view']['page callback'] = 'node_page_view'; $items['node/%node/revisions/%/view']['access callback'] = 'module_grants_node_revision_access'; $items['node/%node/revisions/%/view']['access arguments'] = array('view revisions', 1); $items['node/%node/revisions/%/delete']['access callback'] = 'module_grants_node_revision_access'; $items['node/%node/revisions/%/delete']['access arguments'] = array('delete revisions', 1); $items['node/%node/revisions/%/revert']['access callback'] = 'module_grants_node_revision_access'; $items['node/%node/revisions/%/revert']['access arguments'] = array('revert revisions', 1); if (isset($items['node/%node/translate'])) { // [#766504] $items['node/%node/translate']['access callback'] = '_module_grants_translation_tab_access'; } } /** * Similar to _translation_tab_access(), but uses module_grants_node_access() * instead of node_access(). * * @param $node * The node for which translation tab access is checked */ function _module_grants_translation_tab_access($node) { return !empty($node->language) && translation_supported_type($node->type) && user_access('translate content') && module_grants_node_access('view', $node); } /** * Similar to node_access() in node.module but ANDs rather than ORs grants * together on a per module base to create more natural behaviour. * Also makes sure that published and unpublished content are treated * in the same way, i.e. that grants are checked in either case. * * @param $node_op * One of 'view', 'update' or 'delete'. We don't need to deal with 'create'. * @param $node * The node for which the supplied operation is checked * @param $account * user object, use NULL or omit for current user * @return * FALSE if the supplied operation isn't permitted on the node */ function module_grants_node_access($node_op, $node = NULL, $account = NULL) { static $access = array(); global $user; if (!$node) { return FALSE; } $nid = $node->nid; // If no account object is supplied, the access check is for the current user. if (empty($account)) { $account = $user; } $uid = $account->uid; if (isset($access["$uid:$nid:$node_op"])) { return $access["$uid:$nid:$node_op"]; } // If the node is in a restricted format, disallow editing. if ($node_op == 'update' && !filter_access($node->format)) { return ($access["$uid:$nid:$node_op"] = FALSE); } if (user_access('administer nodes', $account)) { return $access["$uid:$nid:$node_op"] = TRUE; } if (!user_access('access content', $account)) { return $access["$uid:$nid:$node_op"] = FALSE; } $module = node_get_types('module', $node); if ($module == 'node') { $module = 'node_content'; } $result = module_invoke($module, 'access', $node_op, $node, $account); if (!is_null($result)) { //drupal_set_message("'$node_op' access=$result by module $module: '$node->title'", 'warning', FALSE); return $access["$uid:$nid:$node_op"] = $result; } // Having arrived here, node access has still neither been granted nor denied. // We're about to hand over to enabled content access modules, that is those // that implement hook_node_grants() and consult the node_access table. // By default the node_access table allows 'view' access to all and does not // take the node's publication status into account. This would mean that // anonymous users would be able to view content that isn't published, // assuming they have the 'access content' permission, which is normal. // Therefore, to differentiate view access for unpublished content between // anonymous and authorised users, we only allow view access to unpublished // content to roles that have the 'view revisions' or 'view revisions of // any|own content" permissions (from Revisioning). // So, do NOT give any of these view revisions permissions to the anonymous // user role. // The exception are authors viewing their own content. It would be silly to // disallow authors viewing the (unpublished) content they just saved! // if ($node_op == 'view' && !$node->status) { $may_view = module_invoke('revisioning', 'user_node_access', 'view revisions', $node) || user_access('view revisions'); if (!$may_view) { if ($uid != $node->uid) { // Not the author: no permission to view this unpublished content. return $access["$uid:$nid:$node_op"] = FALSE; } } } $base_sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid=0 OR nid=%d) AND ((gid=0 AND realm='all')"; // If module_grants_lenient is set, then a content access module that has // nothing to say about the node in question will be deemed to be ok with // $account having access to $node. // If module_grants_lenient isn't set and a content access module has nothing // to say about the node in question this will be taken as a 'deny access'. $nid1 = variable_get('module_grants_lenient', TRUE) ? $nid : NULL; $all_grants = _module_grants_by_module($node_op, $account, $nid1); if (count($all_grants) == 0) { // no module implements hook_node_grants() // Note that in the absence of any content access modules the node_access // table by default contains a single row that grants the 'all' realm // 'view' access to all nodes via nid=0. $sql = "$base_sql) AND grant_$node_op >=1"; $result = db_result(db_query($sql, $nid)); //drupal_set_message("'$node_op' access=$result by core (via node_access table): '$node->title'", 'warning', FALSE); return $access["$nid:$node_op"] = $result; } $or_modules = variable_get('module_grants_OR_modules', FALSE); foreach ($all_grants as $module => $module_grants) { $sql = $base_sql . (empty($module_grants) ? "" : " OR ($module_grants)") .") AND grant_$node_op >=1"; // Effectively AND module_grants together by breaking loop as soon as one fails // A single SQL statement may be slightly quicker but won't tells us // which of the modules denied access. This is useful debug feedback. $result = db_result(db_query($sql, $nid)); //drupal_set_message("'$node_op' access=$result by $module-grants: '$node->title'", 'warning', FALSE); if ($or_modules) { if ($result > 0) { // OR module grants together: break as soon as one succeeds break; } } elseif ($result == 0) { // AND module grants together: break as soon as one fails break; } } return $access["$uid:$nid:$node_op"] = $result; } /** * Menu options dealing with revisions have their revision-specific * permissions checked via user_access(), before being tested for the * associated node-specific operation. * Return a boolean indicating whether the current user has access to the * requested revision AND node. * * @param $revision_op * The requested revision operation, e.g. 'view revisions'. * @param $node * Node object for which revision access is requested. * @return * TRUE when the current user has the requested access to the supplied revision * * @see node.module, _node_revision_access() * * Note, unlike _node_revision_access(), it is ok to also call this function * on nodes that have only a single revision. * Also unlike _node_revision_access(), the function below makes sure not to * cache access to a revision based on vid alone, as different revision * operations may be requested by various modules in response to a single HTTP * request (read: mouse-click). */ function module_grants_node_revision_access($revision_op, $node) { static $access = array(); if (!$node) { return FALSE; } $vid = $node->vid; if (isset($access["$vid:$revision_op"])) { return $access["$vid:$revision_op"]; } if (!isset($node->num_revisions) || !isset($node->is_current)) { drupal_set_message('Node object data incomplete -- have you enabled the Node Tools submodule?', 'warning', FALSE); } // See if other modules have anything to say about this revision_op, i.e. // whether they implement hook_user_node_access($revision_op, $node). $or_modules = variable_get('module_grants_OR_modules', FALSE); $hook = 'user_node_access'; foreach (module_implements($hook) as $module) { $result = module_invoke($module, $hook, $revision_op, $node); if (!is_null($result)) { if ($or_modules) { if ($result) { // OR permissions together: return as soon as one succeeds break; } } elseif (!$result) { // AND permissons together: return as soon as one fails break; } } } // If no module implements hook_user_node_access() for this revision_op, // then fall back to the equivalent of what _node_revision_access() does, i.e. // check user permission, followed by node access. $node_op = is_null($result) ? _module_grants_user_node_access($revision_op, $node) : $result; if ($node_op && $node_op != 'view' && $node_op != 'update' && $node_op != 'delete') { drupal_set_message($module .'_'. $hook ." returns illegal node operation 'node_op'", 'warning', FALSE); } $access["$vid:$revision_op"] = $node_op && module_grants_node_access($node_op, $node); return $access["$vid:$revision_op"]; } /** * Implementation of hook_db_rewrite_sql(). * * This module defines module_grants_node_access() (above) as a replacement for * node_access(), which is used only for single node views. Node access in * listings is processed with node_db_rewrite_sql(), which needs to have the * same treatment. * This function is similar to node_db_rewrite_sql() in node.module but ANDs * rather than ORs grants together on a per module base to create a more * natural behaviour. * Supplied by mcarbone, see [#601064]. */ function module_grants_db_rewrite_sql($query, $primary_table, $primary_field) { if ($primary_field == 'nid' && !variable_get('module_grants_OR_modules', FALSE)) { if (!node_access_view_all_nodes()) { //$return['where'] = _module_grants_node_access_where_sql(); //Rather than the above line [#753586] suggests the following $return['where'] = _module_grants_node_access_where_sql('view', $primary_table); return $return; } } } /** * Similar to user_access() but also takes node info into account. Returns * a node operation, to be checked by module_grants_node_access(). * * @param $revision_op * Revision operation for which associated user permission is checked, e.g. * 'view revisions' * @param $node * @return bool * FALSE if the $revision_op is known to Module Grants but not permitted on * this node, 'view', 'update' or 'delete' otherwise */ function _module_grants_user_node_access($revision_op, $node) { switch ($revision_op) { case 'view revisions': // Suppress Revisions tab when there's only one revision -- consistent with core. if (!user_access('view revisions') || $node->num_revisions == 1) { return FALSE; } break; case 'view revision list': // Suppress Revision summary when there's only one revision. if (!user_access('view revisions') || $node->num_revisions == 1) { return FALSE; } break; case 'revert revisions': return user_access('revert revisions') ? 'update' : FALSE; case 'delete revisions': // Don't need 'delete revisions' permission when deleting node of 1 revision return user_access('delete revisions') || $node->num_revisions == 1 ? 'delete' : FALSE; default: drupal_set_message(t('Unknown Module Grants operation %op', array('%op' => $revision_op)), 'warning', FALSE); } return 'view'; } /** * Delete a node and all its revisions. * Required because node.module's node_delete() has a hard-wired call to * node_access() when we should be using module_grants_node_access(). */ function _module_grants_node_delete($nid) { $node = node_load($nid); db_query('DELETE FROM {node} WHERE nid = %d', $node->nid); db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid); // Call the node-specific callback (if any). node_invoke($node, 'delete'); node_invoke_nodeapi($node, 'delete'); // Clear the page and block caches. cache_clear_all(); // Remove this node from the search index if needed. if (function_exists('search_wipe')) { search_wipe($node->nid, 'node'); } watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title)); drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title))); } /** * Generate an SQL where clause for use in fetching a node listing. * * Similar to _node_access_where_sql() in node.module but ANDs rather than ORs * grants together on a per module base to create a more natural behaviour. * * @param $node_op * The operation that must be allowed to return a node. * @param $node_access_alias * If the node_access table has been given an SQL alias other than the default * 'n', that must be passed here. * @param $account * The user object for the user performing the operation. If omitted, the * current user is used. * @return * An SQL where clause. */ function _module_grants_node_access_where_sql($node_op = 'view', $node_access_alias = 'n', $account = NULL) { global $user; if (user_access('administer nodes')) { return; } if (empty($account)) { $account = $user; } $all_grants = _module_grants_by_module($node_op, $account); $grants = array(); foreach ($all_grants as $module => $module_grants) { $lenient_subquery = ''; if (variable_get('module_grants_lenient', TRUE)) { $module_realms = array_keys(module_invoke($module, 'node_grants', $account, $node_op)); if (!empty($module_realms)) { $lenient_subquery = "(SELECT COUNT(1) FROM {node_access} nasq WHERE $node_access_alias.nid=nasq.nid AND realm IN ('". implode("','", $module_realms) ."')) = 0 OR "; } } $grants[] = '('. $lenient_subquery ."(SELECT COUNT(1) FROM {node_access} nasq WHERE $node_access_alias.nid=nasq.nid AND ($module_grants)) > 0)"; } //return = count($grants) ? implode(' AND ', $grants) : ''; //[#601064], comment #13 $base_sql = "((SELECT COUNT(1) FROM {node_access} nasq WHERE $node_access_alias.nid=nasq.nid AND gid=0 AND realm='all') > 0)"; $sql = $base_sql . (count($grants) ? ' OR '. implode(' AND ', $grants) : ''); return $sql; } /** * Return a map, keyed by module name, of SQL clauses representing the grants * associated with the module, as returned by that module's hook_node_grants(). * * @param $node_op * The operation, i.e 'view', 'update' or 'delete' * @param $account * User account object * @param $nid * Optional. If passed in, only modules with at least one row in the * node_acces table for the supplied nid are included (lenient interpretation * of absence of node grants). If not passed in, then all modules implementing * hook_node_grants() will be included (strict). * @return * An array of module grants SQL, keyed by module name */ function _module_grants_by_module($node_op, $account, $nid = NULL) { $hook = 'node_grants'; $all_grants = array(); foreach (module_implements($hook) as $module) { $module_grants = module_invoke($module, $hook, $account, $node_op); if (!empty($module_grants)) { // If a nid has been passed in, don't collect the grants for this module // unless it has at least one row in the node_access table for this nid. if ($nid) { $count = db_result(db_query("SELECT COUNT(*) FROM {node_access} WHERE nid=%d AND realm IN ('". implode("','", array_keys($module_grants)) ."')", $nid)); if ($count == 0 && $module != 'domain') { // [#564318] // Module doesn't have a node_access row for this node, so continue // to next module. continue; } } $module_gids = array(); foreach ($module_grants as $realm => $gids) { foreach ($gids as $key => $gid) { if (is_numeric($gid)) { // skip $gid=='domain' etc, see [#675596] $module_gids[] = "(gid=$gid AND realm='$realm')"; } } } // [#564318] Domain Access has special case with a global cross-domain grant if ($module == 'domain' && $nid) { $module_gids[] = "(nid=$nid AND gid=0 AND realm='domain_site')"; } // Within a module OR the gid/realm combinations together if (!empty($module_gids)) { $all_grants[$module] = implode(' OR ', $module_gids); } } } return $all_grants; } /** * Implementation of hook_node_access_explain(). * * Adds an 'explained' text to the last column of the table that appears inside * the Devel Node Access block. * See devel_node_access submodule in the Devel project. * * @todo: I18n */ function module_grants_node_access_explain($row) { global $user; $ops_allowed = array(); foreach (array('view', 'update', 'delete') as $node_op) { $node = node_load($row->nid); if (module_grants_node_access($node_op, $node, $user)) { $ops_allowed[] = $node_op; } } $access = empty($ops_allowed) ? 'not view' : implode(', ', $ops_allowed); return theme('username', $user) . " may $access this node."; }