diff --git a/modules/node/node.module b/modules/node/node.module index 7a6246d..bcd8e8c 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -3422,20 +3422,22 @@ function _node_query_node_access_alter($query, $type) { $grant_conditions = db_or(); // If any grant exists for the specified user, then user has access // to the node for the specified operation. + $counter = 0; foreach ($grants as $realm => $gids) { foreach ($gids as $gid) { $grant_conditions->condition(db_and() - ->condition('na.gid', $gid) - ->condition('na.realm', $realm) + ->where('na.gid = :gid_' . $counter . '_' . $gid, array(':gid_' . $counter . '_' . $gid => $gid)) + ->where('na.realm = :realm_' . $counter . '_' . $gid, array(':realm_' . $counter . '_' . $gid => $realm)) ); } + $counter++; } // Attach conditions to the subquery for nodes. if (count($grant_conditions->conditions())) { $subquery->condition($grant_conditions); } - $subquery->condition('na.grant_' . $op, 1, '>='); + $subquery->where('na.grant_' . $op . ' >= 1'); $field = 'nid'; // Now handle entities. if ($type == 'entity') { @@ -3451,7 +3453,17 @@ function _node_query_node_access_alter($query, $type) { } // Otherwise attach it to the node query itself. else { - $query->exists($subquery); + if (empty($tableinfo['join type'])) { + // If we are looking at the main table of the query, apply the + // subquery directly. + $query->exists($subquery); + } + else { + // If we are looking at a joined table, add the node access check + // to the join condition. + $tables[$nalias]['condition'] .= ' AND EXISTS(' . (string)$subquery . ')'; + $tables[$nalias]['arguments'] += $subquery->arguments(); + } } } } diff --git a/modules/node/node.test b/modules/node/node.test index 5c9118e..4251ba7 100644 --- a/modules/node/node.test +++ b/modules/node/node.test @@ -2842,6 +2842,117 @@ class NodeEntityViewModeAlterTest extends NodeWebTestCase { } } + /** + * Tests the interaction of the node access system with joined Query objects. + */ +class NodeAccessSubqueryTest extends NodeWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Node access subqueries', + 'description' => 'Tests that node access checks get applied to the node base table across referenced fields.', + 'group' => 'Node', + ); + } + + public function setUp() { + $modules = array('node_access_test'); + parent::setUp($modules); + + node_access_rebuild(); + variable_set('node_access_test_private', TRUE); + + // Create some users. + $this->admin_user = $this->drupalCreateUser(array('access content', 'bypass node access')); + $this->user = $this->drupalCreateUser(array('access content')); + + // Add a custom field to the page content type. + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); + $this->field = field_create_field( + array( + 'field_name' => $this->field_name, + 'type' => 'number_integer', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED + ) + ); + $instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'node', + 'bundle' => 'page', + ); + $this->instance = field_create_instance($instance); + } + + /** + * Tests grants in subqueries. + */ + function testNodeAccessSubquery() { + // Create a page node. + $langcode = LANGUAGE_NONE; + $field_data = array(); + $nodes = array(); + + $nodes[1] = $this->drupalCreateNode(array('title' => 'Private node 1', 'private'=> TRUE, 'uid' => $this->admin_user->uid, 'status'=>0)); + $nodes[2] = $this->drupalCreateNode(array( + 'title' => 'Public node 2', + $this->field_name => array( + $langcode => array( + 0 => array( + 'value' => $nodes[1]->nid + ) + ) + ), + 'private' => FALSE, + 'uid' => $this->user->uid, + 'status'=>1, + )); + $nodes[3] = $this->drupalCreateNode(array('title' => 'Public node 3', 'private' => TRUE, 'uid' => $this->user->uid, 'status'=>1)); + $nodes[4] = $this->drupalCreateNode(array( + 'title' => 'Public node 4', + $this->field_name => array( + $langcode => array( + 0 => array( + 'value' => $nodes[3]->nid + ) + ) + ), + 'private' => FALSE, + 'uid' => $this->user->uid, + 'status'=>1, + )); + $nodes[5] = $this->drupalCreateNode(array('title' => 'Public node 5', 'uid' => $this->user->uid, 'private' => FALSE, 'status'=>1)); + + $expected_admin_count = 5; // one row for each node + $expected_user_count = 4; // one row for each node with my uid + + // Set up template query. + $join_table = _field_sql_storage_tablename($this->field); + $join_column = $this->field_name . '_value'; + + $base_query = db_select('node', 'n'); + $base_query->addTag('node_access'); + $base_query->addJoin('LEFT OUTER', $join_table, 'jf', 'n.vid = jf.revision_id'); + + // Now add subquery join. + $base_query->addJoin('LEFT OUTER', 'node', 's', 'jf.' . $join_column .' = s.nid'); + $base_query + ->fields('n', array('nid', 'title')) + ->fields('s', array('nid', 'title')); + + // We need to clone, because the node_access tag is only altered once. + $query = clone $base_query; + $query->addMetaData('account', $this->admin_user); + $num_rows = $query->countQuery()->execute()->fetchField(); + $this->assertEqual($num_rows, $expected_admin_count, "Admin user should get $expected_admin_count rows returned after initial load. Actual: $num_rows"); + + // Need a fresh copy of the query, because the node access alteration happens in the pre-execute phase, which only happens once. + $query = clone $base_query; + $query->addMetaData('account', $this->user); + $num_rows = $query->countQuery()->execute()->fetchField(); + $this->assertEqual($num_rows, $expected_user_count, "Regular user should get $expected_user_count rows returned after initial load. Actual: $num_rows"); + } +} + /** * Tests the cache invalidation of node operations. */