Commit 8a17309e authored by Dries's avatar Dries

Issue #1658846 by penyaskito, Schnitzel, xjm, agentrickard, effulgentsia,...

Issue #1658846 by penyaskito, Schnitzel, xjm, agentrickard, effulgentsia, kalman.hosszu, vijaycs85, Gábor Hojtsy, disasm, YesCT: Add language support to node access grants and records.
parent 575b5049
......@@ -84,7 +84,10 @@ public function setValue($values) {
else {
$this->properties['entity']->setValue(NULL);
}
unset($values['entity'], $values['fid'], $values['display'], $values['description']);
unset($values['entity'], $values['fid']);
// @todo These properties are sometimes set due to being present in form
// values. Needs to be cleaned up somewhere.
unset($values['display'], $values['description'], $values['upload']);
if ($values) {
throw new \InvalidArgumentException('Property ' . key($values) . ' is unknown.');
}
......
......@@ -109,11 +109,19 @@ protected function accessGrants(EntityInterface $node, $operation, $langcode = L
// Check the database for potential access grants.
$query = db_select('node_access');
$query->addExpression('1');
// Only interested for granting in the current operation.
$query->condition('grant_' . $operation, 1, '>=');
$nids = db_or()->condition('nid', $node->id());
// Check for grants for this node and the correct langcode.
$nids = db_and()
->condition('nid', $node->nid)
->condition('langcode', $langcode);
// If the node is published, also take the default grant into account. The
// default is saved with a node ID of 0.
$status = $node instanceof EntityNG ? $node->status : $node->get('status', $langcode)->value;
if ($status) {
$nids->condition('nid', 0);
$nids = db_or()
->condition($nids)
->condition('nid', 0);
}
$query->condition($nids);
$query->range(0, 1);
......
......@@ -23,16 +23,6 @@ public static function getInfo() {
);
}
/**
* Asserts node_access() correctly grants or denies access.
*/
function assertNodeAccess($ops, $node, $account) {
foreach ($ops as $op => $result) {
$msg = t("node_access returns @result with operation '@op'.", array('@result' => $result ? 'true' : 'false', '@op' => $op));
$this->assertEqual($result, node_access($op, $node, $account), $msg);
}
}
function setUp() {
parent::setUp();
// Clear permissions for authenticated users.
......@@ -76,4 +66,5 @@ function testNodeAccess() {
$node5 = $this->drupalCreateNode();
$this->assertNodeAccess(array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE), $node5, $web_user3);
}
}
......@@ -30,4 +30,35 @@ function setUp() {
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
}
}
/**
* Asserts that node_access() correctly grants or denies access.
*
* @param array $ops
* An associative array of the expected node access grants for the node
* and account, with each key as the name of an operation (e.g. 'view',
* 'delete') and each value a Boolean indicating whether access to that
* operation should be granted.
* @param \Drupal\node\Plugin\Core\Entity\Node $node
* The node object to check.
* @param \Drupal\user\Plugin\Core\Entity\User $account
* The user account for which to check access.
* @param string|null $langcode
* (optional) The language code indicating which translation of the node
* to check. If NULL, the untranslated (fallback) access is checked.
*/
function assertNodeAccess(array $ops, $node, $account, $langcode = NULL) {
foreach ($ops as $op => $result) {
$msg = format_string(
'node_access() returns @result with operation %op, language code %langcode.',
array(
'@result' => $result ? 'true' : 'false',
'%op' => $op,
'%langcode' => !empty($langcode) ? $langcode : 'empty'
)
);
$this->assertEqual($result, node_access($op, $node, $account, $langcode), $msg);
}
}
}
......@@ -234,11 +234,17 @@ function hook_node_grants($account, $op) {
* of this gid within this realm can edit this node.
* - 'grant_delete': If set to 1 a user that has been identified as a member
* of this gid within this realm can delete this node.
*
*
* When an implementation is interested in a node but want to deny access to
* everyone, it may return a "deny all" grant:
*
* - langcode: (optional) The language code of a specific translation of the
* node, if any. Modules may add this key to grant different access to
* different translations of a node, such that (e.g.) a particular group is
* granted access to edit the Catalan version of the node, but not the
* Hungarian version. If no value is provided, the langcode is set
* automatically from the $node parameter and the node's original language (if
* specified) is used as a fallback. Only specify multiple grant records with
* different languages for a node if the site has those languages configured.
*
* A "deny all" grant may be used to deny all access to a particular node or
* node translation:
* @code
* $grants[] = array(
* 'realm' => 'all',
......@@ -246,15 +252,14 @@ function hook_node_grants($account, $op) {
* 'grant_view' => 0,
* 'grant_update' => 0,
* 'grant_delete' => 0,
* 'priority' => 1,
* 'langcode' => 'ca',
* );
* @endcode
*
* Setting the priority should cancel out other grants. In the case of a
* conflict between modules, it is safer to use hook_node_access_records_alter()
* to return only the deny grant.
*
* Note: a deny all grant is not written to the database; denies are implicit.
* Note that another module node access module could override this by granting
* access to one or more nodes, since grants are additive. To enforce that
* access is denied in a particular case, use hook_node_access_records_alter().
* Also note that a deny all is not written to the database; denies are
* implicit.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node that has just been saved.
......@@ -271,8 +276,9 @@ function hook_node_access_records(\Drupal\Core\Entity\EntityInterface $node) {
// treated just like any other node and we completely ignore it.
if ($node->private) {
$grants = array();
// Only published nodes should be viewable to all users. If we allow access
// blindly here, then all users could view an unpublished node.
// Only published Catalan translations of private nodes should be viewable
// to all users. If we fail to check $node->status, all users would be able
// to view an unpublished node.
if ($node->status) {
$grants[] = array(
'realm' => 'example',
......@@ -280,6 +286,7 @@ function hook_node_access_records(\Drupal\Core\Entity\EntityInterface $node) {
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'langcode' => 'ca'
);
}
// For the example_author array, the GID is equivalent to a UID, which
......@@ -292,6 +299,7 @@ function hook_node_access_records(\Drupal\Core\Entity\EntityInterface $node) {
'grant_view' => 1,
'grant_update' => 1,
'grant_delete' => 1,
'langcode' => 'ca'
);
return $grants;
......
......@@ -149,6 +149,20 @@ function node_schema() {
'not null' => TRUE,
'default' => 0,
),
'langcode' => array(
'description' => 'The {language}.langcode of this node.',
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
),
'fallback' => array(
'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
),
'gid' => array(
'description' => "The grant ID a user must possess in the specified realm to gain this row's privileges on the node.",
'type' => 'int',
......@@ -188,7 +202,7 @@ function node_schema() {
'size' => 'tiny',
),
),
'primary key' => array('nid', 'gid', 'realm'),
'primary key' => array('nid', 'gid', 'realm', 'langcode'),
'foreign keys' => array(
'affected_node' => array(
'table' => 'node',
......@@ -709,6 +723,34 @@ function node_update_8014() {
update_module_enable(array('datetime'));
}
/**
* Add language support to the {node_access} table.
*/
function node_update_8015() {
// Add the langcode field.
$langcode_field = array(
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
'description' => 'The {language}.langcode of this node.',
);
db_add_field('node_access', 'langcode', $langcode_field);
// Add the fallback field.
$fallback_field = array(
'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
);
db_add_field('node_access', 'fallback', $fallback_field);
db_drop_primary_key('node_access');
db_add_primary_key('node_access', array('nid', 'gid', 'realm', 'langcode'));
}
/**
* @} End of "addtogroup updates-7.x-to-8.x"
* The next series of updates should start at 9000.
......
......@@ -2509,10 +2509,6 @@ function node_form_system_themes_admin_form_submit($form, &$form_state) {
* TRUE if the operation may be performed, FALSE otherwise.
*
* @see node_menu()
*
* @todo
* Add langcode support to node_access schema / queries.
* http://drupal.org/node/1658846
*/
function node_access($op, $node, $account = NULL, $langcode = NULL) {
if (!$node instanceof EntityInterface) {
......@@ -2523,6 +2519,18 @@ function node_access($op, $node, $account = NULL, $langcode = NULL) {
// If no language code was provided, default to the node's langcode.
if (empty($langcode)) {
$langcode = $node->langcode;
// If the Language module is enabled, try to use the language from content
// negotiation.
if (module_exists('language')) {
// Load languages the node exists in.
$node_translations = $node->getTranslationLanguages();
// Load the language from content negotiation.
$content_negotiation_langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
// If there is a translation available, use it.
if (isset($node_translations[$content_negotiation_langcode])) {
$langcode = $content_negotiation_langcode;
}
}
}
// Make sure that if an account is passed, that it is a fully loaded user
......@@ -2756,6 +2764,9 @@ function node_query_node_access_alter(AlterableInterface $query) {
if (!$op = $query->getMetaData('op')) {
$op = 'view';
}
if (!$langcode = $query->getMetaData('langcode')) {
$langcode = FALSE;
}
// If $account can bypass node access, or there are no node access modules,
// or the operation is 'view' and the $account has a global view grant
......@@ -2793,7 +2804,6 @@ function node_query_node_access_alter(AlterableInterface $query) {
// Find all instances of the base table being joined -- could appear
// more than once in the query, and could be aliased. Join each one to
// the node_access table.
$grants = node_access_grants($op, $account);
foreach ($tables as $nalias => $tableinfo) {
$table = $tableinfo['table'];
......@@ -2803,8 +2813,8 @@ function node_query_node_access_alter(AlterableInterface $query) {
->fields('na', array('nid'));
$grant_conditions = db_or();
// If any grant exists for the specified user, then user has access
// to the node for the specified operation.
// If any grant exists for the specified user, then user has access to the
// node for the specified operation.
foreach ($grants as $realm => $gids) {
foreach ($gids as $gid) {
$grant_conditions->condition(db_and()
......@@ -2819,6 +2829,20 @@ function node_query_node_access_alter(AlterableInterface $query) {
$subquery->condition($grant_conditions);
}
$subquery->condition('na.grant_' . $op, 1, '>=');
// Add langcode-based filtering if this is a multilingual site.
if (language_multilingual()) {
// If no specific langcode to check for is given, use the grant entry
// which is set as a fallback.
// If a specific langcode is given, use the grant entry for it.
if ($langcode === FALSE) {
$subquery->condition('na.fallback', 1, '=');
}
else {
$subquery->condition('na.langcode', $langcode, '=');
}
}
$field = 'nid';
// Now handle entities.
$subquery->where("$nalias.$field = na.nid");
......@@ -2869,11 +2893,8 @@ function node_access_acquire_grants(EntityInterface $node, $delete = TRUE) {
* @param \Drupal\Core\Entity\EntityInterface $node
* The node whose grants are being written.
* @param $grants
* A list of grants to write. Each grant is an array that must contain the
* following keys: realm, gid, grant_view, grant_update, grant_delete.
* The realm is specified by a particular module; the gid is as well, and
* is a module-defined id to define grant privileges. each grant_* field
* is a boolean value.
* A list of grants to write. See hook_node_access_records() for the
* expected structure of the grants array.
* @param $realm
* (optional) If provided, read/write grants for that realm only. Defaults to
* NULL.
......@@ -2892,18 +2913,35 @@ function _node_access_write_grants(EntityInterface $node, $grants, $realm = NULL
}
$query->execute();
}
// Only perform work when node_access modules are active.
if (!empty($grants) && count(module_implements('node_grants'))) {
$query = db_insert('node_access')->fields(array('nid', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete'));
$query = db_insert('node_access')->fields(array('nid', 'langcode', 'fallback', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete'));
// If we have defined a granted langcode, use it. But if not, add a grant
// for every language this node is translated to.
foreach ($grants as $grant) {
if ($realm && $realm != $grant['realm']) {
continue;
}
// Only write grants; denies are implicit.
if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) {
$grant['nid'] = $node->nid;
$query->values($grant);
if (isset($grant['langcode'])) {
$grant_languages = array($grant['langcode'] => language_load($grant['langcode']));
}
else {
$grant_languages = $node->getTranslationLanguages(TRUE);
}
foreach ($grant_languages as $grant_langcode => $grant_language) {
// Only write grants; denies are implicit.
if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) {
$grant['nid'] = $node->nid;
$grant['langcode'] = $grant_langcode;
// The record with the original langcode is used as the fallback.
if ($grant['langcode'] == $node->langcode) {
$grant['fallback'] = 1;
}
else {
$grant['fallback'] = 0;
}
$query->values($grant);
}
}
}
$query->execute();
......@@ -3485,7 +3523,9 @@ function node_modules_disabled($modules) {
// At this point, the module is already disabled, but its code is still
// loaded in memory. Module functions must no longer be called. We only
// check whether a hook implementation function exists and do not invoke it.
if (!node_access_needs_rebuild() && module_hook($module, 'node_grants')) {
// Node access also needs to be rebuilt if language module is disabled to
// remove any language-specific grants.
if (!node_access_needs_rebuild() && (module_hook($module, 'node_grants') || $module == 'language')) {
node_access_needs_rebuild(TRUE);
}
}
......
name: 'Node module access tests language'
description: 'Support module for language-aware node access testing.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- options
hidden: true
<?php
/**
* @file
* Test module with a language-aware node access implementation.
*
* The module adds a 'private' field to page nodes that allows each translation
* of the node to be marked as private (viewable only by administrators).
*/
use Drupal\Core\Entity\EntityInterface;
/**
* Implements hook_node_grants().
*
* This module defines a single grant realm. All users belong to this group.
*/
function node_access_test_language_node_grants($account, $op) {
$grants['node_access_language_test'] = array(7888);
return $grants;
}
/**
* Implements hook_node_access_records().
*/
function node_access_test_language_node_access_records(EntityInterface $node) {
$grants = array();
// Create grants for each translation of the node.
foreach ($node->getTranslationLanguages() as $langcode => $language) {
// If the translation is not marked as private, grant access.
$grants[] = array(
'realm' => 'node_access_language_test',
'gid' => 7888,
'grant_view' => empty($node->field_private[$langcode][0]['value']) ? 1 : 0,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
'langcode' => $langcode,
);
}
return $grants;
}
/**
* Implements hook_enable().
*
* Creates the 'private' field, which allows the node to be marked as private
* (restricted access) in a given translation.
*/
function node_access_test_language_enable() {
$field_private = array(
'field_name' => 'field_private',
'type' => 'list_boolean',
'cardinality' => 1,
'translatable' => TRUE,
'settings' => array(
'allowed_values' => array(0 => 'Not private', 1 => 'Private'),
),
);
$field_private = field_create_field($field_private);
$instance = array(
'field_name' => $field_private['field_name'],
'entity_type' => 'node',
'bundle' => 'page',
'widget' => array(
'type' => 'options_buttons',
),
);
$instance = field_create_instance($instance);
}
/**
* Implements hook_disable().
*/
function node_access_test_language_disable() {
field_delete_instance(field_read_instance('node', 'field_private', 'page'));
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment