summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDries2013-03-27 18:23:49 (GMT)
committerDries2013-03-27 18:23:49 (GMT)
commit8a17309e5f0dec51d10229d6bb41c4782652d868 (patch)
tree4710a3e71cc77a0e78d088529c350e5280c7e1ba
parent575b5049a83784eccabed34d92e83557fd750668 (diff)
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.
-rw-r--r--core/modules/file/lib/Drupal/file/Type/FileItem.php5
-rw-r--r--core/modules/node/lib/Drupal/node/NodeAccessController.php12
-rw-r--r--core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php324
-rw-r--r--core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php271
-rw-r--r--core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php216
-rw-r--r--core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php11
-rw-r--r--core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php31
-rw-r--r--core/modules/node/node.api.php36
-rw-r--r--core/modules/node/node.install44
-rw-r--r--core/modules/node/node.module78
-rw-r--r--core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info.yml8
-rw-r--r--core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module79
12 files changed, 1037 insertions, 78 deletions
diff --git a/core/modules/file/lib/Drupal/file/Type/FileItem.php b/core/modules/file/lib/Drupal/file/Type/FileItem.php
index e1c7d24..cecce05 100644
--- a/core/modules/file/lib/Drupal/file/Type/FileItem.php
+++ b/core/modules/file/lib/Drupal/file/Type/FileItem.php
@@ -84,7 +84,10 @@ class FileItem extends FieldItemBase {
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.');
}
diff --git a/core/modules/node/lib/Drupal/node/NodeAccessController.php b/core/modules/node/lib/Drupal/node/NodeAccessController.php
index 815c2ab..ec68b7e 100644
--- a/core/modules/node/lib/Drupal/node/NodeAccessController.php
+++ b/core/modules/node/lib/Drupal/node/NodeAccessController.php
@@ -109,11 +109,19 @@ class NodeAccessController extends EntityAccessController {
// 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);
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php
new file mode 100644
index 0000000..6b60907
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php
@@ -0,0 +1,324 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\NodeAccessLanguageAwareCombinationTest.
+ */
+
+namespace Drupal\node\Tests;
+
+use Drupal\Core\Language\Language;
+
+/**
+ * Tests node access with multiple languages and access control modules.
+ */
+class NodeAccessLanguageAwareCombinationTest extends NodeTestBase {
+
+ /**
+ * Enable language and two node access modules.
+ *
+ * @var array
+ */
+ public static $modules = array('language', 'node_access_test_language', 'node_access_test');
+
+ /**
+ * A set of nodes to use in testing.
+ *
+ * @var array
+ */
+ protected $nodes = array();
+
+ /**
+ * A normal authenticated user.
+ *
+ * @var \Drupal\user\Plugin\Core\Entity\User.
+ */
+ protected $web_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Node access language-aware combination',
+ 'description' => 'Tests node access functionality with multiple languages and two node access modules.',
+ 'group' => 'Node',
+ );
+ }
+
+ public function setUp() {
+ parent::setUp();
+
+ // After enabling a node access module, the access table has to be rebuild.
+ node_access_rebuild();
+
+ // Add Hungarian and Catalan.
+ $language = new Language(array(
+ 'langcode' => 'hu',
+ ));
+ language_save($language);
+ $language = new Language(array(
+ 'langcode' => 'ca',
+ ));
+ language_save($language);
+
+ // Create a normal authenticated user.
+ $this->web_user = $this->drupalCreateUser(array('access content'));
+
+ // Load the user 1 user for later use as an admin user with permission to
+ // see everything.
+ $this->admin_user = user_load(1);
+
+ // The node_access_test_language module allows individual translations of a
+ // node to be marked private (not viewable by normal users), and the
+ // node_access_test module allows whole nodes to be marked private. (In a
+ // real-world implementation, hook_node_access_records_alter() might be
+ // implemented by one or both modules to enforce that private nodes or
+ // translations are always private, but we want to test the default,
+ // additive behavior of node access).
+
+ // Create six Hungarian nodes with Catalan translations:
+ // 1. One public with neither language marked as private.
+ // 2. One private with neither language marked as private.
+ // 3. One public with only the Hungarian translation private.
+ // 4. One public with only the Catalan translation private.
+ // 5. One public with both the Hungarian and Catalan translations private.
+ // 6. One private with both the Hungarian and Catalan translations private.
+ $this->nodes['public_both_public'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 0)),
+ 'private' => FALSE,
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 0;
+ $node->save();
+
+ $this->nodes['private_both_public'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 0)),
+ 'private' => TRUE,
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 0;
+ $node->save();
+
+ $this->nodes['public_hu_private'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 1)),
+ 'private' => FALSE,
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 0;
+ $node->save();
+
+ $this->nodes['public_ca_private'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 0)),
+ 'private' => FALSE,
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 1;
+ $node->save();
+
+ $this->nodes['public_both_private'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 1)),
+ 'private' => FALSE,
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 1;
+ $node->save();
+
+ $this->nodes['private_both_private'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 1)),
+ 'private' => TRUE,
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 1;
+ $node->save();
+
+ $this->nodes['public_no_language_private'] = $this->drupalCreateNode(array(
+ 'field_private' => array(array('value' => 1)),
+ 'private' => FALSE,
+ ));
+ $this->nodes['public_no_language_public'] = $this->drupalCreateNode(array(
+ 'field_private' => array(array('value' => 0)),
+ 'private' => FALSE,
+ ));
+ $this->nodes['private_no_language_private'] = $this->drupalCreateNode(array(
+ 'field_private' => array(array('value' => 1)),
+ 'private' => TRUE,
+ ));
+ $this->nodes['private_no_language_public'] = $this->drupalCreateNode(array(
+ 'field_private' => array(array('value' => 1)),
+ 'private' => TRUE,
+ ));
+ }
+
+ /**
+ * Tests node_access() and node access queries with multiple node languages.
+ */
+ function testNodeAccessLanguageAwareCombination() {
+
+ $expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
+ $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+ // When the node and both translations are public, access should only be
+ // denied when a translation that does not exist is requested.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['public_both_public'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access, $this->nodes['public_both_public'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access, $this->nodes['public_both_public'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_public'], $this->web_user, 'en');
+
+ // If the node is marked private but both existing translations are not,
+ // access should still be granted, because the grants are additive.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['private_both_public'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access, $this->nodes['private_both_public'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access, $this->nodes['private_both_public'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_public'], $this->web_user, 'en');
+
+ // If the node is marked private, but a existing translation is public,
+ // access should only be granted for the public translation. For a
+ // translation that does not exist yet (English translation), the access is
+ // denied. With the Hungarian translation marked as private, but the Catalan
+ // translation public, the access is granted.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_hu_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_hu_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access, $this->nodes['public_hu_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_hu_private'], $this->web_user, 'en');
+
+ // With the Catalan translation marked as private, but the node public,
+ // access is granted for the existing Hungarian translation, but not for the
+ // Catalan nor the English ones.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['public_ca_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access, $this->nodes['public_ca_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_ca_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_ca_private'], $this->web_user, 'en');
+
+ // With both translations marked as private, but the node public, access
+ // should be denied in all cases.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user, 'en');
+
+ // If the node and both its existing translations are private, access should
+ // be denied in all cases.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user, 'en');
+
+ // No access for all languages as the language aware node access module
+ // denies access.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user, 'en');
+
+ // Access only for request with no language defined.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['public_no_language_public'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_public'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_public'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_public'], $this->web_user, 'en');
+
+ // No access for all languages as both node access modules deny access.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user, 'en');
+
+ // No access for all languages as the non language aware node access module
+ // denies access.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user, 'en');
+
+
+ // Query the node table with the node access tag in several languages.
+
+ // Query with no language specified. The fallback (hu or und) will be used.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Four nodes should be returned with public Hungarian translations or the
+ // no language public node.
+ $this->assertEqual(count($nids), 4, 'db_select() returns 4 nodes when no langcode is specified.');
+ $this->assertTrue(array_key_exists($this->nodes['public_both_public']->nid, $nids), 'Returned node ID is full public node.');
+ $this->assertTrue(array_key_exists($this->nodes['public_ca_private']->nid, $nids), 'Returned node ID is Hungarian public only node.');
+ $this->assertTrue(array_key_exists($this->nodes['private_both_public']->nid, $nids), 'Returned node ID is both public non-language-aware private only node.');
+ $this->assertTrue(array_key_exists($this->nodes['public_no_language_public']->nid, $nids), 'Returned node ID is no language public node.');
+
+ // Query with Hungarian (hu) specified.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addMetaData('langcode', 'hu')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Three nodes should be returned (with public Hungarian translations).
+ $this->assertEqual(count($nids), 3, 'db_select() returns 3 nodes.');
+ $this->assertTrue(array_key_exists($this->nodes['public_both_public']->nid, $nids), 'Returned node ID is both public node.');
+ $this->assertTrue(array_key_exists($this->nodes['public_ca_private']->nid, $nids), 'Returned node ID is Hungarian public only node.');
+ $this->assertTrue(array_key_exists($this->nodes['private_both_public']->nid, $nids), 'Returned node ID is both public non-language-aware private only node.');
+
+ // Query with Catalan (ca) specified.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addMetaData('langcode', 'ca')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Three nodes should be returned (with public Catalan translations).
+ $this->assertEqual(count($nids), 3, 'db_select() returns 3 nodes.');
+ $this->assertTrue(array_key_exists($this->nodes['public_both_public']->nid, $nids), 'Returned node ID is both public node.');
+ $this->assertTrue(array_key_exists($this->nodes['public_hu_private']->nid, $nids), 'Returned node ID is Catalan public only node.');
+ $this->assertTrue(array_key_exists($this->nodes['private_both_public']->nid, $nids), 'Returned node ID is both public non-language-aware private only node.');
+
+ // Query with German (de) specified.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addMetaData('langcode', 'de')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // There are no nodes with German translations, so no results are returned.
+ $this->assertTrue(empty($nids), 'db_select() returns an empty result.');
+
+ // Query the nodes table as admin user (full access) with the node access
+ // tag and no specific langcode.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->admin_user)
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // All nodes are returned.
+ $this->assertEqual(count($nids), 10, 'db_select() returns all nodes.');
+
+ // Query the nodes table as admin user (full access) with the node access
+ // tag and langcode de.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->admin_user)
+ ->addMetaData('langcode', 'de')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Even though there is no German translation, all nodes are returned
+ // because node access filtering does not occur when the user is user 1.
+ $this->assertEqual(count($nids), 10, 'db_select() returns all nodes.');
+ }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php
new file mode 100644
index 0000000..8e5c454
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php
@@ -0,0 +1,271 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\NodeAccessLanguageAwareTest.
+ */
+
+namespace Drupal\node\Tests;
+
+use Drupal\Core\Language\Language;
+
+/**
+ * Tests node access functionality for multiple languages.
+ */
+class NodeAccessLanguageAwareTest extends NodeTestBase {
+
+ /**
+ * Enable language and a language-aware node access module.
+ *
+ * @var array
+ */
+ public static $modules = array('language', 'node_access_test_language');
+
+ /**
+ * A set of nodes to use in testing.
+ *
+ * @var array
+ */
+ protected $nodes = array();
+
+ /**
+ * A normal authenticated user.
+ *
+ * @var \Drupal\user\Plugin\Core\Entity\User.
+ */
+ protected $web_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Node access language-aware',
+ 'description' => 'Test node_access and db_select() with node_access tag functionality with multiple languages with node_access_test_language which is language-aware.',
+ 'group' => 'Node',
+ );
+ }
+
+ public function setUp() {
+ parent::setUp();
+
+ // After enabling a node access module, the access table has to be rebuild.
+ node_access_rebuild();
+
+ // Create a normal authenticated user.
+ $this->web_user = $this->drupalCreateUser(array('access content'));
+
+ // Load the user 1 user for later use as an admin user with permission to
+ // see everything.
+ $this->admin_user = user_load(1);
+
+ // Add Hungarian and Catalan.
+ $language = new Language(array(
+ 'langcode' => 'hu',
+ ));
+ language_save($language);
+ $language = new Language(array(
+ 'langcode' => 'ca',
+ ));
+ language_save($language);
+
+ // The node_access_test_language module allows individual translations of a
+ // node to be marked private (not viewable by normal users).
+
+ // Create six nodes:
+ // 1. Four Hungarian nodes with Catalan translations
+ // - One with neither language marked as private.
+ // - One with only the Hungarian translation private.
+ // - One with only the Catalan translation private.
+ // - One with both the Hungarian and Catalan translations private.
+ // 2. Two nodes with no language specified.
+ // - One public.
+ // - One private.
+ $this->nodes['both_public'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 0)),
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 0;
+ $node->save();
+
+ $this->nodes['ca_private'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 0)),
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 1;
+ $node->save();
+
+ $this->nodes['hu_private'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 1)),
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 0;
+ $node->save();
+
+ $this->nodes['both_private'] = $node = $this->drupalCreateNode(array(
+ 'body' => array(array()),
+ 'langcode' => 'hu',
+ 'field_private' => array(array('value' => 1)),
+ ));
+ $translation = $node->getTranslation('ca');
+ $translation->field_private[0]->value = 1;
+ $node->save();
+
+ $this->nodes['no_language_public'] = $this->drupalCreateNode(array(
+ 'field_private' => array(array('value' => 0)),
+ ));
+ $this->nodes['no_language_private'] = $this->drupalCreateNode(array(
+ 'field_private' => array(array('value' => 1)),
+ ));
+ }
+
+ /**
+ * Tests node_access() and node access queries with multiple node languages.
+ */
+ function testNodeAccessLanguageAware() {
+ // The node_access_test_language module only grants view access.
+ $expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
+ $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+ // When both Hungarian and Catalan are marked as public, access to the
+ // Hungarian translation should be granted when no language is specified or
+ // when the Hungarian translation is specified explicitly.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['both_public'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access, $this->nodes['both_public'], $this->web_user, 'hu');
+ // Access to the Catalan translation should also be granted.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['both_public'], $this->web_user, 'ca');
+ // There is no English translation, so a request to access the English
+ // translation is denied.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_public'], $this->web_user, 'en');
+
+ // When Hungarian is marked as private, access to the Hungarian translation
+ // should be denied when no language is specified or when the Hungarian
+ // translation is specified explicitly.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['hu_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['hu_private'], $this->web_user, 'hu');
+ // Access to the Catalan translation should be granted.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['hu_private'], $this->web_user, 'ca');
+ // There is no English translation, so a request to access the English
+ // translation is denied.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['hu_private'], $this->web_user, 'en');
+
+ // When Catalan is marked as private, access to the Hungarian translation
+ // should be granted when no language is specified or when the Hungarian
+ // translation is specified explicitly.
+ $this->assertNodeAccess($expected_node_access, $this->nodes['ca_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access, $this->nodes['ca_private'], $this->web_user, 'hu');
+ // Access to the Catalan translation should be granted.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['ca_private'], $this->web_user, 'ca');
+ // There is no English translation, so a request to access the English
+ // translation is denied.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['ca_private'], $this->web_user, 'en');
+
+ // When both translations are marked as private, access should be denied
+ // regardless of the language specified.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user, 'en');
+
+ // When no language is specified for a private node, access to every
+ // language is denied.
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user, 'en');
+
+ // When no language is specified for a public node, access should be granted
+ // only for the existing language (not specified), so only the request with
+ // no language will give access, as this request will be made with the
+ // langcode of the node, which is "not specified".
+ $this->assertNodeAccess($expected_node_access, $this->nodes['no_language_public'], $this->web_user);
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_public'], $this->web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_public'], $this->web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_public'], $this->web_user, 'en');
+
+ // Query the node table with the node access tag in several languages.
+
+ // Query with no language specified. The fallback (hu) will be used.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Three nodes should be returned:
+ // - Node with both translations public.
+ // - Node with only the Catalan translation marked as private.
+ // - No language node marked as public.
+ $this->assertEqual(count($nids), 3, 'db_select() returns 3 nodes when no langcode is specified.');
+ $this->assertTrue(array_key_exists($this->nodes['both_public']->nid, $nids), 'The node with both translations public is returned.');
+ $this->assertTrue(array_key_exists($this->nodes['ca_private']->nid, $nids), 'The node with only the Catalan translation private is returned.');
+ $this->assertTrue(array_key_exists($this->nodes['no_language_public']->nid, $nids), 'The node with no language is returned.');
+
+ // Query with Hungarian (hu) specified.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addMetaData('langcode', 'hu')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Two nodes should be returned: the node with both translations public, and
+ // the node with only the Catalan translation marked as private.
+ $this->assertEqual(count($nids), 2, 'db_select() returns 2 nodes when the hu langcode is specified.');
+ $this->assertTrue(array_key_exists($this->nodes['both_public']->nid, $nids), 'The node with both translations public is returned.');
+ $this->assertTrue(array_key_exists($this->nodes['ca_private']->nid, $nids), 'The node with only the Catalan translation private is returned.');
+
+ // Query with Catalan (ca) specified.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addMetaData('langcode', 'ca')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Two nodes should be returned: the node with both translations public, and
+ // the node with only the Hungarian translation marked as private.
+ $this->assertEqual(count($nids), 2, 'db_select() returns 2 nodes when the hu langcode is specified.');
+ $this->assertTrue(array_key_exists($this->nodes['both_public']->nid, $nids), 'The node with both translations public is returned.');
+ $this->assertTrue(array_key_exists($this->nodes['hu_private']->nid, $nids), 'The node with only the Hungarian translation private is returned.');
+
+ // Query with German (de) specified.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->web_user)
+ ->addMetaData('langcode', 'de')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // There are no nodes with German translations, so no results are returned.
+ $this->assertTrue(empty($nids), 'db_select() returns an empty result when the de langcode is specified.');
+
+ // Query the nodes table as admin user (full access) with the node access
+ // tag and no specific langcode.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->admin_user)
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // All nodes are returned.
+ $this->assertEqual(count($nids), 6, 'db_select() returns all nodes.');
+
+ // Query the nodes table as admin user (full access) with the node access
+ // tag and langcode de.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $this->admin_user)
+ ->addMetaData('langcode', 'de')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Even though there is no German translation, all nodes are returned
+ // because node access filtering does not occur when the user is user 1.
+ $this->assertEqual(count($nids), 6, 'db_select() returns all nodes.');
+ }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
index b418ef4..fceb154 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
@@ -24,34 +24,20 @@ class NodeAccessLanguageTest extends NodeTestBase {
public static function getInfo() {
return array(
'name' => 'Node access language',
- 'description' => 'Test node_access functionality with multiple languages.',
+ 'description' => 'Test node_access and db_select() with node_access tag functionality with multiple languages with a test node access module that is not language-aware.',
'group' => 'Node',
);
}
- /**
- * Asserts node_access correctly grants or denies access.
- */
- function assertNodeAccess($ops, $node, $account, $langcode = NULL) {
- foreach ($ops as $op => $result) {
- $msg = t("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);
- }
- }
-
function setUp() {
parent::setUp();
- // Clear permissions for authenticated users.
- db_delete('role_permission')
- ->condition('rid', DRUPAL_AUTHENTICATED_RID)
- ->execute();
- }
+ // After enabling a node access module, the access table has to be rebuild.
+ node_access_rebuild();
+
+ // Enable the private node feature of the node_access_test module.
+ state()->set('node_access_test.private', TRUE);
- /**
- * Runs tests for node_access function with multiple languages.
- */
- function testNodeAccess() {
// Add Hungarian and Catalan.
$language = new Language(array(
'langcode' => 'hu',
@@ -61,31 +47,199 @@ class NodeAccessLanguageTest extends NodeTestBase {
'langcode' => 'ca',
));
language_save($language);
+ }
+
+ /**
+ * Tests node_access() with multiple node languages and no private nodes.
+ */
+ function testNodeAccess() {
+ $web_user = $this->drupalCreateUser(array('access content'));
+
+ $expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
+ $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+ // Creating a public node with langcode Hungarian, will be saved as the
+ // fallback in node access table.
+ $node_public = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => FALSE));
+ $this->assertTrue($node_public->langcode == 'hu', 'Node created as Hungarian.');
+
+ // Tests the default access is provided for the public Hungarian node.
+ $this->assertNodeAccess($expected_node_access, $node_public, $web_user);
+
+ // Tests that Hungarian provided specifically results in the same.
+ $this->assertNodeAccess($expected_node_access, $node_public, $web_user, 'hu');
+
+ // There is no specific Catalan version of this node and Croatian is not
+ // even set up on the system in this scenario, so the user will not get
+ // access to these nodes.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'hr');
+
+ // Creating a public node with no special langcode, like when no language
+ // module enabled.
+ $node_public_no_language = $this->drupalCreateNode(array('private' => FALSE));
+ $this->assertTrue($node_public_no_language->langcode == LANGUAGE_NOT_SPECIFIED, 'Node created with not specified language.');
+
+ // Tests that access is granted if requested with no language.
+ $this->assertNodeAccess($expected_node_access, $node_public_no_language, $web_user);
+
+ // Tests that access is not granted if requested with Hungarian language.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'hu');
- // Tests the default access provided for a published Hungarian node.
+ // There is no specific Catalan version of this node and Croatian is not
+ // even set up on the system in this scenario, so the user will not get
+ // access to these nodes.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'hr');
+
+ // Reset the node access cache and turn on our test node_access() code.
+ drupal_static_reset('node_access');
+ variable_set('node_access_test_secret_catalan', 1);
+
+ // Tests that access is granted if requested with no language.
+ $this->assertNodeAccess($expected_node_access, $node_public_no_language, $web_user);
+
+ // Tests that Hungarian is still not accessible.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'hu');
+
+ // Tests that Catalan is still not accessible.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'ca');
+ }
+
+ /**
+ * Tests node_access() with multiple node languages and private nodes.
+ */
+ function testNodeAccessPrivate() {
$web_user = $this->drupalCreateUser(array('access content'));
+
$node = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu'));
$this->assertTrue($node->langcode == 'hu', 'Node created as Hungarian.');
$expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
- $this->assertNodeAccess($expected_node_access, $node, $web_user);
+ $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+ // Creating a private node with langcode Hungarian, will be saved as the
+ // fallback in node access table.
+ $node_public = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => TRUE));
+ $this->assertTrue($node_public->langcode == 'hu', 'Node created as Hungarian.');
+
+ // Tests the default access is not provided for the private Hungarian node.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user);
// Tests that Hungarian provided specifically results in the same.
- $this->assertNodeAccess($expected_node_access, $node, $web_user, 'hu');
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'hu');
// There is no specific Catalan version of this node and Croatian is not
- // even set up on the system in this scenario, so these languages will not
- // play a role in the node's permissions.
- $this->assertNodeAccess($expected_node_access, $node, $web_user, 'ca');
- $this->assertNodeAccess($expected_node_access, $node, $web_user, 'hr');
+ // even set up on the system in this scenario, so the user will not get
+ // access to these nodes.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'hr');
+
+ // Creating a private node with no special langcode, like when no language
+ // module enabled.
+ $node_private_no_language = $this->drupalCreateNode(array('private' => TRUE));
+ $this->assertTrue($node_private_no_language->langcode == LANGUAGE_NOT_SPECIFIED, 'Node created with not specified language.');
+
+ // Tests that access is not granted if requested with no language.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user);
+
+ // Tests that access is not granted if requested with Hungarian language.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'hu');
+
+ // There is no specific Catalan version of this node and Croatian is not
+ // even set up on the system in this scenario, so the user will not get
+ // access to these nodes.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'ca');
+ $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'hr');
// Reset the node access cache and turn on our test node_access() code.
entity_access_controller('node')->resetCache();
state()->set('node_access_test_secret_catalan', 1);
- // Tests that Hungarian is still accessible.
- $this->assertNodeAccess($expected_node_access, $node, $web_user, 'hu');
+ // Tests that access is not granted if requested with no language.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user);
+
+ // Tests that Hungarian is still not accessible.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'hu');
- // Tests that Catalan is not accessible anymore.
- $this->assertNodeAccess(array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE), $node, $web_user, 'ca');
+ // Tests that Catalan is still not accessible.
+ $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'ca');
}
+
+ /**
+ * Tests db_select() with a 'node_access' tag and langcode metadata.
+ */
+ function testNodeAccessQueryTag() {
+ // Create a normal authenticated user.
+ $web_user = $this->drupalCreateUser(array('access content'));
+
+ // Load the user 1 user for later use as an admin user with permission to
+ // see everything.
+ $admin_user = user_load(1);
+
+ // Creating a private node with langcode Hungarian, will be saved as
+ // the fallback in node access table.
+ $node_private = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => TRUE));
+ $this->assertTrue($node_private->langcode == 'hu', 'Node created as Hungarian.');
+
+ // Creating a public node with langcode Hungarian, will be saved as
+ // the fallback in node access table.
+ $node_public = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => FALSE));
+ $this->assertTrue($node_public->langcode == 'hu', 'Node created as Hungarian.');
+
+ // Creating a public node with no special langcode, like when no language
+ // module enabled.
+ $node_no_language = $this->drupalCreateNode(array('private' => FALSE));
+ $this->assertTrue($node_no_language->langcode == LANGUAGE_NOT_SPECIFIED, 'Node created with not specified language.');
+
+ // Query the nodes table as the web user with the node access tag and no
+ // specific langcode.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $web_user)
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // The public node and no language node should be returned. Because no
+ // langcode is given it will use the fallback node.
+ $this->assertEqual(count($nids), 2, 'db_select() returns 2 node');
+ $this->assertTrue(array_key_exists($node_public->nid, $nids), 'Returned node ID is public node.');
+ $this->assertTrue(array_key_exists($node_no_language->nid, $nids), 'Returned node ID is no language node.');
+
+ // Query the nodes table as the web user with the node access tag and
+ // langcode de.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $web_user)
+ ->addMetaData('langcode', 'de')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // Because no nodes are created in German, no nodes are returned.
+ $this->assertTrue(empty($nids), 'db_select() returns an empty result.');
+
+ // Query the nodes table as admin user (full access) with the node access
+ // tag and no specific langcode.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $admin_user)
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // All nodes are returned.
+ $this->assertEqual(count($nids), 3, 'db_select() returns all three nodes.');
+
+ // Query the nodes table as admin user (full access) with the node access
+ // tag and langcode de.
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->addMetaData('account', $admin_user)
+ ->addMetaData('langcode', 'de')
+ ->addTag('node_access');
+ $nids = $select->execute()->fetchAllAssoc('nid');
+
+ // All nodes are returned because node access tag is not invoked when the
+ // user is user 1.
+ $this->assertEqual(count($nids), 3, 'db_select() returns all three nodes.');
+ }
+
}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php
index 4f87348..73b3a44 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php
@@ -23,16 +23,6 @@ class NodeAccessTest extends NodeTestBase {
);
}
- /**
- * 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 @@ class NodeAccessTest extends NodeTestBase {
$node5 = $this->drupalCreateNode();
$this->assertNodeAccess(array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE), $node5, $web_user3);
}
+
}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php b/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php
index f1ed6aa..957e4ee 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php
@@ -30,4 +30,35 @@ abstract class NodeTestBase extends WebTestBase {
$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);
+ }
+ }
+
}
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index d5e396c..5854170 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -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;
diff --git a/core/modules/node/node.install b/core/modules/node/node.install
index ebbce13..b0735d3 100644
--- a/core/modules/node/node.install
+++ b/core/modules/node/node.install
@@ -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',
@@ -710,6 +724,34 @@ function node_update_8014() {
}
/**
+ * 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.
*/
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 02d4e9c..9d5fe32 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -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);
}
}
diff --git a/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info.yml b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info.yml
new file mode 100644
index 0000000..a1ec076
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info.yml
@@ -0,0 +1,8 @@
+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
diff --git a/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module
new file mode 100644
index 0000000..3d67454
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module
@@ -0,0 +1,79 @@
+<?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'));
+}