updated core to 8.6.2
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
namespace Drupal\Tests\block\Functional\Views;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\block\Functional\AssertBlockAppearsTrait;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
@@ -360,14 +362,16 @@ class DisplayBlockTest extends ViewTestBase {
|
||||
$this->drupalGet('test-page');
|
||||
|
||||
$id = 'block:block=' . $block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en';
|
||||
$id_token = Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get());
|
||||
$cached_id = 'block:block=' . $cached_block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en';
|
||||
$cached_id_token = Crypt::hmacBase64($cached_id, Settings::getHashSalt() . $this->container->get('private_key')->get());
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
|
||||
$this->assertRaw('<div' . new Attribute(['data-contextual-id' => $id]) . '></div>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $id]));
|
||||
$this->assertRaw('<div' . new Attribute(['data-contextual-id' => $cached_id]) . '></div>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $cached_id]));
|
||||
$this->assertRaw('<div' . new Attribute(['data-contextual-id' => $id, 'data-contextual-token' => $id_token]) . '></div>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $id]));
|
||||
$this->assertRaw('<div' . new Attribute(['data-contextual-id' => $cached_id, 'data-contextual-token' => $cached_id_token]) . '></div>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $cached_id]));
|
||||
|
||||
// Get server-rendered contextual links.
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
|
||||
$post = ['ids[0]' => $id, 'ids[1]' => $cached_id];
|
||||
$post = ['ids[0]' => $id, 'ids[1]' => $cached_id, 'tokens[0]' => $id_token, 'tokens[1]' => $cached_id_token];
|
||||
$url = 'contextual/render?_format=json,destination=test-page';
|
||||
$this->getSession()->getDriver()->getClient()->request('POST', $url, $post);
|
||||
$this->assertResponse(200);
|
||||
|
||||
+1
@@ -16,5 +16,6 @@ class ModerationStateConstraint extends Constraint {
|
||||
|
||||
public $message = 'Invalid state transition from %from to %to';
|
||||
public $invalidStateMessage = 'State %state does not exist on %workflow workflow';
|
||||
public $invalidTransitionAccess = 'You do not have access to transition from %original_state to %new_state';
|
||||
|
||||
}
|
||||
|
||||
+71
-19
@@ -2,10 +2,13 @@
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\content_moderation\StateTransitionValidationInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
@@ -29,6 +32,20 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* The state transition validation service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidationInterface
|
||||
*/
|
||||
protected $stateTransitionValidation;
|
||||
|
||||
/**
|
||||
* Creates a new ModerationStateConstraintValidator instance.
|
||||
*
|
||||
@@ -36,10 +53,16 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidationInterface $state_transition_validation
|
||||
* The state transition validation service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information) {
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information, AccountInterface $current_user, StateTransitionValidationInterface $state_transition_validation) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
$this->currentUser = $current_user;
|
||||
$this->stateTransitionValidation = $state_transition_validation;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +71,9 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('current_user'),
|
||||
$container->get('content_moderation.state_transition_validation')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,32 +101,59 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
return;
|
||||
}
|
||||
|
||||
$new_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value);
|
||||
$original_state = $this->getOriginalOrInitialState($entity);
|
||||
|
||||
// If a new state is being set and there is an existing state, validate
|
||||
// there is a valid transition between them.
|
||||
if (!$original_state->canTransitionTo($new_state->id())) {
|
||||
$this->context->addViolation($constraint->message, [
|
||||
'%from' => $original_state->label(),
|
||||
'%to' => $new_state->label(),
|
||||
]);
|
||||
}
|
||||
else {
|
||||
// If we're sure the transition exists, make sure the user has permission
|
||||
// to use it.
|
||||
if (!$this->stateTransitionValidation->isTransitionValid($workflow, $original_state, $new_state, $this->currentUser)) {
|
||||
$this->context->addViolation($constraint->invalidTransitionAccess, [
|
||||
'%original_state' => $original_state->label(),
|
||||
'%new_state' => $new_state->label(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the original or initial state of the given entity.
|
||||
*
|
||||
* When a state is being validated, the original state is used to validate
|
||||
* that a valid transition exists for target state and the user has access
|
||||
* to the transition between those two states. If the entity has been
|
||||
* moderated before, we can load the original unmodified revision and
|
||||
* translation for this state.
|
||||
*
|
||||
* If the entity is new we need to load the initial state from the workflow.
|
||||
* Even if a value was assigned to the moderation_state field, the initial
|
||||
* state is used to compute an appropriate transition for the purposes of
|
||||
* validation.
|
||||
*
|
||||
* @return \Drupal\workflows\StateInterface
|
||||
* The original or default moderation state.
|
||||
*/
|
||||
protected function getOriginalOrInitialState(ContentEntityInterface $entity) {
|
||||
$state = NULL;
|
||||
$workflow_type = $this->moderationInformation->getWorkflowForEntity($entity)->getTypePlugin();
|
||||
if (!$entity->isNew() && !$this->isFirstTimeModeration($entity)) {
|
||||
$original_entity = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadRevision($entity->getLoadedRevisionId());
|
||||
if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) {
|
||||
$original_entity = $original_entity->getTranslation($entity->language()->getId());
|
||||
}
|
||||
|
||||
// If the state of the original entity doesn't exist on the workflow,
|
||||
// we cannot do any further validation of transitions, because none will
|
||||
// be setup for a state that doesn't exist. Instead allow any state to
|
||||
// take its place.
|
||||
if (!$workflow->getTypePlugin()->hasState($original_entity->moderation_state->value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$new_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value);
|
||||
$original_state = $workflow->getTypePlugin()->getState($original_entity->moderation_state->value);
|
||||
|
||||
if (!$original_state->canTransitionTo($new_state->id())) {
|
||||
$this->context->addViolation($constraint->message, [
|
||||
'%from' => $original_state->label(),
|
||||
'%to' => $new_state->label(),
|
||||
]);
|
||||
if ($workflow_type->hasState($original_entity->moderation_state->value)) {
|
||||
$state = $workflow_type->getState($original_entity->moderation_state->value);
|
||||
}
|
||||
}
|
||||
return $state ?: $workflow_type->getInitialState($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\workflows\StateInterface;
|
||||
use Drupal\workflows\Transition;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
@@ -47,4 +49,12 @@ class StateTransitionValidation implements StateTransitionValidationInterface {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isTransitionValid(WorkflowInterface $workflow, StateInterface $original_state, StateInterface $new_state, AccountInterface $user) {
|
||||
$transition = $workflow->getTypePlugin()->getTransitionFromStateToState($original_state->id(), $new_state->id());
|
||||
return $user->hasPermission('use ' . $workflow->id() . ' transition ' . $transition->id());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\workflows\StateInterface;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
@@ -23,4 +25,21 @@ interface StateTransitionValidationInterface {
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Checks if a transition between two states if valid for the given user.
|
||||
*
|
||||
* @param \Drupal\workflows\WorkflowInterface $workflow
|
||||
* The workflow entity.
|
||||
* @param \Drupal\workflows\StateInterface $original_state
|
||||
* The original workflow state.
|
||||
* @param \Drupal\workflows\StateInterface $new_state
|
||||
* The new workflow state.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The user to validate.
|
||||
*
|
||||
* @return bool
|
||||
* Returns TRUE if transition is valid, otherwise FALSE.
|
||||
*/
|
||||
public function isTransitionValid(WorkflowInterface $workflow, StateInterface $original_state, StateInterface $new_state, AccountInterface $user);
|
||||
|
||||
}
|
||||
|
||||
@@ -158,32 +158,15 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
|
||||
]);
|
||||
$this->drupalLogin($limited_user);
|
||||
|
||||
// Check the user can add content, but can't see the moderation state
|
||||
// select.
|
||||
// Check the user can see the content entity form, but can't see the
|
||||
// moderation state select or save the entity form.
|
||||
$this->drupalGet('node/add/moderated_content');
|
||||
$session_assert->statusCodeEquals(200);
|
||||
$session_assert->fieldNotExists('moderation_state[0][state]');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'moderated content',
|
||||
], 'Save');
|
||||
|
||||
// Manually move the content to archived because the user doesn't have
|
||||
// permission to do this.
|
||||
$node = $this->getNodeByTitle('moderated content');
|
||||
$node->moderation_state->value = 'archived';
|
||||
$node->save();
|
||||
|
||||
// Check the user can see the current state but not the select.
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$session_assert->statusCodeEquals(200);
|
||||
$session_assert->pageTextContains('Archived');
|
||||
$session_assert->fieldNotExists('moderation_state[0][state]');
|
||||
$this->drupalPostForm(NULL, [], 'Save');
|
||||
|
||||
// When saving they should still be on the edit form, and see the validation
|
||||
// error message.
|
||||
$session_assert->pageTextContains('Edit Moderated content moderated content');
|
||||
$session_assert->pageTextContains('Invalid state transition from Archived to Archived');
|
||||
$session_assert->pageTextContains('You do not have access to transition from Draft to Draft');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
|
||||
@@ -15,6 +16,7 @@ use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
|
||||
use ContentModerationTestTrait;
|
||||
use UserCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
@@ -29,6 +31,13 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* An admin user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -40,6 +49,9 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
$this->installSchema('system', ['sequences']);
|
||||
|
||||
$this->adminUser = $this->createUser(array_keys($this->container->get('user.permissions')->getPermissions()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +60,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testValidTransition() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
@@ -76,6 +90,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testInvalidTransition() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
@@ -125,6 +141,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* Test validation with content that has no initial state or an invalid state.
|
||||
*/
|
||||
public function testInvalidStateWithoutExisting() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
// Create content without moderation enabled for the content type.
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
@@ -156,15 +173,24 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
// validating.
|
||||
$workflow->getTypePlugin()->deleteState('deleted_state');
|
||||
$workflow->save();
|
||||
|
||||
// When there is an invalid state, the content will revert to "draft". This
|
||||
// will allow a draft to draft transition.
|
||||
$node->moderation_state->value = 'draft';
|
||||
$violations = $node->validate();
|
||||
$this->assertCount(0, $violations);
|
||||
// This will disallow a draft to archived transition.
|
||||
$node->moderation_state->value = 'archived';
|
||||
$violations = $node->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test state transition validation with multiple languages.
|
||||
*/
|
||||
public function testInvalidStateMultilingual() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
@@ -220,6 +246,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* Tests that content without prior moderation information can be moderated.
|
||||
*/
|
||||
public function testExistingContentWithNoModeration() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
@@ -254,6 +282,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* Tests that content without prior moderation information can be translated.
|
||||
*/
|
||||
public function testExistingMultilingualContentWithNoModeration() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
// Enable French.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
|
||||
@@ -293,4 +323,81 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
$node_fr->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider transitionAccessValidationTestCases
|
||||
*/
|
||||
public function testTransitionAccessValidation($permissions, $target_state, $messages) {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addState('foo', 'Foo');
|
||||
$workflow->getTypePlugin()->addTransition('draft_to_foo', 'Draft to foo', ['draft'], 'foo');
|
||||
$workflow->getTypePlugin()->addTransition('foo_to_foo', 'Foo to foo', ['foo'], 'foo');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
|
||||
$this->setCurrentUser($this->createUser($permissions));
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test content',
|
||||
'moderation_state' => $target_state,
|
||||
]);
|
||||
$this->assertTrue($node->isNew());
|
||||
$violations = $node->validate();
|
||||
$this->assertCount(count($messages), $violations);
|
||||
foreach ($messages as $i => $message) {
|
||||
$this->assertEquals($message, $violations->get($i)->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for ::testTransitionAccessValidation.
|
||||
*/
|
||||
public function transitionAccessValidationTestCases() {
|
||||
return [
|
||||
'Invalid transition, no permissions validated' => [
|
||||
[],
|
||||
'archived',
|
||||
['Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>'],
|
||||
],
|
||||
'Valid transition, missing permission' => [
|
||||
[],
|
||||
'published',
|
||||
['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
|
||||
],
|
||||
'Valid transition, granted published permission' => [
|
||||
['use editorial transition publish'],
|
||||
'published',
|
||||
[],
|
||||
],
|
||||
'Valid transition, granted draft permission' => [
|
||||
['use editorial transition create_new_draft'],
|
||||
'draft',
|
||||
[],
|
||||
],
|
||||
'Valid transition, incorrect permission granted' => [
|
||||
['use editorial transition create_new_draft'],
|
||||
'published',
|
||||
['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
|
||||
],
|
||||
// Test with an additional state and set of transitions, since the
|
||||
// "published" transition can start from either "draft" or "published", it
|
||||
// does not capture bugs that fail to correctly distinguish the initial
|
||||
// workflow state from the set state of a new entity.
|
||||
'Valid transition, granted foo permission' => [
|
||||
['use editorial transition draft_to_foo'],
|
||||
'foo',
|
||||
[],
|
||||
],
|
||||
'Valid transition, incorrect foo permission granted' => [
|
||||
['use editorial transition foo_to_foo'],
|
||||
'foo',
|
||||
['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Foo</em>'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -191,13 +191,19 @@ function _contextual_links_to_id($contextual_links) {
|
||||
/**
|
||||
* Unserializes the result of _contextual_links_to_id().
|
||||
*
|
||||
* @see _contextual_links_to_id
|
||||
* Note that $id is user input. Before calling this method the ID should be
|
||||
* checked against the token stored in the 'data-contextual-token' attribute
|
||||
* which is passed via the 'tokens' request parameter to
|
||||
* \Drupal\contextual\ContextualController::render().
|
||||
*
|
||||
* @param string $id
|
||||
* A serialized representation of a #contextual_links property value array.
|
||||
*
|
||||
* @return array
|
||||
* The value for a #contextual_links property.
|
||||
*
|
||||
* @see _contextual_links_to_id()
|
||||
* @see \Drupal\contextual\ContextualController::render()
|
||||
*/
|
||||
function _contextual_id_to_links($id) {
|
||||
$contextual_links = [];
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Post update functions for Contextual Links.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ensure new page loads use the updated JS and get the updated markup.
|
||||
*/
|
||||
function contextual_post_update_fixed_endpoint_and_markup() {
|
||||
// Empty update to trigger a change to css_js_query_string and invalidate
|
||||
// cached markup.
|
||||
}
|
||||
@@ -168,12 +168,16 @@
|
||||
// Collect the IDs for all contextual links placeholders.
|
||||
const ids = [];
|
||||
$placeholders.each(function() {
|
||||
ids.push($(this).attr('data-contextual-id'));
|
||||
ids.push({
|
||||
id: $(this).attr('data-contextual-id'),
|
||||
token: $(this).attr('data-contextual-token'),
|
||||
});
|
||||
});
|
||||
|
||||
// Update all contextual links placeholders whose HTML is cached.
|
||||
const uncachedIDs = _.filter(ids, contextualID => {
|
||||
const html = storage.getItem(`Drupal.contextual.${contextualID}`);
|
||||
const uncachedIDs = [];
|
||||
const uncachedTokens = [];
|
||||
ids.forEach(contextualID => {
|
||||
const html = storage.getItem(`Drupal.contextual.${contextualID.id}`);
|
||||
if (html && html.length) {
|
||||
// Initialize after the current execution cycle, to make the AJAX
|
||||
// request for retrieving the uncached contextual links as soon as
|
||||
@@ -182,13 +186,14 @@
|
||||
// Drupal.contextual.collection.
|
||||
window.setTimeout(() => {
|
||||
initContextual(
|
||||
$context.find(`[data-contextual-id="${contextualID}"]`),
|
||||
$context.find(`[data-contextual-id="${contextualID.id}"]`),
|
||||
html,
|
||||
);
|
||||
});
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
uncachedIDs.push(contextualID.id);
|
||||
uncachedTokens.push(contextualID.token);
|
||||
});
|
||||
|
||||
// Perform an AJAX request to let the server render the contextual links
|
||||
@@ -197,7 +202,7 @@
|
||||
$.ajax({
|
||||
url: Drupal.url('contextual/render'),
|
||||
type: 'POST',
|
||||
data: { 'ids[]': uncachedIDs },
|
||||
data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens },
|
||||
dataType: 'json',
|
||||
success(results) {
|
||||
_.each(results, (html, contextualID) => {
|
||||
|
||||
@@ -95,25 +95,31 @@
|
||||
|
||||
var ids = [];
|
||||
$placeholders.each(function () {
|
||||
ids.push($(this).attr('data-contextual-id'));
|
||||
ids.push({
|
||||
id: $(this).attr('data-contextual-id'),
|
||||
token: $(this).attr('data-contextual-token')
|
||||
});
|
||||
});
|
||||
|
||||
var uncachedIDs = _.filter(ids, function (contextualID) {
|
||||
var html = storage.getItem('Drupal.contextual.' + contextualID);
|
||||
var uncachedIDs = [];
|
||||
var uncachedTokens = [];
|
||||
ids.forEach(function (contextualID) {
|
||||
var html = storage.getItem('Drupal.contextual.' + contextualID.id);
|
||||
if (html && html.length) {
|
||||
window.setTimeout(function () {
|
||||
initContextual($context.find('[data-contextual-id="' + contextualID + '"]'), html);
|
||||
initContextual($context.find('[data-contextual-id="' + contextualID.id + '"]'), html);
|
||||
});
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
uncachedIDs.push(contextualID.id);
|
||||
uncachedTokens.push(contextualID.token);
|
||||
});
|
||||
|
||||
if (uncachedIDs.length > 0) {
|
||||
$.ajax({
|
||||
url: Drupal.url('contextual/render'),
|
||||
type: 'POST',
|
||||
data: { 'ids[]': uncachedIDs },
|
||||
data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens },
|
||||
dataType: 'json',
|
||||
success: function success(results) {
|
||||
_.each(results, function (html, contextualID) {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace Drupal\contextual;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -63,8 +65,16 @@ class ContextualController implements ContainerInjectionInterface {
|
||||
throw new BadRequestHttpException(t('No contextual ids specified.'));
|
||||
}
|
||||
|
||||
$tokens = $request->request->get('tokens');
|
||||
if (!isset($tokens)) {
|
||||
throw new BadRequestHttpException(t('No contextual ID tokens specified.'));
|
||||
}
|
||||
|
||||
$rendered = [];
|
||||
foreach ($ids as $id) {
|
||||
foreach ($ids as $key => $id) {
|
||||
if (!isset($tokens[$key]) || !Crypt::hashEquals($tokens[$key], Crypt::hmacBase64($id, Settings::getHashSalt() . \Drupal::service('private_key')->get()))) {
|
||||
throw new BadRequestHttpException('Invalid contextual ID specified.');
|
||||
}
|
||||
$element = [
|
||||
'#type' => 'contextual_links',
|
||||
'#contextual_links' => _contextual_id_to_links($id),
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Drupal\contextual\Element;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Core\Render\Element\RenderElement;
|
||||
use Drupal\Component\Render\FormattableMarkup;
|
||||
@@ -43,7 +45,12 @@ class ContextualLinksPlaceholder extends RenderElement {
|
||||
* @see _contextual_links_to_id()
|
||||
*/
|
||||
public static function preRenderPlaceholder(array $element) {
|
||||
$element['#markup'] = new FormattableMarkup('<div@attributes></div>', ['@attributes' => new Attribute(['data-contextual-id' => $element['#id']])]);
|
||||
$token = Crypt::hmacBase64($element['#id'], Settings::getHashSalt() . \Drupal::service('private_key')->get());
|
||||
$attribute = new Attribute([
|
||||
'data-contextual-id' => $element['#id'],
|
||||
'data-contextual-token' => $token,
|
||||
]);
|
||||
$element['#markup'] = new FormattableMarkup('<div@attributes></div>', ['@attributes' => $attribute]);
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
namespace Drupal\Tests\contextual\Functional;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
@@ -140,17 +141,76 @@ class ContextualDynamicContextTest extends BrowserTestBase {
|
||||
$this->assertRaw('<li class="menu-testcontextual-hidden-manage-edit"><a href="' . base_path() . 'menu-test-contextual/1/edit" class="use-ajax" data-dialog-type="modal" data-is-something>Edit menu - contextual</a></li>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the contextual placeholder content is protected by a token.
|
||||
*/
|
||||
public function testTokenProtection() {
|
||||
$this->drupalLogin($this->editorUser);
|
||||
|
||||
// Create a node that will have a contextual link.
|
||||
$node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
|
||||
|
||||
// Now, on the front page, all article nodes should have contextual links
|
||||
// placeholders, as should the view that contains them.
|
||||
$id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en';
|
||||
|
||||
// Editor user: can access contextual links and can edit articles.
|
||||
$this->drupalGet('node');
|
||||
$this->assertContextualLinkPlaceHolder($id);
|
||||
|
||||
$http_client = $this->getHttpClient();
|
||||
$url = Url::fromRoute('contextual.render', [], [
|
||||
'query' => [
|
||||
'_format' => 'json',
|
||||
'destination' => 'node',
|
||||
],
|
||||
])->setAbsolute()->toString();
|
||||
|
||||
$response = $http_client->request('POST', $url, [
|
||||
'cookies' => $this->getSessionCookies(),
|
||||
'form_params' => ['ids' => [$id], 'tokens' => []],
|
||||
'http_errors' => FALSE,
|
||||
]);
|
||||
$this->assertEquals('400', $response->getStatusCode());
|
||||
$this->assertContains('No contextual ID tokens specified.', (string) $response->getBody());
|
||||
|
||||
$response = $http_client->request('POST', $url, [
|
||||
'cookies' => $this->getSessionCookies(),
|
||||
'form_params' => ['ids' => [$id], 'tokens' => ['wrong_token']],
|
||||
'http_errors' => FALSE,
|
||||
]);
|
||||
$this->assertEquals('400', $response->getStatusCode());
|
||||
$this->assertContains('Invalid contextual ID specified.', (string) $response->getBody());
|
||||
|
||||
$response = $http_client->request('POST', $url, [
|
||||
'cookies' => $this->getSessionCookies(),
|
||||
'form_params' => ['ids' => [$id], 'tokens' => ['wrong_key' => $this->createContextualIdToken($id)]],
|
||||
'http_errors' => FALSE,
|
||||
]);
|
||||
$this->assertEquals('400', $response->getStatusCode());
|
||||
$this->assertContains('Invalid contextual ID specified.', (string) $response->getBody());
|
||||
|
||||
$response = $http_client->request('POST', $url, [
|
||||
'cookies' => $this->getSessionCookies(),
|
||||
'form_params' => ['ids' => [$id], 'tokens' => [$this->createContextualIdToken($id)]],
|
||||
'http_errors' => FALSE,
|
||||
]);
|
||||
$this->assertEquals('200', $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a contextual link placeholder with the given id exists.
|
||||
*
|
||||
* @param string $id
|
||||
* A contextual link id.
|
||||
*
|
||||
* @return bool
|
||||
* The result of the assertion.
|
||||
*/
|
||||
protected function assertContextualLinkPlaceHolder($id) {
|
||||
return $this->assertRaw('<div' . new Attribute(['data-contextual-id' => $id]) . '></div>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $id]));
|
||||
$this->assertSession()->elementAttributeContains(
|
||||
'css',
|
||||
'div[data-contextual-id="' . $id . '"]',
|
||||
'data-contextual-token',
|
||||
$this->createContextualIdToken($id)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,12 +218,9 @@ class ContextualDynamicContextTest extends BrowserTestBase {
|
||||
*
|
||||
* @param string $id
|
||||
* A contextual link id.
|
||||
*
|
||||
* @return bool
|
||||
* The result of the assertion.
|
||||
*/
|
||||
protected function assertNoContextualLinkPlaceHolder($id) {
|
||||
return $this->assertNoRaw('<div' . new Attribute(['data-contextual-id' => $id]) . '></div>', format_string('Contextual link placeholder with id @id does not exist.', ['@id' => $id]));
|
||||
$this->assertSession()->elementNotExists('css', 'div[data-contextual-id="' . $id . '"]');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,6 +235,7 @@ class ContextualDynamicContextTest extends BrowserTestBase {
|
||||
* The response object.
|
||||
*/
|
||||
protected function renderContextualLinks($ids, $current_path) {
|
||||
$tokens = array_map([$this, 'createContextualIdToken'], $ids);
|
||||
$http_client = $this->getHttpClient();
|
||||
$url = Url::fromRoute('contextual.render', [], [
|
||||
'query' => [
|
||||
@@ -188,9 +246,22 @@ class ContextualDynamicContextTest extends BrowserTestBase {
|
||||
|
||||
return $http_client->request('POST', $this->buildUrl($url), [
|
||||
'cookies' => $this->getSessionCookies(),
|
||||
'form_params' => ['ids' => $ids],
|
||||
'form_params' => ['ids' => $ids, 'tokens' => $tokens],
|
||||
'http_errors' => FALSE,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a contextual ID token.
|
||||
*
|
||||
* @param string $id
|
||||
* The contextual ID to create a token for.
|
||||
*
|
||||
* @return string
|
||||
* The contextual ID token.
|
||||
*/
|
||||
protected function createContextualIdToken($id) {
|
||||
return Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\node\Tests\Views;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* Tests views contextual links on nodes.
|
||||
*
|
||||
* @group node
|
||||
*/
|
||||
class NodeContextualLinksTest extends NodeTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['contextual'];
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = ['test_contextual_links'];
|
||||
|
||||
/**
|
||||
* Tests contextual links.
|
||||
*/
|
||||
public function testNodeContextualLinks() {
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->drupalCreateNode(['promote' => 1]);
|
||||
$this->drupalGet('node');
|
||||
|
||||
$user = $this->drupalCreateUser(['administer nodes', 'access contextual links']);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$response = $this->renderContextualLinks(['node:node=1:'], 'node');
|
||||
$this->assertResponse(200);
|
||||
$json = Json::decode($response);
|
||||
$this->setRawContent($json['node:node=1:']);
|
||||
|
||||
// @todo Add these back when the functionality for making Views displays
|
||||
// appear in contextual links is working again.
|
||||
// $this->assertLinkByHref('node/1/contextual-links', 0, 'The contextual link to the view was found.');
|
||||
// $this->assertLink('Test contextual link', 0, 'The contextual link to the view was found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server-rendered contextual links for the given contextual link ids.
|
||||
*
|
||||
* Copied from \Drupal\contextual\Tests\ContextualDynamicContextTest::renderContextualLinks().
|
||||
*
|
||||
* @param array $ids
|
||||
* An array of contextual link ids.
|
||||
* @param string $current_path
|
||||
* The Drupal path for the page for which the contextual links are rendered.
|
||||
*
|
||||
* @return string
|
||||
* The response body.
|
||||
*/
|
||||
protected function renderContextualLinks($ids, $current_path) {
|
||||
// Build POST values.
|
||||
$post = [];
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$post['ids[' . $i . ']'] = $ids[$i];
|
||||
}
|
||||
|
||||
// Serialize POST values.
|
||||
foreach ($post as $key => $value) {
|
||||
// Encode according to application/x-www-form-urlencoded
|
||||
// Both names and values needs to be urlencoded, according to
|
||||
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
|
||||
$post[$key] = urlencode($key) . '=' . urlencode($value);
|
||||
}
|
||||
$post = implode('&', $post);
|
||||
|
||||
// Perform HTTP request.
|
||||
return $this->curlExec([
|
||||
CURLOPT_URL => \Drupal::url('contextual.render', [], ['absolute' => TRUE, 'query' => ['destination' => $current_path]]),
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $post,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the node page works if Contextual Links is disabled.
|
||||
*
|
||||
* All views have Contextual links enabled by default, even with the
|
||||
* Contextual links module disabled. This tests if no calls are done to the
|
||||
* Contextual links module by views when it is disabled.
|
||||
*
|
||||
* @see https://www.drupal.org/node/2379811
|
||||
*/
|
||||
public function testPageWithDisabledContextualModule() {
|
||||
\Drupal::service('module_installer')->uninstall(['contextual']);
|
||||
\Drupal::service('module_installer')->install(['views_ui']);
|
||||
|
||||
// Ensure that contextual links don't get called for admin users.
|
||||
$admin_user = User::load(1);
|
||||
$admin_user->setPassword('new_password');
|
||||
$admin_user->pass_raw = 'new_password';
|
||||
$admin_user->save();
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->drupalCreateNode(['promote' => 1]);
|
||||
|
||||
$this->drupalLogin($admin_user);
|
||||
$this->drupalGet('node');
|
||||
}
|
||||
|
||||
}
|
||||
+1
-21
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\node\Tests;
|
||||
namespace Drupal\Tests\node\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
@@ -158,17 +158,6 @@ class NodeRevisionsTest extends NodeTestBase {
|
||||
// Confirm that this is the default revision.
|
||||
$this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.');
|
||||
|
||||
// Confirm that the "Edit" and "Delete" contextual links appear for the
|
||||
// default revision.
|
||||
$ids = ['node:node=' . $node->id() . ':changed=' . $node->getChangedTime()];
|
||||
$json = $this->renderContextualLinks($ids, 'node/' . $node->id());
|
||||
$this->verbose($json[$ids[0]]);
|
||||
|
||||
$expected = '<li class="entitynodeedit-form"><a href="' . base_path() . 'node/' . $node->id() . '/edit">Edit</a></li>';
|
||||
$this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Edit" contextual link is shown for the default revision.');
|
||||
$expected = '<li class="entitynodedelete-form"><a href="' . base_path() . 'node/' . $node->id() . '/delete">Delete</a></li>';
|
||||
$this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Delete" contextual link is shown for the default revision.');
|
||||
|
||||
// Confirm that revisions revert properly.
|
||||
$this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert", [], t('Revert'));
|
||||
$this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [
|
||||
@@ -188,15 +177,6 @@ class NodeRevisionsTest extends NodeTestBase {
|
||||
$node = node_revision_load($node->getRevisionId());
|
||||
$this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.');
|
||||
|
||||
// Confirm that "Edit" and "Delete" contextual links don't appear for
|
||||
// non-default revision.
|
||||
$ids = ['node_revision::node=' . $node->id() . '&node_revision=' . $node->getRevisionId() . ':'];
|
||||
$json = $this->renderContextualLinks($ids, 'node/' . $node->id() . '/revisions/' . $node->getRevisionId() . '/view');
|
||||
$this->verbose($json[$ids[0]]);
|
||||
|
||||
$this->assertFalse(strstr($json[$ids[0]], '<li class="entitynodeedit-form">'), 'The "Edit" contextual link is not shown for a non-default revision.');
|
||||
$this->assertFalse(strstr($json[$ids[0]], '<li class="entitynodedelete-form">'), 'The "Delete" contextual link is not shown for a non-default revision.');
|
||||
|
||||
// Confirm revisions delete properly.
|
||||
$this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete", [], t('Delete'));
|
||||
$this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', [
|
||||
+14
-11
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\node\Tests;
|
||||
namespace Drupal\Tests\node\Functional;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\system\Tests\Menu\AssertBreadcrumbTrait;
|
||||
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* Ensures that node type functions work correctly.
|
||||
@@ -15,6 +16,7 @@ use Drupal\system\Tests\Menu\AssertBreadcrumbTrait;
|
||||
class NodeTypeTest extends NodeTestBase {
|
||||
|
||||
use AssertBreadcrumbTrait;
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
@@ -87,6 +89,7 @@ class NodeTypeTest extends NodeTestBase {
|
||||
* Tests editing a node type using the UI.
|
||||
*/
|
||||
public function testNodeTypeEditing() {
|
||||
$assert = $this->assertSession();
|
||||
$this->drupalPlaceBlock('system_breadcrumb_block');
|
||||
$web_user = $this->drupalCreateUser(['bypass node access', 'administer content types', 'administer node fields']);
|
||||
$this->drupalLogin($web_user);
|
||||
@@ -96,8 +99,8 @@ class NodeTypeTest extends NodeTestBase {
|
||||
|
||||
// Verify that title and body fields are displayed.
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->assertRaw('Title', 'Title field was found.');
|
||||
$this->assertRaw('Body', 'Body field was found.');
|
||||
$assert->pageTextContains('Title');
|
||||
$assert->pageTextContains('Body');
|
||||
|
||||
// Rename the title field.
|
||||
$edit = [
|
||||
@@ -106,8 +109,8 @@ class NodeTypeTest extends NodeTestBase {
|
||||
$this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->assertRaw('Foo', 'New title label was displayed.');
|
||||
$this->assertNoRaw('Title', 'Old title label was not displayed.');
|
||||
$assert->pageTextContains('Foo');
|
||||
$assert->pageTextNotContains('Title');
|
||||
|
||||
// Change the name and the description.
|
||||
$edit = [
|
||||
@@ -117,11 +120,11 @@ class NodeTypeTest extends NodeTestBase {
|
||||
$this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
|
||||
|
||||
$this->drupalGet('node/add');
|
||||
$this->assertRaw('Bar', 'New name was displayed.');
|
||||
$this->assertRaw('Lorem ipsum', 'New description was displayed.');
|
||||
$assert->pageTextContains('Bar');
|
||||
$assert->pageTextContains('Lorem ipsum');
|
||||
$this->clickLink('Bar');
|
||||
$this->assertRaw('Foo', 'Title field was found.');
|
||||
$this->assertRaw('Body', 'Body field was found.');
|
||||
$assert->pageTextContains('Foo');
|
||||
$assert->pageTextContains('Body');
|
||||
|
||||
// Change the name through the API
|
||||
/** @var \Drupal\node\NodeTypeInterface $node_type */
|
||||
@@ -146,7 +149,7 @@ class NodeTypeTest extends NodeTestBase {
|
||||
]);
|
||||
// Check that the body field doesn't exist.
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->assertNoRaw('Body', 'Body field was not found.');
|
||||
$assert->pageTextNotContains('Body');
|
||||
}
|
||||
|
||||
/**
|
||||
+11
-6
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\node\Tests;
|
||||
namespace Drupal\Tests\node\Functional;
|
||||
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
@@ -12,6 +12,7 @@ use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* Tests the node entity preview functionality.
|
||||
@@ -22,6 +23,9 @@ class PagePreviewTest extends NodeTestBase {
|
||||
|
||||
use EntityReferenceTestTrait;
|
||||
use CommentTestTrait;
|
||||
use TestFileCreationTrait {
|
||||
getTestFiles as drupalGetTestFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the comment, node and taxonomy modules to test on the preview.
|
||||
@@ -177,7 +181,8 @@ class PagePreviewTest extends NodeTestBase {
|
||||
$this->drupalPostForm(NULL, ['field_image[0][alt]' => 'Picture of llamas'], t('Preview'));
|
||||
|
||||
// Check that the preview is displaying the title, body and term.
|
||||
$this->assertTitle(t('@title | Drupal', ['@title' => $edit[$title_key]]), 'Basic page title is preview.');
|
||||
$expected_title = $edit[$title_key] . ' | Drupal';
|
||||
$this->assertSession()->titleEquals($expected_title);
|
||||
$this->assertEscaped($edit[$title_key], 'Title displayed and escaped.');
|
||||
$this->assertText($edit[$body_key], 'Body displayed.');
|
||||
$this->assertText($edit[$term_key], 'Term displayed.');
|
||||
@@ -210,13 +215,13 @@ class PagePreviewTest extends NodeTestBase {
|
||||
$this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.');
|
||||
$this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.');
|
||||
$this->assertFieldByName('field_image[0][alt]', 'Picture of llamas');
|
||||
$this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')], NULL, [], [], 'node-page-form');
|
||||
$this->getSession()->getPage()->pressButton('Add another item');
|
||||
$this->assertFieldByName('field_test_multi[0][value]');
|
||||
$this->assertFieldByName('field_test_multi[1][value]');
|
||||
|
||||
// Return to page preview to check everything is as expected.
|
||||
$this->drupalPostForm(NULL, [], t('Preview'));
|
||||
$this->assertTitle(t('@title | Drupal', ['@title' => $edit[$title_key]]), 'Basic page title is preview.');
|
||||
$this->assertSession()->titleEquals($expected_title);
|
||||
$this->assertEscaped($edit[$title_key], 'Title displayed and escaped.');
|
||||
$this->assertText($edit[$body_key], 'Body displayed.');
|
||||
$this->assertText($edit[$term_key], 'Term displayed.');
|
||||
@@ -353,8 +358,8 @@ class PagePreviewTest extends NodeTestBase {
|
||||
$this->assertText('Basic page ' . $title . ' has been created.');
|
||||
$node = $this->drupalGetNodeByTitle($title);
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')]);
|
||||
$this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')]);
|
||||
$this->getSession()->getPage()->pressButton('Add another item');
|
||||
$this->getSession()->getPage()->pressButton('Add another item');
|
||||
$edit = [
|
||||
'field_test_multi[1][value]' => $example_text_2,
|
||||
'field_test_multi[2][value]' => $example_text_3,
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\node\Functional\Views;
|
||||
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* Tests views contextual links on nodes.
|
||||
*
|
||||
* @group node
|
||||
*/
|
||||
class NodeContextualLinksTest extends NodeTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['contextual'];
|
||||
|
||||
/**
|
||||
* Tests if the node page works if Contextual Links is disabled.
|
||||
*
|
||||
* All views have Contextual links enabled by default, even with the
|
||||
* Contextual links module disabled. This tests if no calls are done to the
|
||||
* Contextual links module by views when it is disabled.
|
||||
*
|
||||
* @see https://www.drupal.org/node/2379811
|
||||
*/
|
||||
public function testPageWithDisabledContextualModule() {
|
||||
\Drupal::service('module_installer')->uninstall(['contextual']);
|
||||
\Drupal::service('module_installer')->install(['views_ui']);
|
||||
|
||||
// Ensure that contextual links don't get called for admin users.
|
||||
$admin_user = User::load(1);
|
||||
$admin_user->setPassword('new_password');
|
||||
$admin_user->passRaw = 'new_password';
|
||||
$admin_user->save();
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->drupalCreateNode(['promote' => 1]);
|
||||
|
||||
$this->drupalLogin($admin_user);
|
||||
$this->drupalGet('node');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\node\FunctionalJavascript;
|
||||
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
|
||||
|
||||
/**
|
||||
* Create a node with revisions and test contextual links.
|
||||
*
|
||||
* @group node
|
||||
*/
|
||||
class ContextualLinksTest extends WebDriverTestBase {
|
||||
|
||||
use ContextualLinkClickTrait;
|
||||
|
||||
/**
|
||||
* An array of node revisions.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $nodes;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'contextual'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalCreateContentType([
|
||||
'type' => 'page',
|
||||
'name' => 'Basic page',
|
||||
'display_submitted' => FALSE,
|
||||
]);
|
||||
|
||||
// Create initial node.
|
||||
$node = $this->drupalCreateNode();
|
||||
|
||||
$nodes = [];
|
||||
|
||||
// Get original node.
|
||||
$nodes[] = clone $node;
|
||||
|
||||
// Create two revisions.
|
||||
$revision_count = 2;
|
||||
for ($i = 0; $i < $revision_count; $i++) {
|
||||
|
||||
// Create revision with a random title and body and update variables.
|
||||
$node->title = $this->randomMachineName();
|
||||
$node->body = [
|
||||
'value' => $this->randomMachineName(32),
|
||||
'format' => filter_default_format(),
|
||||
];
|
||||
$node->setNewRevision();
|
||||
|
||||
$node->save();
|
||||
|
||||
// Make sure we get revision information.
|
||||
$node = Node::load($node->id());
|
||||
$nodes[] = clone $node;
|
||||
}
|
||||
|
||||
$this->nodes = $nodes;
|
||||
|
||||
$this->drupalLogin($this->createUser(
|
||||
[
|
||||
'view page revisions',
|
||||
'revert page revisions',
|
||||
'delete page revisions',
|
||||
'edit any page content',
|
||||
'delete any page content',
|
||||
'access contextual links',
|
||||
'administer content types',
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the contextual links on revisions.
|
||||
*/
|
||||
public function testRevisionContextualLinks() {
|
||||
// Confirm that the "Edit" and "Delete" contextual links appear for the
|
||||
// default revision.
|
||||
$this->drupalGet('node/' . $this->nodes[0]->id());
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->waitFor(10, function () use ($page) {
|
||||
return $page->find('css', "main .contextual");
|
||||
});
|
||||
|
||||
$this->toggleContextualTriggerVisibility('main');
|
||||
$page->find('css', 'main .contextual button')->press();
|
||||
$links = $page->findAll('css', "main .contextual-links li a");
|
||||
|
||||
$this->assertEquals('Edit', $links[0]->getText());
|
||||
$this->assertEquals('Delete', $links[1]->getText());
|
||||
|
||||
// Confirm that "Edit" and "Delete" contextual links don't appear for
|
||||
// non-default revision.
|
||||
$this->drupalGet("node/" . $this->nodes[0]->id() . "/revisions/" . $this->nodes[1]->getRevisionId() . "/view");
|
||||
$this->assertSession()->pageTextContains($this->nodes[1]->getTitle());
|
||||
$page->waitFor(10, function () use ($page) {
|
||||
return $page->find('css', "main .contextual");
|
||||
});
|
||||
|
||||
$this->toggleContextualTriggerVisibility('main');
|
||||
$contextual_button = $page->find('css', 'main .contextual button');
|
||||
$this->assertEmpty(0, $contextual_button);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Drupal\Tests\path\Functional;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Add, edit, delete, and change alias and verify its consistency in the
|
||||
@@ -24,7 +25,7 @@ class PathAliasTest extends PathTestBase {
|
||||
parent::setUp();
|
||||
|
||||
// Create test user and log in.
|
||||
$web_user = $this->drupalCreateUser(['create page content', 'edit own page content', 'administer url aliases', 'create url aliases']);
|
||||
$web_user = $this->drupalCreateUser(['create page content', 'edit own page content', 'administer url aliases', 'create url aliases', 'access content overview']);
|
||||
$this->drupalLogin($web_user);
|
||||
}
|
||||
|
||||
@@ -327,6 +328,34 @@ class PathAliasTest extends PathTestBase {
|
||||
$node5->delete();
|
||||
$path_alias = \Drupal::service('path.alias_storage')->lookupPathAlias('/node/' . $node5->id(), $node5->language()->getId());
|
||||
$this->assertFalse($path_alias, 'Alias was successfully deleted when the referenced node was deleted.');
|
||||
|
||||
// Create sixth test node.
|
||||
$node6 = $this->drupalCreateNode();
|
||||
|
||||
// Create an invalid alias with two leading slashes and verify that the
|
||||
// extra slash is removed when the link is generated. This ensures that URL
|
||||
// aliases cannot be used to inject external URLs.
|
||||
// @todo The user interface should either display an error message or
|
||||
// automatically trim these invalid aliases, rather than allowing them to
|
||||
// be silently created, at which point the functional aspects of this
|
||||
// test will need to be moved elsewhere and switch to using a
|
||||
// programmatically-created alias instead.
|
||||
$alias = $this->randomMachineName(8);
|
||||
$edit = ['path[0][alias]' => '//' . $alias];
|
||||
$this->drupalPostForm($node6->toUrl('edit-form'), $edit, t('Save'));
|
||||
$this->drupalGet(Url::fromRoute('system.admin_content'));
|
||||
// This checks the link href before clicking it, rather than using
|
||||
// \Drupal\Tests\BrowserTestBase::assertSession()->addressEquals() after
|
||||
// clicking it, because the test browser does not always preserve the
|
||||
// correct number of slashes in the URL when it visits internal links;
|
||||
// using \Drupal\Tests\BrowserTestBase::assertSession()->addressEquals()
|
||||
// would actually make the test pass unconditionally on the testbot (or
|
||||
// anywhere else where Drupal is installed in a subdirectory).
|
||||
$link_xpath = $this->xpath('//a[normalize-space(text())=:label]', [':label' => $node6->getTitle()]);
|
||||
$link_href = $link_xpath[0]->getAttribute('href');
|
||||
$this->assertEquals($link_href, base_path() . $alias);
|
||||
$this->clickLink($node6->getTitle());
|
||||
$this->assertResponse(404);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -320,6 +320,13 @@ class RouterTest extends WebTestBase {
|
||||
$this->drupalGet($url);
|
||||
$this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
|
||||
$this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test');
|
||||
|
||||
// Ensure that external URLs in destination query params are not redirected
|
||||
// to.
|
||||
$url = $request->getUriForPath('/////////////////////////////////////////////////router_test/test1') . '?qs=test&destination=http://www.example.com%5c@drupal8alt.test';
|
||||
$this->drupalGet($url);
|
||||
$this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
|
||||
$this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user