diff --git a/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php index 4f380125bb..b856c9afa0 100644 --- a/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -131,7 +131,12 @@ public function load($conditions) { foreach ($conditions as $field => $value) { if ($field == 'source' || $field == 'alias') { // Use LIKE for case-insensitive matching. - $select->condition($field, $this->connection->escapeLike($value), 'LIKE'); + if (is_array($value)) { + $select->condition($field, $value, 'IN'); + } + else { + $select->condition($field, $this->connection->escapeLike($value), 'LIKE'); + } } else { $select->condition($field, $value); @@ -160,7 +165,7 @@ public function delete($conditions) { foreach ($conditions as $field => $value) { if ($field == 'source' || $field == 'alias') { // Use LIKE for case-insensitive matching. - $query->condition($field, $this->connection->escapeLike($value), 'LIKE'); + $query->condition($field, $value, 'LIKE'); } else { $query->condition($field, $value); diff --git a/core/modules/path/path.module b/core/modules/path/path.module index a182214dab..328c434c33 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -5,6 +5,7 @@ * Enables users to rename URLs. */ +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Form\FormStateInterface; @@ -76,3 +77,17 @@ function path_entity_base_field_info(EntityTypeInterface $entity_type) { return $fields; } } + +/** + * Implements hook_entity_revision_delete(). + */ +function path_entity_revision_delete(EntityInterface $entity) { + $entity_type_id = $entity->getEntityTypeId(); + $source = str_replace('{' . $entity_type_id . '}', $entity->id(), $entity->getEntityType()->getLinkTemplate('revision')); + $source = str_replace('{' . $entity_type_id . '_revision}', '%', $source); + $revisionable_conditions = [ + 'source' => $source, + 'langcode' => $entity->language()->getId(), + ]; + \Drupal::service('path.alias_storage')->delete($revisionable_conditions); +} diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php index ee03361d96..b8cacf2152 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php @@ -32,6 +32,15 @@ public function delete() { 'langcode' => $entity->language()->getId(), ]; \Drupal::service('path.alias_storage')->delete($conditions); + + $entity_type_id = $entity->getEntityTypeId(); + $source = str_replace('{' . $entity_type_id . '}', $entity->id(), $entity->getEntityType()->getLinkTemplate('revision')); + $source = str_replace('{' . $entity_type_id . '_revision}', '%', $source); + $revisionable_conditions = [ + 'source' => $source, + 'langcode' => $entity->language()->getId(), + ]; + \Drupal::service('path.alias_storage')->delete($revisionable_conditions); } } diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index 4d8da7a8ad..76dddc20e8 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -51,23 +51,33 @@ public function preSave() { * {@inheritdoc} */ public function postSave($update) { - if (!$update) { - if ($this->alias) { - $entity = $this->getEntity(); - if ($path = \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode())) { - $this->pid = $path['pid']; - } + /** @var \Drupal\Core\Path\AliasStorageInterface $alias_storage */ + $alias_storage = \Drupal::service('path.alias_storage'); + + // Only process a non-empty alias. + $entity = $this->getEntity(); + $canonical = '/' . $entity->toUrl()->getInternalPath(); + if ($entity->getEntityType()->isRevisionable() && !$entity->isDefaultRevision()) { + $source = '/' . $entity->toUrl('revision')->getInternalPath(); + } + else { + $source = $canonical; + } + + if ($this->alias && (!$update || $source != $this->source)) { + $default_alias = $alias_storage->load(['source' => $canonical]); + if ($default_alias['alias'] != $this->alias && $path = $alias_storage->save($source, $this->alias, $this->getLangcode())) { + $this->pid = $path['pid']; } } else { // Delete old alias if user erased it. - if ($this->pid && !$this->alias) { - \Drupal::service('path.alias_storage')->delete(['pid' => $this->pid]); + if (!$this->alias) { + $alias_storage->delete(['source' => $source]); } // Only save a non-empty alias. - elseif ($this->alias) { - $entity = $this->getEntity(); - \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode(), $this->pid); + elseif ($source) { + $alias_storage->save($source, $this->alias, $this->getLangcode(), $this->pid); } } } diff --git a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php index b31af4694b..cd14910317 100644 --- a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php +++ b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php @@ -28,7 +28,15 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $entity = $items->getEntity(); $path = []; if (!$entity->isNew()) { - $conditions = ['source' => '/' . $entity->urlInfo()->getInternalPath()]; + if ($entity->getEntityType()->isRevisionable() && !$entity->isDefaultRevision()) { + $conditions = ['source' => [ + '/' . $entity->toUrl('revision')->getInternalPath(), + '/' . $entity->urlInfo()->getInternalPath(), + ]]; + } + else { + $conditions = ['source' => '/' . $entity->urlInfo()->getInternalPath()]; + } if ($items->getLangcode() != LanguageInterface::LANGCODE_NOT_SPECIFIED) { $conditions['langcode'] = $items->getLangcode(); } diff --git a/core/modules/path/tests/src/Functional/PathContentModerationTest.php b/core/modules/path/tests/src/Functional/PathContentModerationTest.php new file mode 100644 index 0000000000..32fef3d3f0 --- /dev/null +++ b/core/modules/path/tests/src/Functional/PathContentModerationTest.php @@ -0,0 +1,152 @@ + 'moderated', 'type' => 'moderated']); + $node_type->save(); + + // Set the content type as moderated. + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated'); + $workflow->save(); + + $this->drupalLogin($this->rootUser); + } + + public function testNodePathAlias() { + // Create some moderated content with a path alias. + $this->drupalGet('node/add/moderated'); + $this->assertSession()->fieldValueEquals('path[0][alias]', ''); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'moderated content', + 'path[0][alias]' => '/moderated-content', + ], t('Save and Publish')); + $node = $this->getNodeByTitle('moderated content'); + + $this->drupalGet('/moderated-content'); + $this->assertSession()->pageTextContains('moderated content'); + + // Add a forward revision with the same alias and check the alias is + // from the default revision. + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertSession()->fieldValueEquals('path[0][alias]', '/moderated-content'); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'forward revision', + 'path[0][alias]' => '/moderated-content', + ], t('Save and Create New Draft')); + + // Check the alias still shows the default content. + $this->drupalGet('/moderated-content'); + $this->assertSession()->pageTextContains('moderated content'); + + // Add a third revision, updating alias, and check the alias still comes + // from the default. + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertSession()->fieldValueEquals('path[0][alias]', '/moderated-content'); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'revision three', + 'path[0][alias]' => '/revision-three', + ], t('Save and Create New Draft')); + + // Check both aliases return what we'd expect. + $this->drupalGet('/revision-three'); + $this->assertSession()->pageTextContains('revision three'); + $this->drupalGet('/moderated-content'); + $this->assertSession()->pageTextContains('moderated content'); + + // Add another forward revision with no alias. + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertSession()->fieldValueEquals('path[0][alias]', '/revision-three'); + $this->drupalPostForm('node/' . $node->id() . '/edit', [ + 'title[0][value]' => 'revision four', + 'path[0][alias]' => '', + ], t('Save and Create New Draft')); + + // The first two aliases should still work, and the revision with no alias + // should work on the revision path. + $this->drupalGet('/node/1/revisions/4/view'); + $this->assertSession()->pageTextContains('revision four'); + $this->drupalGet('/revision-three'); + $this->assertSession()->pageTextContains('revision three'); + $this->drupalGet('/moderated-content'); + $this->assertSession()->pageTextContains('moderated content'); + + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertSession()->fieldValueEquals('path[0][alias]', '/moderated-content'); + $this->drupalPostForm('node/' . $node->id() . '/edit', [ + 'title[0][value]' => 'revision five', + 'path[0][alias]' => '/revision-five', + ], t('Save and Publish')); + $this->drupalGet('/revision-five'); + $this->assertSession()->pageTextContains('revision five'); + $this->drupalGet('/node/1/revisions/4/view'); + $this->assertSession()->pageTextContains('revision four'); + $this->drupalGet('/revision-three'); + $this->assertSession()->pageTextContains('revision three'); + $this->drupalGet('/moderated-content'); + $this->assertSession()->statusCodeEquals('404'); + + $this->drupalPostForm('node/' . $node->id() . '/delete', [], t('Delete')); + $aliases = \Drupal::database()->select(AliasStorage::TABLE)->fields(AliasStorage::TABLE)->execute()->fetchAll(); + $this->assertEquals([], $aliases); + + // Create some moderated content with a path alias. + $this->drupalGet('node/add/moderated'); + $this->assertSession()->fieldValueEquals('path[0][alias]', ''); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'moderated content 2', + 'path[0][alias]' => '/moderated-content-2', + ], t('Save and Publish')); + $node = $this->getNodeByTitle('moderated content 2'); + + $this->drupalGet('/moderated-content-2'); + $this->assertSession()->pageTextContains('moderated content 2'); + + // Add a forward revision with a new alias. + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertSession()->fieldValueEquals('path[0][alias]', '/moderated-content-2'); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'forward revision', + 'path[0][alias]' => '/forward-revision', + ], t('Save and Create New Draft')); + + // Check both aliases return what we'd expect. + $this->drupalGet('/forward-revision'); + $this->assertSession()->pageTextContains('forward revision'); + $this->drupalGet('/moderated-content-2'); + $this->assertSession()->pageTextContains('moderated content 2'); + + // Delete the forward revision. + $this->drupalPostForm('node/2/revisions/7/delete', [], t('Delete')); + $this->drupalGet('/forward-revision'); + $this->assertSession()->statusCodeEquals('404'); + $this->drupalGet('/moderated-content-2'); + $this->assertSession()->pageTextContains('moderated content 2'); + } +}