Update to Drupal 8.2.3. For more information, see https://www.drupal.org/project/drupal/releases/8.2.3

This commit is contained in:
Pantheon Automation 2016-11-16 12:26:40 -08:00 committed by Greg Anderson
parent 507b45a0ed
commit 0a95b8440e
19 changed files with 300 additions and 15 deletions

View file

@ -1,3 +1,7 @@
Drupal 8.2.3, 2016-11-16
------------------------
- Fixed security issues. See SA-CORE-2016-005.
Drupal 8.2.0, 2016-10-05
------------------------
- Updated the git repository configuration to not normalize line endings for

View file

@ -81,7 +81,7 @@ class Drupal {
/**
* The current system version.
*/
const VERSION = '8.2.2';
const VERSION = '8.2.3';
/**
* Core API compatibility.

View file

@ -5,7 +5,6 @@ namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
/**
* Query builder for SELECT statements.
*
@ -456,6 +455,22 @@ class Select extends Query implements SelectInterface {
// Modules may alter all queries or only those having a particular tag.
if (isset($this->alterTags)) {
// Many contrib modules as well as Entity Reference in core assume that
// query tags used for access-checking purposes follow the pattern
// $entity_type . '_access'. But this is not the case for taxonomy terms,
// since the core Taxonomy module used to add term_access instead of
// taxonomy_term_access to its queries. Provide backwards compatibility
// by adding both tags here instead of attempting to fix all contrib
// modules in a coordinated effort.
// TODO:
// - Extract this mechanism into a hook as part of a public (non-security)
// issue.
// - Emit E_USER_DEPRECATED if term_access is used.
// https://www.drupal.org/node/2575081
$term_access_tags = array('term_access' => 1, 'taxonomy_term_access' => 1);
if (array_intersect_key($this->alterTags, $term_access_tags)) {
$this->alterTags += $term_access_tags;
}
$hooks = array('query');
foreach ($this->alterTags as $tag => $value) {
$hooks[] = 'query_' . $tag;

View file

@ -190,6 +190,7 @@ class MachineName extends Textfield {
$element['#attached']['library'][] = 'core/drupal.machine-name';
$options = [
'replace_pattern',
'replace_token',
'replace',
'maxlength',
'target',
@ -198,6 +199,11 @@ class MachineName extends Textfield {
'field_suffix',
'suffix',
];
/** @var \Drupal\Core\Access\CsrfTokenGenerator $token_generator */
$token_generator = \Drupal::service('csrf_token');
$element['#machine_name']['replace_token'] = $token_generator->get($element['#machine_name']['replace_pattern']);
$element['#attached']['drupalSettings']['machineName']['#' . $source['#id']] = array_intersect_key($element['#machine_name'], array_flip($options));
$element['#attached']['drupalSettings']['langcode'] = $language->getId();

View file

@ -186,6 +186,8 @@
* @param {string} settings.replace_pattern
* A regular expression (without modifiers) matching disallowed characters
* in the machine name; e.g., '[^a-z0-9]+'.
* @param {string} settings.replace_token
* A token to validate the regular expression.
* @param {string} settings.replace
* A character to replace disallowed characters with; e.g., '_' or '-'.
* @param {number} settings.maxlength
@ -199,6 +201,7 @@
text: source,
langcode: drupalSettings.langcode,
replace_pattern: settings.replace_pattern,
replace_token: settings.replace_token,
replace: settings.replace,
lowercase: true
});

View file

@ -4,7 +4,9 @@ namespace Drupal\system;
use Drupal\Component\Transliteration\TransliterationInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -21,14 +23,24 @@ class MachineNameController implements ContainerInjectionInterface {
*/
protected $transliteration;
/**
* The token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $tokenGenerator;
/**
* Constructs a MachineNameController object.
*
* @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
* The transliteration helper.
* @param \Drupal\Core\Access\CsrfTokenGenerator $token_generator
* The token generator.
*/
public function __construct(TransliterationInterface $transliteration) {
public function __construct(TransliterationInterface $transliteration, CsrfTokenGenerator $token_generator) {
$this->transliteration = $transliteration;
$this->tokenGenerator = $token_generator;
}
/**
@ -36,7 +48,8 @@ class MachineNameController implements ContainerInjectionInterface {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('transliteration')
$container->get('transliteration'),
$container->get('csrf_token')
);
}
@ -54,6 +67,7 @@ class MachineNameController implements ContainerInjectionInterface {
$text = $request->query->get('text');
$langcode = $request->query->get('langcode');
$replace_pattern = $request->query->get('replace_pattern');
$replace_token = $request->query->get('replace_token');
$replace = $request->query->get('replace');
$lowercase = $request->query->get('lowercase');
@ -61,7 +75,15 @@ class MachineNameController implements ContainerInjectionInterface {
if ($lowercase) {
$transliterated = Unicode::strtolower($transliterated);
}
if (isset($replace_pattern) && isset($replace)) {
if (!isset($replace_token)) {
throw new AccessDeniedException("Missing 'replace_token' query parameter.");
}
elseif (!$this->tokenGenerator->validate($replace_token, $replace_pattern)) {
throw new AccessDeniedException("Invalid 'replace_token' query parameter.");
}
// Quote the pattern delimiter and remove null characters to avoid the e
// or other modifiers being injected.
$transliterated = preg_replace('@' . strtr($replace_pattern, ['@' => '\@', chr(0) => '']) . '@', $replace, $transliterated);

View file

@ -1730,3 +1730,19 @@ function system_update_8201() {
/**
* @} End of "addtogroup updates-8.2.0".
*/
/**
* @addtogroup updates-8.2.3
* @{
*/
/**
* Clear caches due to behavior change in MachineName element.
*/
function system_update_8202() {
// Empty update to cause a cache rebuild.
}
/**
* @} End of "addtogroup updates-8.2.3".
*/

View file

@ -2,9 +2,12 @@
namespace Drupal\Tests\system\Unit\Transliteration;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Transliteration\PhpTransliteration;
use Drupal\system\MachineNameController;
use Prophecy\Argument;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\Request;
/**
@ -21,10 +24,22 @@ class MachineNameControllerTest extends UnitTestCase {
*/
protected $machineNameController;
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $tokenGenerator;
protected function setUp() {
parent::setUp();
// Create the machine name controller.
$this->machineNameController = new MachineNameController(new PhpTransliteration());
$this->tokenGenerator = $this->prophesize(CsrfTokenGenerator::class);
$this->tokenGenerator->validate(Argument::cetera())->will(function ($args) {
return $args[0] === 'token-' . $args[1];
});
$this->machineNameController = new MachineNameController(new PhpTransliteration(), $this->tokenGenerator->reveal());
}
/**
@ -38,7 +53,7 @@ class MachineNameControllerTest extends UnitTestCase {
* - The expected content of the JSONresponse.
*/
public function providerTestMachineNameController() {
return array(
$valid_data = array(
array(array('text' => 'Bob', 'langcode' => 'en'), '"Bob"'),
array(array('text' => 'Bob', 'langcode' => 'en', 'lowercase' => TRUE), '"bob"'),
array(array('text' => 'Bob', 'langcode' => 'en', 'replace' => 'Alice', 'replace_pattern' => 'Bob'), '"Alice"'),
@ -53,6 +68,15 @@ class MachineNameControllerTest extends UnitTestCase {
array(array('text' => 'Bob', 'langcode' => 'en', 'lowercase' => TRUE, 'replace' => 'fail()', 'replace_pattern' => ".*@e\0"), '"bob"'),
array(array('text' => 'Bob@e', 'langcode' => 'en', 'lowercase' => TRUE, 'replace' => 'fail()', 'replace_pattern' => ".*@e\0"), '"fail()"'),
);
$valid_data = array_map(function ($data) {
if (isset($data[0]['replace_pattern'])) {
$data[0]['replace_token'] = 'token-' . $data[0]['replace_pattern'];
}
return $data;
}, $valid_data);
return $valid_data;
}
/**
@ -73,4 +97,24 @@ class MachineNameControllerTest extends UnitTestCase {
$this->assertEquals($expected_content, $json->getContent());
}
/**
* Tests the pattern validation.
*/
public function testMachineNameControllerWithInvalidReplacePattern() {
$request = Request::create('', 'GET', ['text' => 'Bob', 'langcode' => 'en', 'replace' => 'Alice', 'replace_pattern' => 'Bob', 'replace_token' => 'invalid']);
$this->setExpectedException(AccessDeniedException::class, "Invalid 'replace_token' query parameter.");
$this->machineNameController->transliterate($request);
}
/**
* Tests the pattern validation with a missing token.
*/
public function testMachineNameControllerWithMissingToken() {
$request = Request::create('', 'GET', ['text' => 'Bob', 'langcode' => 'en', 'replace' => 'Alice', 'replace_pattern' => 'Bob']);
$this->setExpectedException(AccessDeniedException::class, "Missing 'replace_token' query parameter.");
$this->machineNameController->transliterate($request);
}
}

View file

@ -192,7 +192,7 @@ class TaxonomyIndexTid extends ManyToOne {
// https://www.drupal.org/node/1821274.
->sort('weight')
->sort('name')
->addTag('term_access');
->addTag('taxonomy_term_access');
if ($this->options['limit']) {
$query->condition('vid', $vocabulary->id());
}

View file

@ -132,7 +132,7 @@ class NodeTermData extends RelationshipPluginBase {
$query = db_select('taxonomy_term_field_data', 'td');
$query->addJoin($def['type'], 'taxonomy_index', 'tn', 'tn.tid = td.tid');
$query->condition('td.vid', array_filter($this->options['vids']), 'IN');
$query->addTag('term_access');
$query->addTag('taxonomy_term_access');
$query->fields('td');
$query->fields('tn', array('nid'));
$def['table formula'] = $query;

View file

@ -126,7 +126,7 @@ class TermStorage extends SqlContentEntityStorage implements TermStorageInterfac
$query->addField('t', 'tid');
$query->condition('h.tid', $tid);
$query->condition('t.default_langcode', 1);
$query->addTag('term_access');
$query->addTag('taxonomy_term_access');
$query->orderBy('t.weight');
$query->orderBy('t.name');
if ($ids = $query->execute()->fetchCol()) {
@ -178,7 +178,7 @@ class TermStorage extends SqlContentEntityStorage implements TermStorageInterfac
$query->condition('t.vid', $vid);
}
$query->condition('t.default_langcode', 1);
$query->addTag('term_access');
$query->addTag('taxonomy_term_access');
$query->orderBy('t.weight');
$query->orderBy('t.name');
if ($ids = $query->execute()->fetchCol()) {
@ -204,7 +204,7 @@ class TermStorage extends SqlContentEntityStorage implements TermStorageInterfac
$query = $this->database->select('taxonomy_term_field_data', 't');
$query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
$result = $query
->addTag('term_access')
->addTag('taxonomy_term_access')
->fields('t')
->fields('h', array('parent'))
->condition('t.vid', $vid)
@ -320,7 +320,7 @@ class TermStorage extends SqlContentEntityStorage implements TermStorageInterfac
$query->orderby('td.weight');
$query->orderby('td.name');
$query->condition('tn.nid', $nids, 'IN');
$query->addTag('term_access');
$query->addTag('taxonomy_term_access');
if (!empty($vocabs)) {
$query->condition('td.vid', $vocabs, 'IN');
}

View file

@ -16,7 +16,7 @@ class TermViewsData extends EntityViewsData {
$data = parent::getViewsData();
$data['taxonomy_term_field_data']['table']['base']['help'] = $this->t('Taxonomy terms are attached to nodes.');
$data['taxonomy_term_field_data']['table']['base']['access query tag'] = 'term_access';
$data['taxonomy_term_field_data']['table']['base']['access query tag'] = 'taxonomy_term_access';
$data['taxonomy_term_field_data']['table']['wizard_id'] = 'taxonomy_term';
$data['taxonomy_term_field_data']['table']['join'] = array(

View file

@ -0,0 +1,119 @@
<?php
namespace Drupal\taxonomy\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests that appropriate query tags are added.
*
* @group taxonomy
*/
class TaxonomyQueryAlterTest extends WebTestBase {
use TaxonomyTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['taxonomy', 'taxonomy_test'];
/**
* Tests that appropriate tags are added when querying the database.
*/
public function testTaxonomyQueryAlter() {
// Create a new vocabulary and add a few terms to it.
$vocabulary = $this->createVocabulary();
$terms = array();
for ($i = 0; $i < 5; $i++) {
$terms[$i] = $this->createTerm($vocabulary);
}
// Set up hierarchy. Term 2 is a child of 1.
$terms[2]->parent = $terms[1]->id();
$terms[2]->save();
$term_storage = \Drupal::entityManager()->getStorage('taxonomy_term');
$this->setupQueryTagTestHooks();
$loaded_term = $term_storage->load($terms[0]->id());
$this->assertEqual($loaded_term->id(), $terms[0]->id(), 'First term was loaded');
$this->assertQueryTagTestResult(1, 0, 'TermStorage::load()');
$this->setupQueryTagTestHooks();
$loaded_terms = $term_storage->loadTree($vocabulary->id());
$this->assertEqual(count($loaded_terms), count($terms), 'All terms were loaded');
$this->assertQueryTagTestResult(1, 1, 'TermStorage::loadTree()');
$this->setupQueryTagTestHooks();
$loaded_terms = $term_storage->loadParents($terms[2]->id());
$this->assertEqual(count($loaded_terms), 1, 'All parent terms were loaded');
$this->assertQueryTagTestResult(2, 1, 'TermStorage::loadParents()');
$this->setupQueryTagTestHooks();
$loaded_terms = $term_storage->loadChildren($terms[1]->id());
$this->assertEqual(count($loaded_terms), 1, 'All child terms were loaded');
$this->assertQueryTagTestResult(2, 1, 'TermStorage::loadChildren()');
$this->setupQueryTagTestHooks();
$query = db_select('taxonomy_term_data', 't');
$query->addField('t', 'tid');
$query->addTag('taxonomy_term_access');
$tids = $query->execute()->fetchCol();
$this->assertEqual(count($tids), count($terms), 'All term IDs were retrieved');
$this->assertQueryTagTestResult(1, 1, 'custom db_select() with taxonomy_term_access tag (preferred)');
$this->setupQueryTagTestHooks();
$query = db_select('taxonomy_term_data', 't');
$query->addField('t', 'tid');
$query->addTag('term_access');
$tids = $query->execute()->fetchCol();
$this->assertEqual(count($tids), count($terms), 'All term IDs were retrieved');
$this->assertQueryTagTestResult(1, 1, 'custom db_select() with term_access tag (deprecated)');
$this->setupQueryTagTestHooks();
$query = \Drupal::entityQuery('taxonomy_term');
$query->addTag('taxonomy_term_access');
$result = $query->execute();
$this->assertEqual(count($result), count($terms), 'All term IDs were retrieved');
$this->assertQueryTagTestResult(1, 1, 'custom EntityFieldQuery with taxonomy_term_access tag (preferred)');
$this->setupQueryTagTestHooks();
$query = \Drupal::entityQuery('taxonomy_term');
$query->addTag('term_access');
$result = $query->execute();
$this->assertEqual(count($result), count($terms), 'All term IDs were retrieved');
$this->assertQueryTagTestResult(1, 1, 'custom EntityFieldQuery with term_access tag (deprecated)');
}
/**
* Sets up the hooks in the test module.
*/
protected function setupQueryTagTestHooks() {
taxonomy_terms_static_reset();
\Drupal::state()->set('taxonomy_test_query_alter', 0);
\Drupal::state()->set('taxonomy_test_query_term_access_alter', 0);
\Drupal::state()->set('taxonomy_test_query_taxonomy_term_access_alter', 0);
}
/**
* Verifies invocation of the hooks in the test module.
*
* @param int $expected_generic_invocations
* The number of times the generic query_alter hook is expected to have
* been invoked.
* @param int $expected_specific_invocations
* The number of times the tag-specific query_alter hooks are expected to
* have been invoked.
* @param string $method
* A string describing the invoked function which generated the query.
*/
protected function assertQueryTagTestResult($expected_generic_invocations, $expected_specific_invocations, $method) {
$this->assertIdentical($expected_generic_invocations, \Drupal::state()->get('taxonomy_test_query_alter'), 'hook_query_alter() invoked when executing ' . $method);
$this->assertIdentical($expected_specific_invocations, \Drupal::state()->get('taxonomy_test_query_term_access_alter'), 'Deprecated hook_query_term_access_alter() invoked when executing ' . $method);
$this->assertIdentical($expected_specific_invocations, \Drupal::state()->get('taxonomy_test_query_taxonomy_term_access_alter'), 'Preferred hook_query_taxonomy_term_access_alter() invoked when executing ' . $method);
}
}

View file

@ -7,8 +7,8 @@
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;

View file

@ -0,0 +1,8 @@
name: 'Taxonomy test'
type: module
description: 'Provides test hook implementations for taxonomy tests'
package: Testing
version: VERSION
core: 8.x
dependencies:
- taxonomy

View file

@ -0,0 +1,38 @@
<?php
/**
* @file
* Provides test hook implementations for taxonomy tests.
*/
use Drupal\Core\Database\Query\AlterableInterface;
/**
* Implements hook_query_alter().
*/
function taxonomy_test_query_alter(AlterableInterface $query) {
$value = \Drupal::state()->get(__FUNCTION__);
if (isset($value)) {
\Drupal::state()->set(__FUNCTION__, ++$value);
}
}
/**
* Implements hook_query_TAG_alter().
*/
function taxonomy_test_query_term_access_alter(AlterableInterface $query) {
$value = \Drupal::state()->get(__FUNCTION__);
if (isset($value)) {
\Drupal::state()->set(__FUNCTION__, ++$value);
}
}
/**
* Implements hook_query_TAG_alter().
*/
function taxonomy_test_query_taxonomy_term_access_alter(AlterableInterface $query) {
$value = \Drupal::state()->get(__FUNCTION__);
if (isset($value)) {
\Drupal::state()->set(__FUNCTION__, ++$value);
}
}

View file

@ -99,6 +99,7 @@ class UserPasswordForm extends FormBase {
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => $this->t('Submit'));
$form['#cache']['contexts'][] = 'url.query_args';
return $form;
}

View file

@ -292,6 +292,9 @@ class UserPasswordResetTest extends PageCacheTagsTestBase {
unset($edit['pass']);
$this->drupalGet('user/password', array('query' => array('name' => $edit['name'])));
$this->assertFieldByName('name', $edit['name'], 'User name found.');
// Ensure the name field value is not cached.
$this->drupalGet('user/password');
$this->assertNoFieldByName('name', $edit['name'], 'User name not found.');
}
/**

View file

@ -2,6 +2,7 @@
namespace Drupal\Tests\Core\Render\Element;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
@ -78,8 +79,12 @@ class MachineNameTest extends UnitTestCase {
$language_manager = $this->prophesize(LanguageManagerInterface::class);
$language_manager->getCurrentLanguage()->willReturn($language);
$csrf_token = $this->prophesize(CsrfTokenGenerator::class);
$csrf_token->get('[^a-z0-9_]+')->willReturn('tis-a-fine-token');
$container = $this->prophesize(ContainerInterface::class);
$container->get('language_manager')->willReturn($language_manager->reveal());
$container->get('csrf_token')->willReturn($csrf_token->reveal());
\Drupal::setContainer($container->reveal());
$element = MachineName::processMachineName($element, $form_state, $complete_form);
@ -93,7 +98,8 @@ class MachineNameTest extends UnitTestCase {
'label',
'field_prefix',
'field_suffix',
'suffix'
'suffix',
'replace_token',
];
$this->assertEmpty(array_diff_key($settings, array_flip($allowed_options)));
foreach ($allowed_options as $key) {