diff --git a/core/modules/file/lib/Drupal/file/Type/FileItem.php b/core/modules/file/lib/Drupal/file/Type/FileItem.php index e1c7d24e33e0114314f147ac97fb2f97e0b0c1d2..cecce0569d65821e0b5ad4ea645d72caadab986f 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 @@ 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.'); } diff --git a/core/modules/node/lib/Drupal/node/NodeAccessController.php b/core/modules/node/lib/Drupal/node/NodeAccessController.php index 815c2ab95a95997b3a8a13d37dbb898add5ad84b..ec68b7eac6c07ecc4b698f73c7f6e39fb5a3a47c 100644 --- a/core/modules/node/lib/Drupal/node/NodeAccessController.php +++ b/core/modules/node/lib/Drupal/node/NodeAccessController.php @@ -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); 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 0000000000000000000000000000000000000000..6b60907cce3c9d45aa8210a9102ab191d9bff4aa --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php @@ -0,0 +1,324 @@ + '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 0000000000000000000000000000000000000000..8e5c4548bc8f0e01856bbe17e5c9b1255e54617b --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php @@ -0,0 +1,271 @@ + '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 b418ef4af75893f94ff2ea74b24aa8632104b1bd..fceb154068544d3176109a62fa4db2b6625f487f 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 @@ function testNodeAccess() { '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 4f87348738e2a4207102cfbe3de1ad78b8040298..73b3a44af7cb487535035470d03f62d11fee84c1 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 @@ 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); } + } diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php b/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php index f1ed6aa44a2d3fe2379f3b429c04ba7fc7151524..957e4eeb531940bf2d4417433e928eacc629b758 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 @@ 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); + } + } + } diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index d5e396cc9962aa771deedea6e573140538e303ac..5854170f7e4ed163a6b3a780034b17dff789d957 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 ebbce13a638c1e8de529ad29944becdfdb07482e..b0735d34c297ee1d907b4c3dd502acc6821f1f56 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', @@ -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. diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 02d4e9ca5c08c9c707d244766f0fd8d0743b1ccd..9d5fe32d02f8b5bcb6b039a76e0eb5052db22a2a 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 0000000000000000000000000000000000000000..a1ec076351baae9a673ef92beb17083d1b652732 --- /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 0000000000000000000000000000000000000000..3d67454df2f916ec49358edd5d71a050b44ea7db --- /dev/null +++ b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module @@ -0,0 +1,79 @@ +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')); +}