Skip to content

Commit

Permalink
SA-CORE-2018-001 by cashwilliams, catch, cilefen, droplet, dawehner, …
Browse files Browse the repository at this point in the history
…bonus, agentrickard, David_Rothstein, Chi, Gábor Hojtsy, Heine, Wim Leers, Schnitzel, drpal, effulgentsia, tedbow, tim.plunkett, tstoeckler, xjm, will_c, stefan.r, samuel.mortenson, larowlan, greggles, logaritmisk, mpdonadio, pwolanin, plach
  • Loading branch information
Nathaniel Catchpole committed Feb 20, 2018
1 parent 0c11ebf commit a25f05d
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 10 deletions.
5 changes: 3 additions & 2 deletions core/misc/drupal.es6.js
Expand Up @@ -239,9 +239,10 @@ window.Drupal = { behaviors: {}, locale: {} };
Drupal.checkPlain = function (str) {
str = str.toString()
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return str;
};

Expand Down
2 changes: 1 addition & 1 deletion core/misc/drupal.js
Expand Up @@ -48,7 +48,7 @@ window.Drupal = { behaviors: {}, locale: {} };
};

Drupal.checkPlain = function (str) {
str = str.toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
str = str.toString().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
return str;
};

Expand Down
5 changes: 4 additions & 1 deletion core/modules/comment/src/Controller/CommentController.php
Expand Up @@ -279,9 +279,12 @@ public function replyFormAccess(EntityInterface $entity, $field_name, $pid = NUL
// Check if the user has the proper permissions.
$access = AccessResult::allowedIfHasPermission($account, 'post comments');

// If commenting is open on the entity.
$status = $entity->{$field_name}->status;
$access = $access->andIf(AccessResult::allowedIf($status == CommentItemInterface::OPEN)
->addCacheableDependency($entity));
->addCacheableDependency($entity))
// And if user has access to the host entity.
->andIf(AccessResult::allowedIf($entity->access('view')));

// $pid indicates that this is a reply to a comment.
if ($pid) {
Expand Down
120 changes: 120 additions & 0 deletions core/modules/comment/tests/src/Functional/CommentAccessTest.php
@@ -0,0 +1,120 @@
<?php

namespace Drupal\Tests\comment\Functional;

use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;

/**
* Tests comment administration and preview access.
*
* @group comment
*/
class CommentAccessTest extends BrowserTestBase {

use CommentTestTrait;

/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'comment',
];

/**
* Node for commenting.
*
* @var \Drupal\node\NodeInterface
*/
protected $unpublishedNode;

/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();

$node_type = NodeType::create([
'type' => 'article',
'name' => 'Article',
]);
$node_type->save();
$node_author = $this->drupalCreateUser([
'create article content',
'access comments',
]);

$this->drupalLogin($this->drupalCreateUser([
'edit own comments',
'skip comment approval',
'post comments',
'access comments',
'access content',
]));

$this->addDefaultCommentField('node', 'article');
$this->unpublishedNode = $this->createNode([
'title' => 'This is unpublished',
'uid' => $node_author->id(),
'status' => 0,
'type' => 'article',
]);
$this->unpublishedNode->save();
}

/**
* Tests commenting disabled for access-blocked entities.
*/
public function testCannotCommentOnEntitiesYouCannotView() {
$assert = $this->assertSession();

$comment_url = 'comment/reply/node/' . $this->unpublishedNode->id() . '/comment';

// Commenting on an unpublished node results in access denied.
$this->drupalGet($comment_url);
$assert->statusCodeEquals(403);

// Publishing the node grants access.
$this->unpublishedNode->setPublished(TRUE)->save();
$this->drupalGet($comment_url);
$assert->statusCodeEquals(200);
}

/**
* Tests cannot view comment reply form on entities you cannot view.
*/
public function testCannotViewCommentReplyFormOnEntitiesYouCannotView() {
$assert = $this->assertSession();

// Create a comment on an unpublished node.
$comment = Comment::create([
'entity_type' => 'node',
'name' => 'Tony',
'hostname' => 'magic.example.com',
'mail' => 'foo@example.com',
'subject' => 'Comment on unpublished node',
'entity_id' => $this->unpublishedNode->id(),
'comment_type' => 'comment',
'field_name' => 'comment',
'pid' => 0,
'uid' => $this->unpublishedNode->getOwnerId(),
'status' => 1,
]);
$comment->save();

$comment_url = 'comment/reply/node/' . $this->unpublishedNode->id() . '/comment/' . $comment->id();

// Replying to a comment on an unpublished node results in access denied.
$this->drupalGet($comment_url);
$assert->statusCodeEquals(403);

// Publishing the node grants access.
$this->unpublishedNode->setPublished(TRUE)->save();
$this->drupalGet($comment_url);
$assert->statusCodeEquals(200);
}

}
Expand Up @@ -450,6 +450,7 @@ public function testCommentFunctionality() {
'post comments',
'administer comment fields',
'administer comment types',
'view test entity',
]);
$this->drupalLogin($limited_user);

Expand Down
Expand Up @@ -144,6 +144,7 @@ public function testCommentTokenReplacement() {

// Create a user and a comment.
$user = User::create(['name' => 'alice']);
$user->activate();
$user->save();
$this->postComment($user, 'user body', 'user subject', TRUE);

Expand Down
12 changes: 12 additions & 0 deletions core/modules/node/node.install
Expand Up @@ -255,3 +255,15 @@ function node_update_8400() {
$schema['fields']['realm']['description'] = 'The realm in which the user must possess the grant ID. Modules can define one or more realms by implementing hook_node_grants().';
Database::getConnection()->schema()->changeField('node_access', 'realm', 'realm', $schema['fields']['realm']);
}

/**
* Run a node access rebuild, if required.
*/
function node_update_8401() {
// Get the list of node access modules.
$modules = \Drupal::moduleHandler()->getImplementations('node_grants');
// If multilingual usage, then rebuild node access.
if (count($modules) > 0 && \Drupal::languageManager()->isMultilingual()) {
node_access_needs_rebuild(TRUE);
}
}
3 changes: 2 additions & 1 deletion core/modules/node/src/NodeGrantDatabaseStorage.php
Expand Up @@ -211,6 +211,7 @@ public function write(NodeInterface $node, array $grants, $realm = NULL, $delete
$query = $this->database->insert('node_access')->fields(['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.
$fallback_langcode = $node->getUntranslated()->language()->getId();
foreach ($grants as $grant) {
if ($realm && $realm != $grant['realm']) {
continue;
Expand All @@ -227,7 +228,7 @@ public function write(NodeInterface $node, array $grants, $realm = NULL, $delete
$grant['nid'] = $node->id();
$grant['langcode'] = $grant_langcode;
// The record with the original langcode is used as the fallback.
if ($grant['langcode'] == $node->language()->getId()) {
if ($grant['langcode'] == $fallback_langcode) {
$grant['fallback'] = 1;
}
else {
Expand Down
@@ -0,0 +1,131 @@
<?php

namespace Drupal\Tests\node\Functional;

use Drupal\language\Entity\ConfigurableLanguage;

/**
* Tests that the node_access system stores the proper fallback marker.
*
* @group node
*/
class NodeAccessLanguageFallbackTest extends NodeTestBase {

/**
* Enable language and a non-language-aware node access module.
*
* @var array
*/
public static $modules = ['language', 'node_access_test', 'content_translation'];

/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();

// After enabling a node access module, the {node_access} table has to be
// rebuilt.
node_access_rebuild();

// Add Hungarian, Catalan, and Afrikaans.
ConfigurableLanguage::createFromLangcode('hu')->save();
ConfigurableLanguage::createFromLangcode('ca')->save();
ConfigurableLanguage::createFromLangcode('af')->save();

// Enable content translation for the current entity type.
\Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
}

/**
* Tests node access fallback handling with multiple node languages.
*/
public function testNodeAccessLanguageFallback() {
// The node_access_test module allows nodes to be marked private. We need to
// ensure that system honors the fallback system of node access properly.
// Note that node_access_test_language is language-sensitive and does not
// apply to the fallback test.

// Create one node in Hungarian and marked as private.
$node = $this->drupalCreateNode([
'body' => [[]],
'langcode' => 'hu',
'private' => [['value' => 1]],
'status' => 1,
]);

// There should be one entry in node_access, with fallback set to hu.
$this->checkRecords(1, 'hu');

// Create a translation user.
$admin = $this->drupalCreateUser([
'bypass node access',
'administer nodes',
'translate any entity',
'administer content translation',
]);
$this->drupalLogin($admin);
$this->drupalGet('node/' . $node->id() . '/translations');
$this->assertSession()->statusCodeEquals(200);

// Create a Catalan translation through the UI.
$url_options = ['language' => \Drupal::languageManager()->getLanguage('ca')];
$this->drupalGet('node/' . $node->id() . '/translations/add/hu/ca', $url_options);
$this->assertSession()->statusCodeEquals(200);
// Save the form.
$this->getSession()->getPage()->pressButton('Save (this translation)');
$this->assertSession()->statusCodeEquals(200);

// Check the node access table.
$this->checkRecords(2, 'hu');

// Programmatically create a translation. This process lets us check that
// both forms and code behave in the same way.
$storage = \Drupal::entityTypeManager()->getStorage('node');
// Reload the node.
$node = $storage->load(1);
// Create an Afrikaans translation.
$translation = $node->addTranslation('af');
$translation->title->value = $this->randomString();
$translation->status = 1;
$node->save();

// Check the node access table.
$this->checkRecords(3, 'hu');

// For completeness, edit the Catalan version again.
$this->drupalGet('node/' . $node->id() . '/edit', $url_options);
$this->assertSession()->statusCodeEquals(200);
// Save the form.
$this->getSession()->getPage()->pressButton('Save (this translation)');
$this->assertSession()->statusCodeEquals(200);
// Check the node access table.
$this->checkRecords(3, 'hu');
}

/**
* Queries the node_access table and checks for proper storage.
*
* @param int $count
* The number of rows expected by the query (equal to the translation
* count).
* @param $langcode
* The expected language code set as the fallback property.
*/
public function checkRecords($count, $langcode = 'hu') {
$select = \Drupal::database()
->select('node_access', 'na')
->fields('na', ['nid', 'fallback', 'langcode', 'grant_view'])
->condition('na.realm', 'node_access_test', '=')
->condition('na.gid', 8888, '=');
$records = $select->execute()->fetchAll();
// Check that the expected record count is returned.
$this->assertEquals(count($records), $count);
// The fallback value is 'hu' and should be set to 1. For other languages,
// it should be set to 0. Casting to boolean lets us run that comparison.
foreach ($records as $record) {
$this->assertEquals((bool) $record->fallback, $record->langcode === $langcode);
}
}

}
Expand Up @@ -6,6 +6,7 @@
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormBase;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
Expand All @@ -31,22 +32,33 @@ class SystemBrandingOffCanvasForm extends PluginFormBase implements ContainerInj
*/
protected $configFactory;

/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;

/**
* SystemBrandingOffCanvasForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $current_user) {
$this->configFactory = $config_factory;
$this->currentUser = $current_user;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory')
$container->get('config.factory'),
$container->get('current_user')
);
}

Expand All @@ -67,6 +79,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#type' => 'details',
'#title' => t('Site details'),
'#open' => TRUE,
'#access' => $this->currentUser->hasPermission('administer site configuration'),
];
$form['site_information']['site_name'] = [
'#type' => 'textfield',
Expand Down
Expand Up @@ -87,6 +87,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#type' => 'details',
'#title' => $this->t('Edit menu %label', ['%label' => $this->menu->label()]),
'#open' => TRUE,
'#access' => $this->menu->access('edit'),
];
$form['entity_form'] += $this->getEntityForm($this->menu)->buildForm([], $form_state);

Expand Down

0 comments on commit a25f05d

Please sign in to comment.