Issue #3007669 by amateescu, Berdir, catch: Add publishing status to path aliases

merge-requests/55/head
webchick 2019-10-10 10:27:16 -07:00
parent 791a988503
commit e0c5f41ff4
20 changed files with 723 additions and 45 deletions

View File

@ -171,17 +171,30 @@ class AliasStorage implements AliasStorageInterface {
$storage->delete($storage->loadMultiple($result));
}
/**
* Returns a SELECT query for the path_alias base table.
*
* @return \Drupal\Core\Database\Query\SelectInterface
* A Select query object.
*/
protected function getBaseQuery() {
$query = $this->connection->select(static::TABLE, 'base_table');
$query->condition('base_table.status', 1);
return $query;
}
/**
* {@inheritdoc}
*/
public function preloadPathAlias($preloaded, $langcode) {
$select = $this->connection->select(static::TABLE)
->fields(static::TABLE, ['path', 'alias']);
$select = $this->getBaseQuery()
->fields('base_table', ['path', 'alias']);
if (!empty($preloaded)) {
$conditions = new Condition('OR');
foreach ($preloaded as $preloaded_item) {
$conditions->condition('path', $this->connection->escapeLike($preloaded_item), 'LIKE');
$conditions->condition('base_table.path', $this->connection->escapeLike($preloaded_item), 'LIKE');
}
$select->condition($conditions);
}
@ -191,7 +204,7 @@ class AliasStorage implements AliasStorageInterface {
// We order by ID ASC so that fetchAllKeyed() returns the most recently
// created alias for each source. Subsequent queries using fetchField() must
// use ID DESC to have the same effect.
$select->orderBy('id', 'ASC');
$select->orderBy('base_table.id', 'ASC');
return $select->execute()->fetchAllKeyed();
}
@ -201,13 +214,13 @@ class AliasStorage implements AliasStorageInterface {
*/
public function lookupPathAlias($path, $langcode) {
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->connection->select(static::TABLE)
->fields(static::TABLE, ['alias'])
->condition('path', $this->connection->escapeLike($path), 'LIKE');
$select = $this->getBaseQuery()
->fields('base_table', ['alias'])
->condition('base_table.path', $this->connection->escapeLike($path), 'LIKE');
$this->addLanguageFallback($select, $langcode);
$select->orderBy('id', 'DESC');
$select->orderBy('base_table.id', 'DESC');
return $select->execute()->fetchField();
}
@ -217,13 +230,13 @@ class AliasStorage implements AliasStorageInterface {
*/
public function lookupPathSource($alias, $langcode) {
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->connection->select(static::TABLE)
->fields(static::TABLE, ['path'])
->condition('alias', $this->connection->escapeLike($alias), 'LIKE');
$select = $this->getBaseQuery()
->fields('base_table', ['path'])
->condition('base_table.alias', $this->connection->escapeLike($alias), 'LIKE');
$this->addLanguageFallback($select, $langcode);
$select->orderBy('id', 'DESC');
$select->orderBy('base_table.id', 'DESC');
return $select->execute()->fetchField();
}
@ -246,12 +259,12 @@ class AliasStorage implements AliasStorageInterface {
array_pop($langcode_list);
}
elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$query->orderBy('langcode', 'DESC');
$query->orderBy('base_table.langcode', 'DESC');
}
else {
$query->orderBy('langcode', 'ASC');
$query->orderBy('base_table.langcode', 'ASC');
}
$query->condition('langcode', $langcode_list, 'IN');
$query->condition('base_table.langcode', $langcode_list, 'IN');
}
/**
@ -304,11 +317,11 @@ class AliasStorage implements AliasStorageInterface {
* {@inheritdoc}
*/
public function pathHasMatchingAlias($initial_substring) {
$query = $this->connection->select(static::TABLE);
$query = $this->getBaseQuery();
$query->addExpression(1);
return (bool) $query
->condition('path', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
->condition('base_table.path', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
->range(0, 1)
->execute()
->fetchField();

View File

@ -3,6 +3,7 @@
namespace Drupal\Core\Path\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@ -34,6 +35,7 @@ use Drupal\Core\Path\PathAliasInterface;
* "revision" = "revision_id",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "published" = "status",
* },
* admin_permission = "administer url aliases",
* list_cache_tags = { "route_match" },
@ -41,6 +43,8 @@ use Drupal\Core\Path\PathAliasInterface;
*/
class PathAlias extends ContentEntityBase implements PathAliasInterface {
use EntityPublishedTrait;
/**
* {@inheritdoc}
*/
@ -61,6 +65,10 @@ class PathAlias extends ContentEntityBase implements PathAliasInterface {
$fields['langcode']->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED);
// Add the published field.
$fields += static::publishedBaseFieldDefinitions($entity_type);
$fields['status']->setTranslatable(FALSE);
return $fields;
}

View File

@ -3,11 +3,12 @@
namespace Drupal\Core\Path;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
/**
* Provides an interface defining a path_alias entity.
*/
interface PathAliasInterface extends ContentEntityInterface {
interface PathAliasInterface extends ContentEntityInterface, EntityPublishedInterface {
/**
* Gets the source path of the alias.

View File

@ -17,8 +17,8 @@ class PathAliasStorageSchema extends SqlContentEntityStorageSchema {
$schema = parent::getEntitySchema($entity_type, $reset);
$schema[$this->storage->getBaseTable()]['indexes'] += [
'path_alias__alias_langcode_id' => ['alias', 'langcode', 'id'],
'path_alias__path_langcode_id' => ['path', 'langcode', 'id'],
'path_alias__alias_langcode_id_status' => ['alias', 'langcode', 'id', 'status'],
'path_alias__path_langcode_id_status' => ['path', 'langcode', 'id', 'status'],
];
return $schema;

View File

@ -0,0 +1,20 @@
<?php
namespace Drupal\Core\Routing;
/**
* Extends the router provider interface to provide caching support.
*/
interface CacheableRouteProviderInterface extends RouteProviderInterface {
/**
* Adds a cache key part to be used in the cache ID of the route collection.
*
* @param string $cache_key_provider
* The provider of the cache key part.
* @param string $cache_key_part
* A string to be used as a cache key part.
*/
public function addExtraCacheKeyPart($cache_key_provider, $cache_key_part);
}

View File

@ -21,7 +21,7 @@ use Drupal\Core\Database\Connection;
/**
* A Route Provider front-end for all Drupal-stored routes.
*/
class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProviderInterface, EventSubscriberInterface {
class RouteProvider implements CacheableRouteProviderInterface, PreloadableRouteProviderInterface, PagedRouteProviderInterface, EventSubscriberInterface {
/**
* The database connection from which to read route information.
@ -98,6 +98,13 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
*/
const ROUTE_LOAD_CID_PREFIX = 'route_provider.route_load:';
/**
* An array of cache key parts to be used for the route match cache.
*
* @var string[]
*/
protected $extraCacheKeyParts = [];
/**
* Constructs a new PathMatcher.
*
@ -442,6 +449,13 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
return $this->connection->query("SELECT COUNT(*) FROM {" . $this->connection->escapeTable($this->tableName) . "}")->fetchField();
}
/**
* {@inheritdoc}
*/
public function addExtraCacheKeyPart($cache_key_provider, $cache_key_part) {
$this->extraCacheKeyParts[$cache_key_provider] = $cache_key_part;
}
/**
* Returns the cache ID for the route collection cache.
*
@ -455,8 +469,17 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
// Include the current language code in the cache identifier as
// the language information can be elsewhere than in the path, for example
// based on the domain.
$language_part = $this->getCurrentLanguageCacheIdPart();
return 'route:' . $language_part . ':' . $request->getPathInfo() . ':' . $request->getQueryString();
$this->addExtraCacheKeyPart('language', $this->getCurrentLanguageCacheIdPart());
// Sort the cache key parts by their provider in order to have predictable
// cache keys.
ksort($this->extraCacheKeyParts);
$key_parts = [];
foreach ($this->extraCacheKeyParts as $provider => $key_part) {
$key_parts[] = '[' . $provider . ']=' . $key_part;
}
return 'route:' . implode(':', $key_parts) . ':' . $request->getPathInfo() . ':' . $request->getQueryString();
}
/**

View File

@ -9,6 +9,7 @@ use Drupal\Core\Url;
* JSON:API integration test for the "PathAlias" content entity type.
*
* @group jsonapi
* @group path
*/
class PathAliasTest extends ResourceTestBase {
@ -86,6 +87,7 @@ class PathAliasTest extends ResourceTestBase {
'alias' => '/frontpage1',
'path' => '/<front>',
'langcode' => 'en',
'status' => TRUE,
'drupal_internal__id' => 1,
'drupal_internal__revision_id' => 1,
],

View File

@ -63,28 +63,42 @@ class PathItem extends FieldItemBase {
* {@inheritdoc}
*/
public function postSave($update) {
$path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias');
$entity = $this->getEntity();
// If specified, rely on the langcode property for the language, so that the
// existing language of an alias can be kept. That could for example be
// unspecified even if the field/entity has a specific langcode.
$alias_langcode = ($this->langcode && $this->pid) ? $this->langcode : $this->getLangcode();
if (!$update) {
if ($this->alias) {
$entity = $this->getEntity();
if ($path = \Drupal::service('path.alias_storage')->save('/' . $entity->toUrl()->getInternalPath(), $this->alias, $alias_langcode)) {
$this->pid = $path['pid'];
// If we have an alias, we need to create or update a path alias entity.
if ($this->alias) {
if (!$update || !$this->pid) {
$path_alias = $path_alias_storage->create([
'path' => '/' . $entity->toUrl()->getInternalPath(),
'alias' => $this->alias,
'langcode' => $alias_langcode,
]);
$path_alias->save();
$this->pid = $path_alias->id();
}
elseif ($this->pid) {
$path_alias = $path_alias_storage->load($this->pid);
if ($this->alias != $path_alias->getAlias()) {
$path_alias->setAlias($this->alias);
$path_alias->save();
}
}
}
else {
// Delete old alias if user erased it.
if ($this->pid && !$this->alias) {
\Drupal::service('path.alias_storage')->delete(['pid' => $this->pid]);
elseif ($this->pid && !$this->alias) {
// Otherwise, delete the old alias if the user erased it.
$path_alias = $path_alias_storage->load($this->pid);
if ($entity->isDefaultRevision()) {
$path_alias_storage->delete([$path_alias]);
}
// Only save a non-empty alias.
elseif ($this->alias) {
$entity = $this->getEntity();
\Drupal::service('path.alias_storage')->save('/' . $entity->toUrl()->getInternalPath(), $this->alias, $alias_langcode, $this->pid);
else {
$path_alias_storage->deleteRevision($path_alias->getRevisionID());
}
}
}

View File

@ -2395,6 +2395,7 @@ function system_update_8803() {
'revision' => 'revision_id',
'langcode' => 'langcode',
'uuid' => 'uuid',
'published' => 'status',
],
]);
@ -2423,6 +2424,11 @@ function system_update_8803() {
->setInternal(TRUE)
->setRevisionable(TRUE);
$field_storage_definitions['status'] = BaseFieldDefinition::create('boolean')
->setLabel(new TranslatableMarkup('Published'))
->setRevisionable(TRUE)
->setDefaultValue(TRUE);
$field_storage_definitions['path'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('System path'))
->setDescription(new TranslatableMarkup('The path that this alias belongs to.'))
@ -2472,9 +2478,9 @@ function system_update_8804(&$sandbox = NULL) {
$uuid = \Drupal::service('uuid');
$base_table_insert = $database->insert('path_alias');
$base_table_insert->fields(['id', 'revision_id', 'uuid', 'path', 'alias', 'langcode']);
$base_table_insert->fields(['id', 'revision_id', 'uuid', 'path', 'alias', 'langcode', 'status']);
$revision_table_insert = $database->insert('path_alias_revision');
$revision_table_insert->fields(['id', 'revision_id', 'path', 'alias', 'langcode', 'revision_default']);
$revision_table_insert->fields(['id', 'revision_id', 'path', 'alias', 'langcode', 'status', 'revision_default']);
foreach ($url_aliases as $url_alias) {
$values = [
'id' => $url_alias->pid,
@ -2483,6 +2489,7 @@ function system_update_8804(&$sandbox = NULL) {
'path' => $url_alias->source,
'alias' => $url_alias->alias,
'langcode' => $url_alias->langcode,
'status' => 1,
];
$base_table_insert->values($values);

View File

@ -38,6 +38,11 @@ class PathAliasToEntityUpdateTest extends UpdatePathTestBase {
$query->addField('url_alias', 'source', 'path');
$query->addField('url_alias', 'alias');
$query->addField('url_alias', 'langcode');
// Path aliases did not have a 'status' value before the conversion to
// entities, but we're adding it here to ensure that the field was installed
// and populated correctly.
$query->addExpression('1', 'status');
$original_records = $query->execute()->fetchAllAssoc('id');
// drupal-8.filled.standard.php.gz contains one URL alias and
@ -90,12 +95,12 @@ class PathAliasToEntityUpdateTest extends UpdatePathTestBase {
// Check that correct data was written in both the base and the revision
// tables.
$base_table_records = $database->select('path_alias')
->fields('path_alias', ['id', 'path', 'alias', 'langcode'])
->fields('path_alias', ['id', 'path', 'alias', 'langcode', 'status'])
->execute()->fetchAllAssoc('id');
$this->assertEquals($original_records, $base_table_records);
$revision_table_records = $database->select('path_alias_revision')
->fields('path_alias_revision', ['id', 'path', 'alias', 'langcode'])
->fields('path_alias_revision', ['id', 'path', 'alias', 'langcode', 'status'])
->execute()->fetchAllAssoc('id');
$this->assertEquals($original_records, $revision_table_records);
}

View File

@ -0,0 +1,59 @@
<?php
namespace Drupal\workspaces;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Path\AliasStorage as CoreAliasStorage;
/**
* Provides workspace-specific path alias lookup queries.
*/
class AliasStorage extends CoreAliasStorage {
/**
* The workspace manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* AliasStorage constructor.
*
* @param \Drupal\Core\Database\Connection $connection
* A database connection for reading and writing path aliases.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager service.
*/
public function __construct(Connection $connection, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) {
parent::__construct($connection, $module_handler, $entity_type_manager);
$this->workspaceManager = $workspace_manager;
}
/**
* {@inheritdoc}
*/
protected function getBaseQuery() {
// Don't alter any queries if we're not in a workspace context.
if (!$this->workspaceManager->hasActiveWorkspace()) {
return parent::getBaseQuery();
}
$active_workspace = $this->workspaceManager->getActiveWorkspace();
$query = $this->connection->select('path_alias', 'base_table_2');
$wa_join = $query->leftJoin('workspace_association', NULL, "%alias.target_entity_type_id = 'path_alias' AND %alias.target_entity_id = base_table_2.id AND %alias.workspace = :active_workspace_id", [
':active_workspace_id' => $active_workspace->id(),
]);
$query->innerJoin('path_alias_revision', 'base_table', "%alias.revision_id = COALESCE($wa_join.target_entity_revision_id, base_table_2.revision_id)");
return $query;
}
}

View File

@ -107,6 +107,11 @@ class EntityTypeInfo implements ContainerInjectionInterface {
if (isset($definitions['entity_reference'])) {
$definitions['entity_reference']['constraints']['EntityReferenceSupportedNewEntities'] = [];
}
// Allow path aliases to be changed in workspace-specific pending revisions.
if (isset($definitions['path'])) {
unset($definitions['path']['constraints']['PathAlias']);
}
}
/**

View File

@ -0,0 +1,110 @@
<?php
namespace Drupal\workspaces\EventSubscriber;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Routing\CacheableRouteProviderInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Provides a event subscriber for setting workspace-specific cache keys.
*/
class WorkspaceRequestSubscriber implements EventSubscriberInterface {
/**
* The alias manager that caches alias lookups based on the request.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* The route provider to load routes by name.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The workspace manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a new WorkspaceRequestSubscriber instance.
*
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* The alias manager.
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* The current path.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
*/
public function __construct(AliasManagerInterface $alias_manager, CurrentPathStack $current_path, RouteProviderInterface $route_provider, WorkspaceManagerInterface $workspace_manager) {
$this->aliasManager = $alias_manager;
$this->currentPath = $current_path;
$this->routeProvider = $route_provider;
$this->workspaceManager = $workspace_manager;
}
/**
* Sets the cache key on the alias manager cache decorator.
*
* KernelEvents::CONTROLLER is used in order to be executed after routing.
*
* @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
* The Event to process.
*/
public function onKernelController(FilterControllerEvent $event) {
// Set the cache key on the alias manager cache decorator.
if ($event->isMasterRequest() && $this->workspaceManager->hasActiveWorkspace()) {
$cache_key = $this->workspaceManager->getActiveWorkspace()->id() . ':' . rtrim($this->currentPath->getPath($event->getRequest()), '/');
$this->aliasManager->setCacheKey($cache_key);
}
}
/**
* Adds the active workspace as a cache key part to the route provider.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* An event object.
*/
public function onKernelRequest(GetResponseEvent $event) {
if ($this->workspaceManager->hasActiveWorkspace() && $this->routeProvider instanceof CacheableRouteProviderInterface) {
$this->routeProvider->addExtraCacheKeyPart('workspace', $this->workspaceManager->getActiveWorkspace()->id());
}
}
/**
* {@inheritDoc}
*/
public static function getSubscribedEvents() {
// Use a priority of 190 in order to run after the generic core subscriber.
// @see \Drupal\Core\EventSubscriber\PathSubscriber::getSubscribedEvents()
$events[KernelEvents::CONTROLLER][] = ['onKernelController', 190];
// Use a priority of 33 in order to run before Symfony's router listener.
// @see \Symfony\Component\HttpKernel\EventListener\RouterListener::getSubscribedEvents()
$events[KernelEvents::REQUEST][] = ['onKernelRequest', 33];
return $events;
}
}

View File

@ -262,6 +262,10 @@ class WorkspaceManager implements WorkspaceManagerInterface {
return 'entity.memory_cache:' . $entity_type_id;
}, array_keys($this->getSupportedEntityTypes()));
$this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate);
// Clear the static cache for path aliases. We can't inject the path alias
// manager service because it would create a circular dependency.
\Drupal::service('path.alias_manager')->cacheClear();
}
/**

View File

@ -4,6 +4,7 @@ namespace Drupal\workspaces;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Defines a service provider for the Workspaces module.
@ -18,6 +19,11 @@ class WorkspacesServiceProvider extends ServiceProviderBase {
$renderer_config = $container->getParameter('renderer.config');
$renderer_config['required_cache_contexts'][] = 'workspace';
$container->setParameter('renderer.config', $renderer_config);
// Replace the class of the 'path.alias_storage' service.
$container->getDefinition('path.alias_storage')
->setClass(AliasStorage::class)
->addArgument(new Reference('workspaces.manager'));
}
}

View File

@ -0,0 +1,309 @@
<?php
namespace Drupal\Tests\workspaces\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests path aliases with workspaces.
*
* @group path
* @group workspaces
*/
class PathWorkspacesTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'content_translation', 'node', 'path', 'workspaces'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
ConfigurableLanguage::createFromLangcode('ro')->save();
$this->rebuildContainer();
// Create a content type.
$this->drupalCreateContentType([
'name' => 'article',
'type' => 'article',
]);
$this->drupalLogin($this->rootUser);
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, 'Save settings');
// Enable translation for article node.
$edit = [
'entity_types[node]' => 1,
'settings[node][article][translatable]' => 1,
'settings[node][article][fields][path]' => 1,
'settings[node][article][fields][body]' => 1,
'settings[node][article][settings][language][language_alterable]' => 1,
];
$this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
\Drupal::entityTypeManager()->clearCachedDefinitions();
$this->setupWorkspaceSwitcherBlock();
}
/**
* Tests path aliases with workspaces.
*/
public function testPathAliases() {
// Create a published node in Live, without an alias.
$node = $this->drupalCreateNode([
'type' => 'article',
'status' => TRUE,
]);
// Switch to Stage and create an alias for the node.
$stage = Workspace::load('stage');
$this->switchToWorkspace($stage);
$edit = [
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
// Check that the node can be accessed in Stage with the given alias.
$path = $edit['path[0][alias]'];
$this->assertAccessiblePaths([$path]);
// Check that the 'preload-paths' cache includes the active workspace ID in
// the cache key.
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:stage:/node/1'));
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Check that the alias can not be accessed in Live.
$this->switchToLive();
$this->assertNotAccessiblePaths([$path]);
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Publish the workspace and check that the alias can be accessed in Live.
$stage->publish();
$this->assertAccessiblePaths([$path]);
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
}
/**
* Tests path aliases with workspaces and user switching.
*/
public function testPathAliasesUserSwitch() {
// Create a published node in Live, without an alias.
$node = $this->drupalCreateNode([
'type' => 'article',
'status' => TRUE,
]);
// Switch to Stage and create an alias for the node.
$stage = Workspace::load('stage');
$this->switchToWorkspace($stage);
$edit = [
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
// Check that the node can be accessed in Stage with the given alias.
$path = $edit['path[0][alias]'];
$this->assertAccessiblePaths([$path]);
// Check that the 'preload-paths' cache includes the active workspace ID in
// the cache key.
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:stage:/node/1'));
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Check that the alias can not be accessed in Live, by logging out without
// an explicit switch.
$this->drupalLogout();
$this->assertNotAccessiblePaths([$path]);
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Publish the workspace and check that the alias can be accessed in Live.
$this->drupalLogin($this->rootUser);
$stage->publish();
$this->drupalLogout();
$this->assertAccessiblePaths([$path]);
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
}
/**
* Tests path aliases with workspaces for translatable nodes.
*/
public function testPathAliasesWithTranslation() {
$stage = Workspace::load('stage');
// Create one node with a random alias.
$default_node = $this->drupalCreateNode([
'type' => 'article',
'langcode' => 'en',
'status' => TRUE,
'path' => '/' . $this->randomMachineName(),
]);
// Add published translation with another alias.
$this->drupalGet('node/' . $default_node->id());
$this->drupalGet('node/' . $default_node->id() . '/translations');
$this->clickLink('Add');
$edit_translation = [
'body[0][value]' => $this->randomMachineName(),
'status[value]' => TRUE,
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalPostForm(NULL, $edit_translation, 'Save (this translation)');
// Confirm that the alias works.
$this->drupalGet('ro' . $edit_translation['path[0][alias]']);
$this->assertSession()->pageTextContains($edit_translation['body[0][value]']);
$default_path = $default_node->path->alias;
$translation_path = 'ro' . $edit_translation['path[0][alias]'];
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path, $translation_path]);
// Create a workspace-specific revision for the translation with a new path
// alias.
$edit_new_translation_draft_with_alias = [
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_alias, 'Save (this translation)');
$stage_translation_path = 'ro' . $edit_new_translation_draft_with_alias['path[0][alias]'];
// The new alias of the translation should be available in Stage, but not
// available in Live.
$this->assertAccessiblePaths([$default_path, $stage_translation_path]);
// Check that the previous (Live) path alias no longer works.
$this->assertNotAccessiblePaths([$translation_path]);
// Switch out of Stage and check that the initial path aliases still work.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Switch back to Stage.
$this->switchToWorkspace($stage);
// Create new workspace-specific revision for translation without changing
// the path alias.
$edit_new_translation_draft = [
'body[0][value]' => $this->randomMachineName(),
];
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft, t('Save (this translation)'));
// Confirm that the new draft revision was created.
$this->assertSession()->pageTextContains($edit_new_translation_draft['body[0][value]']);
// Switch out of Stage and check that the initial path aliases still work.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Switch back to Stage.
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path, $stage_translation_path]);
$this->assertNotAccessiblePaths([$translation_path]);
// Create a new workspace-specific revision for translation with path alias
// from the original language's default revision.
$edit_new_translation_draft_with_defaults_alias = [
'path[0][alias]' => $default_node->path->alias,
];
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_defaults_alias, 'Save (this translation)');
// Switch out of Stage and check that the initial path aliases still work.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Check that only one path alias (the original one) is available in Stage.
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path]);
$this->assertNotAccessiblePaths([$translation_path, $stage_translation_path]);
// Create new workspace-specific revision for translation with a deleted
// (empty) path alias.
$edit_new_translation_draft_empty_alias = [
'body[0][value]' => $this->randomMachineName(),
'path[0][alias]' => '',
];
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_empty_alias, 'Save (this translation)');
// Check that only one path alias (the original one) is available now.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path]);
$this->assertNotAccessiblePaths([$translation_path, $stage_translation_path]);
// Create a new workspace-specific revision for the translation with a new
// path alias.
$edit_new_translation = [
'body[0][value]' => $this->randomMachineName(),
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation, 'Save (this translation)');
// Confirm that the new revision was created.
$this->assertSession()->pageTextContains($edit_new_translation['body[0][value]']);
$this->assertSession()->addressEquals('ro' . $edit_new_translation['path[0][alias]']);
// Check that only the new path alias of the translation can be accessed.
$new_stage_translation_path = 'ro' . $edit_new_translation['path[0][alias]'];
$this->assertAccessiblePaths([$default_path, $new_stage_translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Switch out of Stage and check that none of the workspace-specific path
// aliases can be accessed.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path, $new_stage_translation_path]);
// Publish Stage and check that its path alias for the translation can be
// accessed.
$stage->publish();
$this->assertAccessiblePaths([$default_path, $new_stage_translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
}
/**
* Helper callback to verify paths are responding with status 200.
*
* @param string[] $paths
* An array of paths to check for.
*/
protected function assertAccessiblePaths(array $paths) {
foreach ($paths as $path) {
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
}
}
/**
* Helper callback to verify paths are responding with status 404.
*
* @param string[] $paths
* An array of paths to check for.
*/
protected function assertNotAccessiblePaths(array $paths) {
foreach ($paths as $path) {
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(404);
}
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Drupal\Tests\workspaces\Unit;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Routing\CacheableRouteProviderInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber;
use Drupal\workspaces\WorkspaceInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
* @coversDefaultClass \Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber
*
* @group workspace
*/
class WorkspaceRequestSubscriberTest extends UnitTestCase {
/**
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->aliasManager = $this->prophesize(AliasManagerInterface::class)->reveal();
$this->currentPath = $this->prophesize(CurrentPathStack::class)->reveal();
$this->workspaceManager = $this->prophesize(WorkspaceManagerInterface::class);
$active_workspace = $this->prophesize(WorkspaceInterface::class);
$active_workspace->id()->willReturn('test');
$this->workspaceManager->getActiveWorkspace()->willReturn($active_workspace->reveal());
$this->workspaceManager->hasActiveWorkspace()->willReturn(TRUE);
}
/**
* @covers ::onKernelRequest
*/
public function testOnKernelRequestWithCacheableRouteProvider() {
$route_provider = $this->prophesize(CacheableRouteProviderInterface::class);
$route_provider->addExtraCacheKeyPart('workspace', 'test')->shouldBeCalled();
// Check that WorkspaceRequestSubscriber::onKernelRequest() calls
// addExtraCacheKeyPart() on a route provider that implements
// CacheableRouteProviderInterface.
$workspace_request_subscriber = new WorkspaceRequestSubscriber($this->aliasManager, $this->currentPath, $route_provider->reveal(), $this->workspaceManager->reveal());
$event = $this->prophesize(GetResponseEvent::class)->reveal();
$this->assertNull($workspace_request_subscriber->onKernelRequest($event));
}
/**
* @covers ::onKernelRequest
*/
public function testOnKernelRequestWithoutCacheableRouteProvider() {
$route_provider = $this->prophesize(RouteProviderInterface::class);
// Check that WorkspaceRequestSubscriber::onKernelRequest() doesn't call
// addExtraCacheKeyPart() on a route provider that does not implement
// CacheableRouteProviderInterface.
$workspace_request_subscriber = new WorkspaceRequestSubscriber($this->aliasManager, $this->currentPath, $route_provider->reveal(), $this->workspaceManager->reveal());
$event = $this->prophesize(GetResponseEvent::class)->reveal();
$this->assertNull($workspace_request_subscriber->onKernelRequest($event));
}
}

View File

@ -35,6 +35,11 @@ services:
arguments: ['@entity.definition_update_manager', '@entity.last_installed_schema.repository', '@workspaces.manager']
tags:
- { name: 'event_subscriber' }
workspaces.workspace_subscriber:
class: Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber
arguments: ['@path.alias_manager', '@path.current', '@router.route_provider', '@workspaces.manager']
tags:
- { name: event_subscriber }
cache_context.workspace:
class: Drupal\workspaces\WorkspaceCacheContext

View File

@ -83,6 +83,11 @@ abstract class PathAliasResourceTestBase extends EntityResourceTestBase {
'value' => '/frontpage1',
],
],
'status' => [
[
'value' => TRUE,
],
],
'uuid' => [
[
'value' => $this->entity->uuid(),

View File

@ -546,7 +546,7 @@ class RouteProviderTest extends KernelTestBase {
$request = Request::create($path, 'GET');
$provider->getRouteCollectionForRequest($request);
$cache = $this->cache->get('route:en:/path/add/one:');
$cache = $this->cache->get('route:[language]=en:/path/add/one:');
$this->assertEqual('/path/add/one', $cache->data['path']);
$this->assertEqual([], $cache->data['query']);
$this->assertEqual(3, count($cache->data['routes']));
@ -556,7 +556,7 @@ class RouteProviderTest extends KernelTestBase {
$request = Request::create($path, 'GET');
$provider->getRouteCollectionForRequest($request);
$cache = $this->cache->get('route:en:/path/add/one:foo=bar');
$cache = $this->cache->get('route:[language]=en:/path/add/one:foo=bar');
$this->assertEqual('/path/add/one', $cache->data['path']);
$this->assertEqual(['foo' => 'bar'], $cache->data['query']);
$this->assertEqual(3, count($cache->data['routes']));
@ -566,7 +566,7 @@ class RouteProviderTest extends KernelTestBase {
$request = Request::create($path, 'GET');
$provider->getRouteCollectionForRequest($request);
$cache = $this->cache->get('route:en:/path/1/one:');
$cache = $this->cache->get('route:[language]=en:/path/1/one:');
$this->assertEqual('/path/1/one', $cache->data['path']);
$this->assertEqual([], $cache->data['query']);
$this->assertEqual(2, count($cache->data['routes']));
@ -583,7 +583,7 @@ class RouteProviderTest extends KernelTestBase {
$request = Request::create($path, 'GET');
$provider->getRouteCollectionForRequest($request);
$cache = $this->cache->get('route:en:/path/add-one:');
$cache = $this->cache->get('route:[language]=en:/path/add-one:');
$this->assertEqual('/path/add/one', $cache->data['path']);
$this->assertEqual([], $cache->data['query']);
$this->assertEqual(3, count($cache->data['routes']));
@ -598,7 +598,7 @@ class RouteProviderTest extends KernelTestBase {
$request = Request::create($path, 'GET');
$provider->getRouteCollectionForRequest($request);
$cache = $this->cache->get('route:gsw-berne:/path/add-one:');
$cache = $this->cache->get('route:[language]=gsw-berne:/path/add-one:');
$this->assertEquals('/path/add/one', $cache->data['path']);
$this->assertEquals([], $cache->data['query']);
$this->assertEquals(3, count($cache->data['routes']));