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..5d8701e --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php @@ -0,0 +1,267 @@ + 'Node access language-aware combination', + 'description' => 'Tests node access functionality with multiple languages and two node access modules.', + 'group' => 'Node', + ); + } + + function setUp() { + parent::setUp(); + + node_access_rebuild(); + + // Clear permissions for authenticated users. + db_delete('role_permission') + ->condition('rid', DRUPAL_AUTHENTICATED_RID) + ->execute(); + + // Add Hungarian and Catalan. + $language = new Language(array( + 'langcode' => 'hu', + )); + language_save($language); + $language = new Language(array( + 'langcode' => 'ca', + )); + language_save($language); + + $this->web_user = $this->drupalCreateUser(array('access content')); + + // 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'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 0)), + 'ca' => array(0 => array('value' => 0)), + ), + 'private' => FALSE, + )); + $this->nodes['private_both_public'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 0)), + 'ca' => array(0 => array('value' => 0)), + ), + 'private' => TRUE, + )); + $this->nodes['public_hu_private'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 1)), + 'ca' => array(0 => array('value' => 0)), + ), + 'private' => FALSE, + )); + $this->nodes['public_ca_private'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 0)), + 'ca' => array(0 => array('value' => 1)), + ), + 'private' => FALSE, + )); + $this->nodes['public_both_private'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 1)), + 'ca' => array(0 => array('value' => 1)), + ), + 'private' => FALSE, + )); + $this->nodes['private_both_private'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 1)), + 'ca' => array(0 => 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 public, any existing translations should be + // accessible, regardless of whether they are marked private. Access should + // only be denied when a translation that does not exist is specifically + // requested. + // With the Hungarian translation marked as private, but the node public: + $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: + $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: + $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'); + + // 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 (with public Hungarian translations). + $this->assertEqual(count($nids), 3, 'db_select() returns 3 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.'); + + // 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'); + + // The results should be the same as the for default. + $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 user 1 (full access) with the node access tag + // and no specific langcode. + $select = db_select('node', 'n') + ->fields('n', array('nid')) + ->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 user 1 (full access) with the node access tag + // and langcode de. + $select = db_select('node', 'n') + ->fields('n', array('nid')) + ->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 occr 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/NodeAccessLanguageAwareTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php new file mode 100644 index 0000000..f89c191 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php @@ -0,0 +1,235 @@ + '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', + ); + } + + function setUp() { + parent::setUp(); + + node_access_rebuild(); + + // Clear permissions for authenticated users. + db_delete('role_permission') + ->condition('rid', DRUPAL_AUTHENTICATED_RID) + ->execute(); + + // Create a normal authenticated user. + $this->web_user = $this->drupalCreateUser(array('access content')); + + // 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 four Hungarian nodes with Catalan translations: + // 1. One with neither language marked as private. + // 2. One with only the Hungarian translation private. + // 3. One with only the Catalan translation private. + // 4. One with both the Hungarian and Catalan translations private. + $this->nodes['both_public'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 0)), + 'ca' => array(0 => array('value' => 0)), + ) + )); + $this->nodes['ca_private'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 0)), + 'ca' => array(0 => array('value' => 1)), + ) + )); + $this->nodes['hu_private'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 1)), + 'ca' => array(0 => array('value' => 0)), + ) + )); + $this->nodes['both_private'] = $this->drupalCreateNode(array( + 'body' => array('hu' => array(array())), + 'langcode' => 'hu', + 'field_private' => array( + 'hu' => array(0 => array('value' => 1)), + 'ca' => array(0 => 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'); + + // 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'); + + // 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 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.'); + + // 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'); + + // The results should be the same as the for default. + $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 user 1 (full access) with the node access tag + // and no specific langcode. + $select = db_select('node', 'n') + ->fields('n', array('nid')) + ->addTag('node_access'); + $nids = $select->execute()->fetchAllAssoc('nid'); + + // All nodes are returned. + $this->assertEqual(count($nids), 4, 'db_select() returns all nodes.'); + + // Query the nodes table as user 1 (full access) with the node access tag + // and langcode de. + $select = db_select('node', 'n') + ->fields('n', array('nid')) + ->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 occr when the user is user 1. + $this->assertEqual(count($nids), 4, '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 6ff6ae0..33a67be 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php @@ -24,34 +24,24 @@ 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(); + node_access_rebuild(); + // Clear permissions for authenticated users. db_delete('role_permission') ->condition('rid', DRUPAL_AUTHENTICATED_RID) ->execute(); - } - /** - * Runs tests for node_access function with multiple languages. - */ - function testNodeAccess() { + // Enable the private node feature of node_access_test module. + state()->set('node_access_test.private', TRUE); + // Add Hungarian and Catalan. $language = new Language(array( 'langcode' => 'hu', @@ -61,31 +51,189 @@ 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('hu' => array(array())), 'langcode' => 'hu', 'private' => FALSE)); + $this->assertTrue($node_public->langcode == 'hu', 'Node created as Hungarian.'); + + // Tests the default access 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 provided if requested with no language. + $this->assertNodeAccess($expected_node_access, $node_public_no_language, $web_user); + + // Tests that access not provided 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 provided 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('hu' => 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('hu' => 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 not provided if requested with no language. + $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user); + + // Tests that access not provided 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. drupal_static_reset('node_access'); variable_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 not provided 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 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() { + $web_user = $this->drupalCreateUser(array('access content')); - // Tests that Catalan is not accessible anymore. - $this->assertNodeAccess(array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE), $node, $web_user, 'ca'); + // Creating a private node with langcode Hungarian, will be saved as + // the fallback in node access table. + $node_private = $this->drupalCreateNode(array('body' => array('hu' => 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('hu' => 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 user 1 (full access) with the node access tag + // and no specific langcode. + $select = db_select('node', 'n') + ->fields('n', array('nid')) + ->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 user 1 (full access) with the node access tag + // and langcode de. + $select = db_select('node', 'n') + ->fields('n', array('nid')) + ->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 @@ 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 1bba558..40121eb 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 5e5b270..7f508d5 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -234,11 +234,16 @@ 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 + * set automatically from the $node parameter and the node's original + * language (if specified) is used as a fallback. + * + * 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 +251,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 node that a deny all grant is not written to the database; denies are + * implicit. * * @param Drupal\node\Node $node * The node that has just been saved. @@ -271,8 +275,9 @@ function hook_node_access_records(Drupal\node\Node $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 +285,7 @@ function hook_node_access_records(Drupal\node\Node $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 +298,7 @@ function hook_node_access_records(Drupal\node\Node $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 12a6945..552a5a0 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', 'langcode', 'realm'), 'foreign keys' => array( 'affected_node' => array( 'table' => 'node', @@ -707,6 +721,34 @@ function node_update_8013() { } /** + * Add language.langcode and fallback field to node_access table. + */ +function node_update_8014() { + // 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', 'langcode', 'realm')); +} + +/** * @} 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 b64a1dc..bbd6fa0 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -2606,7 +2606,24 @@ function node_access($op, $node, $account = NULL, $langcode = NULL) { // to an empty langcode if a node type was requested. The latter is purely // for caching purposes. if (empty($langcode)) { - $langcode = (is_object($node) && isset($node->nid)) ? $node->langcode : ''; + // Initialize the langcode as an empty string. + $langcode = ''; + if (is_object($node) && isset($node->nid)) { + // Default to the node's default 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; + } + } + } } // If we've already checked access for this node, user and op, return from @@ -2652,9 +2669,16 @@ function node_access($op, $node, $account = NULL, $langcode = NULL) { $query = db_select('node_access'); $query->addExpression('1'); $query->condition('grant_' . $op, 1, '>='); - $nids = db_or()->condition('nid', $node->nid); + // Check if grant is given for the node in this language, so we check that grant. + $nids = db_and() + ->condition('nid', $node->nid) + ->condition('langcode', $langcode); + // But if the node is published, we take into account the default for this grant, + // which is saved with nid = 0. if ($node->status) { - $nids->condition('nid', 0); + $nids = db_or() + ->condition($nids) + ->condition('nid', 0); } $query->condition($nids); $query->range(0, 1); @@ -2909,6 +2933,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 @@ -2956,8 +2983,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() @@ -2972,6 +2999,15 @@ function node_query_node_access_alter(AlterableInterface $query) { $subquery->condition($grant_conditions); } $subquery->condition('na.grant_' . $op, 1, '>='); + // If no langcode is given, add a condition for checking the fallback language. If the language is given, + // just use it as a condition. + 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"); @@ -3022,11 +3058,8 @@ function node_access_acquire_grants(Node $node, $delete = TRUE) { * @param Drupal\node\Node $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. @@ -3045,18 +3078,35 @@ function _node_access_write_grants(Node $node, $grants, $realm = NULL, $delete = } $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(); diff --git a/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info new file mode 100644 index 0000000..e8f72c5 --- /dev/null +++ b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info @@ -0,0 +1,7 @@ +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..27e5883 --- /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')); +}