Commit faf360b6 authored by xjm's avatar xjm

SA-CORE-2018-001 by cashwilliams, catch, cilefen, droplet, dawehner, bonus,...

SA-CORE-2018-001 by cashwilliams, catch, cilefen, droplet, dawehner, 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

(cherry picked from commit a2ae6fd0)
parent d0af739e
......@@ -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;
};
......
......@@ -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;
};
......
......@@ -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) {
......
<?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);
}
}
......@@ -450,6 +450,7 @@ public function testCommentFunctionality() {
'post comments',
'administer comment fields',
'administer comment types',
'view test entity',
]);
$this->drupalLogin($limited_user);
......
......@@ -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);
......
......@@ -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);
}
}
......@@ -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;
......@@ -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 {
......
<?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);
}
}
}
......@@ -73,7 +73,11 @@ protected function setUp() {
*
* @dataProvider providerTestBlocks
*/
public function testBlocks($theme, $block_plugin, $new_page_text, $element_selector, $label_selector, $button_text, $toolbar_item) {
public function testBlocks($theme, $block_plugin, $new_page_text, $element_selector, $label_selector, $button_text, $toolbar_item, $permissions) {
if ($permissions) {
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), $permissions);
}
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$this->enableTheme($theme);
......@@ -174,6 +178,7 @@ public function providerTestBlocks() {
'label_selector' => 'h2',
'button_text' => 'Save Powered by Drupal',
'toolbar_item' => '#toolbar-item-user',
NULL,
],
"$theme: block-branding" => [
'theme' => $theme,
......@@ -183,6 +188,7 @@ public function providerTestBlocks() {
'label_selector' => "a[rel='home']:last-child",
'button_text' => 'Save Site branding',
'toolbar_item' => '#toolbar-item-administration',
['administer site configuration'],
],
"$theme: block-search" => [
'theme' => $theme,
......@@ -192,6 +198,7 @@ public function providerTestBlocks() {
'label_selector' => 'h2',
'button_text' => 'Save Search form',
'toolbar_item' => NULL,
NULL,
],
// This is the functional JS test coverage accompanying
// \Drupal\Tests\settings_tray\Functional\SettingsTrayTest::testPossibleAnnotations().
......@@ -203,6 +210,7 @@ public function providerTestBlocks() {
'label_selector' => NULL,
'button_text' => NULL,
'toolbar_item' => NULL,
NULL,
],
// This is the functional JS test coverage accompanying
// \Drupal\Tests\settings_tray\Functional\SettingsTrayTest::testPossibleAnnotations().
......@@ -214,6 +222,7 @@ public function providerTestBlocks() {
'label_selector' => NULL,
'button_text' => NULL,
'toolbar_item' => NULL,
NULL,
],
];
}
......@@ -551,6 +560,71 @@ protected function isLabelInputVisible() {
return $this->getSession()->getPage()->find('css', static::LABEL_INPUT_SELECTOR)->isVisible();
}
/**
* Tests access to block forms with related configuration is correct.
*/
public function testBlockConfigAccess() {
$page = $this->getSession()->getPage();
$web_assert = $this->assertSession();
// Confirm that System Branding block does not expose Site Name field
// without permission.
$block = $this->placeBlock('system_branding_block');
$this->drupalGet('user');
$this->enableEditMode();
$this->openBlockForm($this->getBlockSelector($block));
// The site name field should not appear because the user doesn't have
// permission.
$web_assert->fieldNotExists('settings[site_information][site_name]');
$page->pressButton('Save Site branding');
$this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
$web_assert->assertWaitOnAjaxRequest();
// Confirm we did not save changes to the configuration.
$this->assertEquals('Drupal', \Drupal::configFactory()->getEditable('system.site')->get('name'));
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['administer site configuration']);
$this->drupalGet('user');
$this->openBlockForm($this->getBlockSelector($block));
// The site name field should appear because the user does have permission.
$web_assert->fieldExists('settings[site_information][site_name]');
// Confirm that the Menu block does not expose menu configuration without
// permission.
// Add a link or the menu will not render.
$menu_link_content = MenuLinkContent::create([
'title' => 'This is on the menu',
'menu_name' => 'main',
'link' => ['uri' => 'route:<front>'],
]);
$menu_link_content->save();
$this->assertNotEmpty($menu_link_content->isEnabled());
$menu_without_overrides = \Drupal::configFactory()->getEditable('system.menu.main')->get();
$block = $this->placeBlock('system_menu_block:main');
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$this->openBlockForm($this->getBlockSelector($block));
// Edit menu form should not appear because the user doesn't have
// permission.
$web_assert->pageTextNotContains('Edit menu');
$page->pressButton('Save Main navigation');
$this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
$web_assert->assertWaitOnAjaxRequest();
// Confirm we did not save changes to the menu or the menu link.
$this->assertEquals($menu_without_overrides, \Drupal::configFactory()->getEditable('system.menu.main')->get());
$menu_link_content = MenuLinkContent::load($menu_link_content->id());
$this->assertNotEmpty($menu_link_content->isEnabled());
// Confirm menu is still on the page.
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['administer menu']);
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$this->openBlockForm($this->getBlockSelector($block));
// Edit menu form should appear because the user does have permission.
$web_assert->pageTextContains('Edit menu');
}
/**
* Test that validation errors appear in the off-canvas dialog.
*/
......@@ -634,6 +708,7 @@ public function testOverriddenBlock() {
public function testOverriddenConfigurationRemoved() {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['administer site configuration', 'administer menu']);
// Confirm the branding block does include 'site_information' section when
// the site name is not overridden.
......
......@@ -7,6 +7,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;
/**
......@@ -30,14 +31,24 @@ 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;
}
/**
......@@ -45,7 +56,8 @@ public function __construct(ConfigFactoryInterface $config_factory) {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory')
$container->get('config.factory'),
$container->get('current_user')
);
}
......@@ -68,7 +80,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#type' => 'details',
'#title' => t('Site details'),
'#open' => TRUE,
'#access' => AccessResult::allowedIf(!$site_config_immutable->hasOverrides('name') && !$site_config_immutable->hasOverrides('slogan')),
'#access' => $this->currentUser->hasPermission('administer site configuration') && !$site_config_immutable->hasOverrides('name') && !$site_config_immutable->hasOverrides('slogan'),
];
$form['site_information']['site_name'] = [
'#type' => 'textfield',
......
......@@ -3,7 +3,6 @@
namespace Drupal\system\Form;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityStorageInterface;
......@@ -98,7 +97,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#type' => 'details',
'#title' => $this->t('Edit menu %label', ['%label' => $this->menu->label()]),
'#open' => TRUE,
'#access' => AccessResult::allowedIf(!$this->hasMenuOverrides()),
'#access' => !$this->hasMenuOverrides() && $this->menu->access('edit'),
];
$form['entity_form'] += $this->getEntityForm($this->menu)->buildForm([], $form_state);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment