Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- path_alias
id: path_alias.path_alias
target_entity_type_id: path_alias
target_bundle: path_alias
default_langcode: und
language_alterable: true

View File

@ -0,0 +1,5 @@
# Schema for the configuration files of the Path module.
field.widget.settings.path:
type: mapping
label: 'Link format settings'

View File

@ -0,0 +1,28 @@
---
label: 'Creating a URL alias for a content item'
related:
- path.overview
- path.editing_alias
- node.creating_content
- node.editing
---
{% set path_permissions_link_text %}
{% trans %}Create and edit URL aliases{% endtrans %}
{% endset %}
{% set path_permissions_link = render_var(help_route_link(path_permissions_link_text, 'user.admin_permissions.module', {'modules': 'path'})) %}
{% set overview_topic = render_var(help_topic_link('path.overview')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Give a content item page a human- or SEO-friendly URL alias; you can follow similar steps to create an alias for a taxonomy term page. See {{ overview_topic }} for more about aliases.{% endtrans %}</p>
<h2>{% trans %}Who can create URL aliases?{% endtrans %}</h2>
<p>{% trans %}Users with the <em>{{ path_permissions_link }}</em> permission can create aliases. To follow the steps in this topic, you will also need permission to edit the content item.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Locate and open the content edit form for the content item, or create a new content item (see related topics on creating and editing content).{% endtrans %}</li>
<li>{% trans %}Under <em>URL alias</em> on the edit form, enter the desired alias (for example, "/about-us"). Make sure the alias starts with a "/" character.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}Verify that the page can be visited with the new alias, for example <em>https://example.com/about-us</em>.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/user_guide/en/content-create.html">{% trans %}Creating a Content Item (Drupal User Guide){% endtrans %}</a></li>
</ul>

View File

@ -0,0 +1,26 @@
---
label: 'Editing a URL alias'
related:
- path.overview
- path.creating_alias
---
{% set path_permissions_link_text %}
{% trans %}Administer URL aliases{% endtrans %}
{% endset %}
{% set path_permissions_link = render_var(help_route_link(path_permissions_link_text, 'user.admin_permissions.module', {'modules': 'path'})) %}
{% set path_aliases_link_text %}
{% trans %}URL aliases{% endtrans %}
{% endset %}
{% set path_aliases_link = render_var(help_route_link(path_aliases_link_text, 'entity.path_alias.collection')) %}
{% set path_overview_topic = render_var(help_topic_link('path.overview')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Change an existing URL alias, to correct the path or the alias value. See {{ path_overview_topic }} for more about aliases.{% endtrans %}</p>
<h2>{% trans %}Who can manage URL aliases?{% endtrans %}</h2>
<p>{% trans %}Users with the <em>{{ path_permissions_link }}</em> permission can edit aliases.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administration menu, navigate to <em>Configuration</em> &gt; <em>Search and metadata</em> &gt; <em>{{ path_aliases_link }}</em>. A list of all the site's aliases will appear.{% endtrans %}</li>
<li>{% trans %}Click <em>Edit</em> in the dropdown button for the alias that you would like to change.{% endtrans %}</li>
<li>{% trans %}Make the required changes and click <em>Save</em>. You will be returned to the URL alias list page.{% endtrans %}</li>
<li>{% trans %}Note that you can also add new aliases from this page, for any path on your site.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,24 @@
---
label: 'Configuring aliases for URLs'
top_level: true
related:
- path.creating_alias
- path.editing_alias
---
<h2>{% trans %}What is a URL?{% endtrans %}</h2>
<p>{% trans %}URL is the abbreviation for "Uniform Resource Locator", which is the page's address on the web. It is the "name" by which a browser identifies a page to display. In the example "Visit us at <em>example.com</em>.", <em>https://example.com</em> would be the URL for the home page of your website. Users use URLs to locate content on the web.{% endtrans %}</p>
<h2>{% trans %}What is a path?{% endtrans %}</h2>
<p>{% trans %}A path is the unique, last part of the URL for a specific function or piece of content. For example, for a page whose full URL is <em>https://example.com/node/7</em>, the path is <em>node/7</em>. Here are some examples of paths you might find in your site:{% endtrans %}</p>
<ul>
<li>{% trans %}node/7: Path to a particular content item.{% endtrans %}</li>
<li>{% trans %}taxonomy/term/6: Path to a taxonomy term page.{% endtrans %}</li>
<li>{% trans %}user/3: Path to a user profile page.{% endtrans %}</li>
</ul>
<h2>{% trans %}What is an alias?{% endtrans %}</h2>
<p>{% trans %}The core software allows you to provide more understandable URLs for pages on your site, which are called <em>aliases</em>. For example, if you have an "About Us" page with the path <em>node/7</em>, you can set up an alias of <em>about</em> so that your visitors will see it as <em>https://www.example.com/about</em>.{% endtrans %}</p>
<h2>{% trans %}Overview of configuring paths, aliases, and URLs{% endtrans %}</h2>
<p>{% trans %}The core Path module provides the URL aliasing functionality. The contributed <a href="https://www.drupal.org/project/pathauto">Pathauto</a> module allows you to configure automatically-generated URL aliases for content items and other pages. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/user_guide/en/content-paths.html">{% trans %}Concept: Paths, Aliases, and URLs (Drupal User Guide){% endtrans %}</a></li>
</ul>

View File

@ -0,0 +1,29 @@
/**
* @file
* Attaches behaviors for the Path module.
*/
(function ($, Drupal) {
/**
* Behaviors for settings summaries on path edit forms.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches summary behavior on path edit forms.
*/
Drupal.behaviors.pathDetailsSummaries = {
attach(context) {
$(context)
.find('.path-form')
.drupalSetSummary((context) => {
const pathElement = document.querySelector(
'.js-form-item-path-0-alias input',
);
const path = pathElement?.value;
return path
? Drupal.t('Alias: @alias', { '@alias': path })
: Drupal.t('No alias');
});
},
};
})(jQuery, Drupal);

View File

@ -0,0 +1,58 @@
id: d6_url_alias
label: URL aliases
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_url_alias
constants:
slash: '/'
process:
# If you are using this file to build a custom migration consider removing
# the id field to allow incremental migrations.
id: pid
_path:
plugin: concat
source:
- constants/slash
- src
alias:
plugin: concat
source:
- constants/slash
- dst
node_translation:
-
plugin: explode
source: src
delimiter: /
-
# If the source path has no slashes return a dummy default value.
plugin: extract
default: 'INVALID_NID'
index:
- 1
-
plugin: migration_lookup
migration:
- d6_node_complete
- d6_node_translation
-
plugin: node_complete_node_translation_lookup
langcode:
-
plugin: null_coalesce
source:
- '@node_translation/1'
- language
-
plugin: default_value
default_value: 'und'
path:
plugin: path_set_translated
source:
- '@_path'
- '@node_translation'
destination:
plugin: entity:path_alias

View File

@ -0,0 +1,53 @@
id: d7_url_alias
label: URL aliases
migration_tags:
- Drupal 7
- Content
source:
plugin: d7_url_alias
constants:
slash: '/'
process:
# If you are using this file to build a custom migration consider removing
# the id field to allow incremental migrations.
id: pid
_path:
plugin: concat
source:
- constants/slash
- source
alias:
plugin: concat
source:
- constants/slash
- alias
node_translation:
-
plugin: explode
source: source
delimiter: /
-
# If the source path has no slashes return a dummy default value.
plugin: extract
default: 'INVALID_NID'
index:
- 1
-
plugin: migration_lookup
migration:
- d7_node_complete
- d7_node_translation
-
plugin: node_complete_node_translation_lookup
langcode:
plugin: null_coalesce
source:
- '@node_translation/1'
- language
path:
plugin: path_set_translated
source:
- '@_path'
- '@node_translation'
destination:
plugin: entity:path_alias

View File

@ -0,0 +1,5 @@
finished:
6:
path: path
7:
path: path

View File

@ -0,0 +1,8 @@
name: Path
type: module
description: 'Allows users to create custom URLs for existing paths on the site.'
package: Core
version: VERSION
configure: entity.path_alias.collection
dependencies:
- drupal:path_alias

View File

@ -0,0 +1,13 @@
<?php
/**
* @file
* Update functions for the path module.
*/
/**
* Implements hook_update_last_removed().
*/
function path_update_last_removed(): int {
return 8200;
}

View File

@ -0,0 +1,15 @@
drupal.path:
version: VERSION
js:
js/path.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupal.form
moved_files:
path/path.js:
deprecation_version: drupal:11.1.0
removed_version: drupal:12.0.0
deprecation_link: https://www.drupal.org/node/3471539
js:
path.js: 'js/path.js'

View File

@ -0,0 +1,5 @@
entity.path_alias.add_form:
route_name: entity.path_alias.add_form
title: 'Add alias'
appears_on:
- entity.path_alias.collection

View File

@ -0,0 +1,6 @@
entity.path_alias.collection:
title: 'URL aliases'
description: 'Add custom URLs to existing paths.'
route_name: entity.path_alias.collection
parent: system.admin_config_search
weight: -5

View File

@ -0,0 +1,4 @@
entity.path_alias.collection:
title: List
route_name: entity.path_alias.collection
base_route: entity.path_alias.collection

View File

@ -0,0 +1,4 @@
administer url aliases:
title: 'Administer URL aliases'
create url aliases:
title: 'Create and edit URL aliases'

View File

@ -0,0 +1,15 @@
<?php
/**
* @file
* Post update functions for the path module.
*/
/**
* Implements hook_removed_post_updates().
*/
function path_removed_post_updates(): array {
return [
'path_post_update_create_language_content_settings' => '9.0.0',
];
}

View File

@ -0,0 +1,2 @@
parameters:
path.skip_procedural_hook_scan: true

View File

@ -0,0 +1,71 @@
<?php
namespace Drupal\path\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides the path admin overview filter form.
*
* @internal
*/
class PathFilterForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'path_admin_filter_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $keys = NULL) {
$form['#attributes'] = ['class' => ['search-form']];
$form['basic'] = [
'#type' => 'details',
'#title' => $this->t('Filter aliases'),
'#open' => TRUE,
'#attributes' => ['class' => ['container-inline']],
];
$form['basic']['filter'] = [
'#type' => 'search',
'#title' => $this->t('Path alias'),
'#title_display' => 'invisible',
'#default_value' => $keys,
'#maxlength' => 128,
'#size' => 25,
];
$form['basic']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Filter'),
];
if ($keys) {
$form['basic']['reset'] = [
'#type' => 'submit',
'#value' => $this->t('Reset'),
'#submit' => ['::resetForm'],
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->setRedirect('entity.path_alias.collection', [], [
'query' => ['search' => trim($form_state->getValue('filter'))],
]);
}
/**
* Resets the filter selections.
*/
public function resetForm(array &$form, FormStateInterface $form_state) {
$form_state->setRedirect('entity.path_alias.collection');
}
}

View File

@ -0,0 +1,157 @@
<?php
namespace Drupal\path\Hook;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\path\PathAliasListBuilder;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\path\PathAliasForm;
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for path.
*/
class PathHooks {
use StringTranslationTrait;
/**
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.path':
$output = '';
$output .= '<h2>' . $this->t('About') . '</h2>';
$output .= '<p>' . $this->t('The Path module allows you to specify an alias, or custom URL, for any existing internal system path. Aliases should not be confused with URL redirects, which allow you to forward a changed or inactive URL to a new URL. In addition to making URLs more readable, aliases also help search engines index content more effectively. Multiple aliases may be used for a single internal system path. To automate the aliasing of paths, you can install the contributed module <a href=":pathauto">Pathauto</a>. For more information, see the <a href=":path">online documentation for the Path module</a>.', [
':path' => 'https://www.drupal.org/documentation/modules/path',
':pathauto' => 'https://www.drupal.org/project/pathauto',
]) . '</p>';
$output .= '<h2>' . $this->t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . $this->t('Creating aliases') . '</dt>';
$output .= '<dd>' . $this->t('If you create or edit a taxonomy term you can add an alias (for example <em>music/jazz</em>) in the field "URL alias". When creating or editing content you can add an alias (for example <em>about-us/team</em>) under the section "URL path settings" in the field "URL alias". Aliases for any other path can be added through the page <a href=":aliases">URL aliases</a>. To add aliases a user needs the permission <a href=":permissions">Create and edit URL aliases</a>.', [
':aliases' => Url::fromRoute('entity.path_alias.collection')->toString(),
':permissions' => Url::fromRoute('user.admin_permissions.module', [
'modules' => 'path',
])->toString(),
]) . '</dd>';
$output .= '<dt>' . $this->t('Managing aliases') . '</dt>';
$output .= '<dd>' . $this->t('The Path module provides a way to search and view a <a href=":aliases">list of all aliases</a> that are in use on your website. Aliases can be added, edited and deleted through this list.', [
':aliases' => Url::fromRoute('entity.path_alias.collection')->toString(),
]) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.path_alias.collection':
return '<p>' . $this->t("An alias defines a different name for an existing URL path - for example, the alias 'about' for the URL path 'node/1'. A URL path can have multiple aliases.") . '</p>';
case 'entity.path_alias.add_form':
return '<p>' . $this->t('Enter the path you wish to create the alias for, followed by the name of the new alias.') . '</p>';
}
return NULL;
}
/**
* Implements hook_entity_type_alter().
*/
#[Hook('entity_type_alter')]
public function entityTypeAlter(array &$entity_types) : void {
// @todo Remove the conditional once core fully supports "path_alias" as an
// optional module. See https://drupal.org/node/3092090.
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
if (isset($entity_types['path_alias'])) {
$entity_types['path_alias']->setFormClass('default', PathAliasForm::class);
$entity_types['path_alias']->setFormClass('delete', ContentEntityDeleteForm::class);
$entity_types['path_alias']->setHandlerClass('route_provider', ['html' => AdminHtmlRouteProvider::class]);
$entity_types['path_alias']->setListBuilderClass(PathAliasListBuilder::class);
$entity_types['path_alias']->setLinkTemplate('collection', '/admin/config/search/path');
$entity_types['path_alias']->setLinkTemplate('add-form', '/admin/config/search/path/add');
$entity_types['path_alias']->setLinkTemplate('edit-form', '/admin/config/search/path/edit/{path_alias}');
$entity_types['path_alias']->setLinkTemplate('delete-form', '/admin/config/search/path/delete/{path_alias}');
}
}
/**
* Implements hook_entity_base_field_info_alter().
*/
#[Hook('entity_base_field_info_alter')]
public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_type): void {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
if ($entity_type->id() === 'path_alias') {
$fields['langcode']->setDisplayOptions('form', [
'type' => 'language_select',
'weight' => 0,
'settings' => [
'include_locked' => FALSE,
],
]);
$fields['path']->setDisplayOptions('form', ['type' => 'string_textfield', 'weight' => 5, 'settings' => ['size' => 45]]);
$fields['alias']->setDisplayOptions('form', ['type' => 'string_textfield', 'weight' => 10, 'settings' => ['size' => 45]]);
}
}
/**
* Implements hook_entity_base_field_info().
*/
#[Hook('entity_base_field_info')]
public function entityBaseFieldInfo(EntityTypeInterface $entity_type): array {
if (in_array($entity_type->id(), ['taxonomy_term', 'node', 'media'], TRUE)) {
$fields['path'] = BaseFieldDefinition::create('path')->setLabel($this->t('URL alias'))->setTranslatable(TRUE)->setDisplayOptions('form', ['type' => 'path', 'weight' => 30])->setDisplayConfigurable('form', TRUE)->setComputed(TRUE);
return $fields;
}
return [];
}
/**
* Implements hook_entity_translation_create().
*/
#[Hook('entity_translation_create')]
public function entityTranslationCreate(ContentEntityInterface $translation): void {
foreach ($translation->getFieldDefinitions() as $field_name => $field_definition) {
if ($field_definition->getType() === 'path' && $translation->get($field_name)->pid) {
// If there are values and a path ID, update the langcode and unset the
// path ID to save this as a new alias.
$translation->get($field_name)->langcode = $translation->language()->getId();
$translation->get($field_name)->pid = NULL;
}
}
}
/**
* Implements hook_field_widget_single_element_form_alter().
*/
#[Hook('field_widget_single_element_form_alter')]
public function fieldWidgetSingleElementFormAlter(&$element, FormStateInterface $form_state, $context): void {
$field_definition = $context['items']->getFieldDefinition();
$field_name = $field_definition->getName();
$entity_type = $field_definition->getTargetEntityTypeId();
$widget_name = $context['widget']->getPluginId();
if ($entity_type === 'path_alias') {
if (($field_name === 'path' || $field_name === 'alias') && $widget_name === 'string_textfield') {
$element['value']['#field_prefix'] = \Drupal::service('router.request_context')->getCompleteBaseUrl();
}
if ($field_name === 'langcode') {
$element['value']['#description'] = $this->t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set as <em>- Not specified -</em>.');
$element['value']['#empty_value'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
$element['value']['#empty_option'] = $this->t('- Not specified -');
}
if ($field_name === 'path') {
$element['value']['#description'] = $this->t('Specify the existing path you wish to alias. For example: /node/28, /media/1, /taxonomy/term/1.');
}
if ($field_name === 'alias') {
$element['value']['#description'] = $this->t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.');
}
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Drupal\path;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Form handler for the path alias edit forms.
*
* @internal
*/
class PathAliasForm extends ContentEntityForm {
/**
* The path_alias entity.
*
* @var \Drupal\path_alias\PathAliasInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
parent::save($form, $form_state);
$this->messenger()->addStatus($this->t('The alias has been saved.'));
$form_state->setRedirect('entity.path_alias.collection');
}
}

View File

@ -0,0 +1,197 @@
<?php
namespace Drupal\path;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\path_alias\AliasManagerInterface;
use Drupal\Core\Url;
use Drupal\path\Form\PathFilterForm;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a class to build a listing of path_alias entities.
*
* @see \Drupal\path_alias\Entity\PathAlias
*/
class PathAliasListBuilder extends EntityListBuilder {
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $currentRequest;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The path alias manager.
*
* @var \Drupal\path_alias\AliasManagerInterface
*/
protected $aliasManager;
/**
* Constructs a new PathAliasListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Symfony\Component\HttpFoundation\Request $current_request
* The current request.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\path_alias\AliasManagerInterface $alias_manager
* The path alias manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, Request $current_request, FormBuilderInterface $form_builder, LanguageManagerInterface $language_manager, AliasManagerInterface $alias_manager) {
parent::__construct($entity_type, $storage);
$this->currentRequest = $current_request;
$this->formBuilder = $form_builder;
$this->languageManager = $language_manager;
$this->aliasManager = $alias_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('request_stack')->getCurrentRequest(),
$container->get('form_builder'),
$container->get('language_manager'),
$container->get('path_alias.manager')
);
}
/**
* {@inheritdoc}
*/
protected function getEntityIds() {
$query = $this->getStorage()->getQuery()->accessCheck(TRUE);
$search = $this->currentRequest->query->get('search');
if ($search) {
$query->condition('alias', $search, 'CONTAINS');
}
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
// Allow the entity query to sort using the table header.
$header = $this->buildHeader();
$query->tableSort($header);
return $query->execute();
}
/**
* {@inheritdoc}
*/
public function render() {
$keys = $this->currentRequest->query->get('search');
$build['path_admin_filter_form'] = $this->formBuilder->getForm(PathFilterForm::class, $keys);
$build += parent::render();
$build['table']['#empty'] = $this->t('No path aliases available. <a href=":link">Add URL alias</a>.', [':link' => Url::fromRoute('entity.path_alias.add_form')->toString()]);
return $build;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = [
'alias' => [
'data' => $this->t('Alias'),
'field' => 'alias',
'specifier' => 'alias',
'sort' => 'asc',
],
'path' => [
'data' => $this->t('System path'),
'field' => 'path',
'specifier' => 'path',
],
];
// Enable language column and filter if multiple languages are added.
if ($this->languageManager->isMultilingual()) {
$header['language_name'] = [
'data' => $this->t('Language'),
'field' => 'langcode',
'specifier' => 'langcode',
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
}
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\Core\Path\Entity\PathAlias $entity */
$langcode = $entity->language()->getId();
$alias = $entity->getAlias();
$path = $entity->getPath();
$url = Url::fromUserInput($path);
$row['data']['alias']['data'] = [
'#type' => 'link',
'#title' => $alias,
'#url' => $url,
];
// Create a new URL for linking to the un-aliased system path.
$system_url = Url::fromUri("base:{$path}");
$row['data']['path']['data'] = [
'#type' => 'link',
'#title' => $path,
'#url' => $system_url,
];
if ($this->languageManager->isMultilingual()) {
$row['data']['language_name'] = $this->languageManager->getLanguageName($langcode);
}
$row['data']['operations']['data'] = $this->buildOperations($entity);
// If the system path maps to a different URL alias, highlight this table
// row to let the user know of old aliases.
if ($alias != $this->aliasManager->getAliasByPath($path, $langcode)) {
$row['class'] = ['warning'];
}
return $row;
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Drupal\path\Plugin\Field\FieldType;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ComputedItemListTrait;
/**
* Represents a configurable entity path field.
*/
class PathFieldItemList extends FieldItemList {
use ComputedItemListTrait;
/**
* {@inheritdoc}
*/
protected function computeValue() {
// Default the langcode to the current language if this is a new entity or
// there is no alias for an existent entity.
// @todo Set the langcode to not specified for untranslatable fields
// in https://www.drupal.org/node/2689459.
$value = ['langcode' => $this->getLangcode()];
$entity = $this->getEntity();
if (!$entity->isNew()) {
/** @var \Drupal\path_alias\AliasRepositoryInterface $path_alias_repository */
$path_alias_repository = \Drupal::service('path_alias.repository');
if ($path_alias = $path_alias_repository->lookupBySystemPath('/' . $entity->toUrl()->getInternalPath(), $this->getLangcode())) {
$value = [
'alias' => $path_alias['alias'],
'pid' => $path_alias['id'],
'langcode' => $path_alias['langcode'],
];
}
}
$this->list[0] = $this->createItem(0, $value);
}
/**
* {@inheritdoc}
*/
public function defaultAccess($operation = 'view', ?AccountInterface $account = NULL) {
if ($operation == 'view') {
return AccessResult::allowed();
}
return AccessResult::allowedIfHasPermissions($account, ['create url aliases', 'administer url aliases'], 'OR')->cachePerPermissions();
}
/**
* {@inheritdoc}
*/
public function delete() {
// Delete all aliases associated with this entity in the current language.
$entity = $this->getEntity();
$path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias');
$entities = $path_alias_storage->loadByProperties([
'path' => '/' . $entity->toUrl()->getInternalPath(),
'langcode' => $entity->language()->getId(),
]);
$path_alias_storage->delete($entities);
}
}

View File

@ -0,0 +1,147 @@
<?php
namespace Drupal\path\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
/**
* Defines the 'path' entity field type.
*/
#[FieldType(
id: "path",
label: new TranslatableMarkup("Path"),
description: new TranslatableMarkup("An entity field containing a path alias and related data."),
default_widget: "path",
no_ui: TRUE,
list_class: PathFieldItemList::class,
constraints: ["PathAlias" => []],
)]
class PathItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['alias'] = DataDefinition::create('string')
->setLabel(t('Path alias'));
$properties['pid'] = DataDefinition::create('integer')
->setLabel(t('Path id'));
$properties['langcode'] = DataDefinition::create('string')
->setLabel(t('Language Code'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [];
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$alias = $this->get('alias')->getValue();
$pid = $this->get('pid')->getValue();
$langcode = $this->get('langcode')->getValue();
return ($alias === NULL || $alias === '') && ($pid === NULL || $pid === '') && ($langcode === NULL || $langcode === '');
}
/**
* {@inheritdoc}
*/
public function preSave() {
$alias = $this->get('alias')->getValue();
if ($alias !== NULL) {
$this->set('alias', trim($alias));
}
}
/**
* {@inheritdoc}
*/
public function postSave($update) {
$path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias');
$entity = $this->getEntity();
$alias = $this->get('alias')->getValue();
$pid = $this->get('pid')->getValue();
$langcode = $this->get('langcode')->getValue();
// 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 = ($langcode && $pid) ? $langcode : $this->getLangcode();
// If we have an alias, we need to create or update a path alias entity.
if ($alias) {
$properties = [
'path' => '/' . $entity->toUrl()->getInternalPath(),
'alias' => $alias,
'langcode' => $alias_langcode,
];
if (!$pid) {
// Try to load it from storage before creating it. In some cases the
// path alias could be created before this function runs. For example,
// \Drupal\workspaces\EntityOperations::entityTranslationInsert will
// create a translation, and an associated path alias will be created
// with it.
$query = $path_alias_storage->getQuery()->accessCheck(FALSE);
foreach ($properties as $field => $value) {
$query->condition($field, $value);
}
$ids = $query->execute();
$pid = $ids ? reset($ids) : $pid;
}
if (!$pid) {
$path_alias = $path_alias_storage->create($properties);
$path_alias->save();
$this->set('pid', $path_alias->id());
}
else {
$path_alias = $path_alias_storage->load($pid);
if ($alias != $path_alias->getAlias()) {
$path_alias->setAlias($alias);
$path_alias->save();
}
}
}
elseif ($pid) {
// Otherwise, delete the old alias if the user erased it.
$path_alias = $path_alias_storage->load($pid);
if ($entity->isDefaultRevision()) {
$path_alias_storage->delete([$path_alias]);
}
else {
$path_alias_storage->deleteRevision($path_alias->getRevisionID());
}
}
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$random = new Random();
$values['alias'] = '/' . str_replace(' ', '-', strtolower($random->sentences(3)));
return $values;
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'alias';
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Drupal\path\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Plugin implementation of the 'path' widget.
*/
#[FieldWidget(
id: 'path',
label: new TranslatableMarkup('URL alias'),
field_types: ['path']
)]
class PathWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$entity = $items->getEntity();
$element += [
'#element_validate' => [[static::class, 'validateFormElement']],
];
$element['alias'] = [
'#type' => 'textfield',
'#title' => $element['#title'],
'#default_value' => $items[$delta]->alias,
'#required' => $element['#required'],
'#maxlength' => 255,
'#description' => $this->t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.'),
];
$element['pid'] = [
'#type' => 'value',
'#value' => $items[$delta]->pid,
];
$element['source'] = [
'#type' => 'value',
'#value' => !$entity->isNew() ? '/' . $entity->toUrl()->getInternalPath() : NULL,
];
$element['langcode'] = [
'#type' => 'value',
'#value' => $items[$delta]->langcode,
];
// If the advanced settings tabs-set is available (normally rendered in the
// second column on wide-resolutions), place the field as a details element
// in this tab-set.
if (isset($form['advanced'])) {
$element += [
'#type' => 'details',
'#title' => $this->t('URL path settings'),
'#open' => !empty($items[$delta]->alias),
'#group' => 'advanced',
'#access' => $entity->get('path')->access('edit'),
'#attributes' => [
'class' => ['path-form'],
],
'#attached' => [
'library' => ['path/drupal.path'],
],
];
$element['#weight'] = 30;
}
return $element;
}
/**
* Form element validation handler for URL alias form element.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public static function validateFormElement(array &$element, FormStateInterface $form_state) {
// Trim the submitted value of whitespace and slashes.
$alias = rtrim(trim($element['alias']['#value']), " \\/");
if ($alias !== '') {
$form_state->setValueForElement($element['alias'], $alias);
/** @var \Drupal\path_alias\PathAliasInterface $path_alias */
$path_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([
'path' => $element['source']['#value'],
'alias' => $alias,
'langcode' => $element['langcode']['#value'],
]);
$violations = $path_alias->validate();
foreach ($violations as $violation) {
// Newly created entities do not have a system path yet, so we need to
// disregard some violations.
if (!$path_alias->getPath() && $violation->getPropertyPath() === 'path') {
continue;
}
$form_state->setError($element['alias'], $violation->getMessage());
}
}
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
return $element['alias'];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Drupal\path\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Validation constraint for changing path aliases in pending revisions.
*/
#[Constraint(
id: 'PathAlias',
label: new TranslatableMarkup('Path alias.', [], ['context' => 'Validation'])
)]
class PathAliasConstraint extends SymfonyConstraint {
/**
* The default violation message.
*
* @var string
*/
public $message = 'You can only change the URL alias for the <em>published</em> version of this content.';
}

View File

@ -0,0 +1,63 @@
<?php
namespace Drupal\path\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Constraint validator for changing path aliases in pending revisions.
*/
class PathAliasConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* Creates a new PathAliasConstraintValidator instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint): void {
$entity = !empty($value->getParent()) ? $value->getEntity() : NULL;
if ($entity && !$entity->isNew() && !$entity->isDefaultRevision()) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $original */
$original = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
$entity_langcode = $entity->language()->getId();
// Only add the violation if the current translation does not have the
// same path alias.
if ($original->hasTranslation($entity_langcode)) {
if ($value->alias != $original->getTranslation($entity_langcode)->path->alias) {
$this->context->addViolation($constraint->message);
}
}
}
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\path\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* A process plugin to update the path of a translated node.
*
* Available configuration keys:
* - source: An array of two values, the first being the original path, and the
* second being an array of the format [nid, langcode] if a translated node
* exists (likely from a migration lookup). Paths not of the format
* '/node/<nid>' will pass through unchanged, as will any inputs with invalid
* or missing translated nodes.
*
* This plugin will return the correct path for the translated node if the above
* conditions are met, and will return the original path otherwise.
*
* Example:
* node_translation:
* -
* plugin: explode
* source: source
* delimiter: /
* -
* # If the source path has no slashes return a dummy default value.
* plugin: extract
* default: 'INVALID_NID'
* index:
* - 1
* -
* plugin: migration_lookup
* migration: d7_node_translation
* _path:
* plugin: concat
* source:
* - constants/slash
* - source
* path:
* plugin: path_set_translated
* source:
* - '@_path'
* - '@node_translation'
*
* In the example above, if the node_translation lookup succeeds and the
* original path is of the format '/node/<original node nid>', then the new path
* will be set to '/node/<translated node nid>'
*/
#[MigrateProcess('path_set_translated')]
class PathSetTranslated extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!is_array($value)) {
throw new MigrateException("The input value should be an array.");
}
$path = $value[0] ?? '';
$nid = (is_array($value[1]) && isset($value[1][0])) ? $value[1][0] : FALSE;
if (preg_match('/^\/node\/\d+$/', $path) && $nid) {
return '/node/' . $nid;
}
return $path;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Drupal\path\Plugin\migrate\source;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Base class for the url_alias source plugins.
*/
abstract class UrlAliasBase extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
// The order of the migration is significant since
// \Drupal\path_alias\AliasRepository::lookupPathAlias() orders by pid
// before returning a result. Postgres does not automatically order by
// primary key therefore we need to add a specific order by.
return $this->select('url_alias', 'ua')->fields('ua')->orderBy('pid');
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'pid' => $this->t('The numeric identifier of the path alias.'),
'language' => $this->t('The language code of the URL alias.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['pid']['type'] = 'integer';
return $ids;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Drupal\path\Plugin\migrate\source\d6;
use Drupal\path\Plugin\migrate\source\UrlAliasBase;
/**
* Drupal 6 URL aliases source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_url_alias",
* source_module = "path"
* )
*/
class UrlAlias extends UrlAliasBase {
/**
* {@inheritdoc}
*/
public function fields() {
$fields = parent::fields();
$fields['src'] = $this->t('The internal system path.');
$fields['dst'] = $this->t('The path alias.');
return $fields;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Drupal\path\Plugin\migrate\source\d7;
use Drupal\path\Plugin\migrate\source\UrlAliasBase;
/**
* Drupal 7 URL aliases source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_url_alias",
* source_module = "path"
* )
*/
class UrlAlias extends UrlAliasBase {
/**
* {@inheritdoc}
*/
public function fields() {
$fields = parent::fields();
$fields['source'] = $this->t('The internal system path.');
$fields['alias'] = $this->t('The path alias.');
return $fields;
}
}

View File

@ -0,0 +1,5 @@
name: 'Path test miscellaneous utilities'
type: module
description: 'Utilities for path testing'
package: Testing
version: VERSION

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\path_test_misc\Hook;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\node\NodeInterface;
/**
* Hook implementations for path_test_misc.
*/
class PathTestMiscHooks {
/**
* Implements hook_ENTITY_TYPE_presave() for node entities.
*
* This is invoked from testAliasDuplicationPrevention.
*/
#[Hook('node_presave')]
public function nodePresave(NodeInterface $node): void {
if ($node->getTitle() !== 'path duplication test') {
return;
}
// Update the title to be able to check that this code ran.
$node->setTitle('path duplication test ran');
// Create a path alias that has the same values as the one in
// PathItem::postSave.
$path = \Drupal::entityTypeManager()->getStorage('path_alias')
->create([
'path' => '/node/1',
'alias' => '/my-alias',
'langcode' => 'en',
]);
$path->save();
}
}

View File

@ -0,0 +1,5 @@
name: 'Path test with node grants'
type: module
description: 'Tests URL alias with hook_node_grants implementation'
package: Testing
version: VERSION

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Drupal\path_test_node_grants\Hook;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for path_test_node_grants.
*/
class PathTestNodeGrantsHooks {
/**
* Implements hook_node_grants().
*/
#[Hook('node_grants')]
public function nodeGrants(AccountInterface $account, $operation) : array {
$grants = [];
return $grants;
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for path.
*
* @group path
*/
class GenericTest extends GenericModuleTestBase {}

View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
/**
* Tests the Path admin UI.
*
* @group path
*/
class PathAdminTest extends PathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['path'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
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',
]);
$this->drupalLogin($web_user);
}
/**
* Tests the filtering aspect of the Path UI.
*/
public function testPathFiltering(): void {
// Create test nodes.
$node1 = $this->drupalCreateNode();
$node2 = $this->drupalCreateNode();
$node3 = $this->drupalCreateNode();
// Create aliases.
$path1 = '/node/' . $node1->id();
$alias1 = '/' . $this->randomMachineName(8);
$edit = [
'path[0][value]' => $path1,
'alias[0][value]' => $alias1,
];
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$path2 = '/node/' . $node2->id();
$alias2 = '/' . $this->randomMachineName(8);
$edit = [
'path[0][value]' => $path2,
'alias[0][value]' => $alias2,
];
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$path3 = '/node/' . $node3->id();
$alias3 = '/' . $this->randomMachineName(4) . '/' . $this->randomMachineName(4);
$edit = [
'path[0][value]' => $path3,
'alias[0][value]' => $alias3,
];
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
// Filter by the first alias.
$edit = [
'filter' => $alias1,
];
$this->submitForm($edit, 'Filter');
$this->assertSession()->linkByHrefExists($alias1);
$this->assertSession()->linkByHrefNotExists($alias2);
$this->assertSession()->linkByHrefNotExists($alias3);
$this->assertSession()->linkByHrefExists($path1);
$this->assertSession()->linkByHrefNotExists($path2);
$this->assertSession()->linkByHrefNotExists($path3);
// Filter by the second alias.
$edit = [
'filter' => $alias2,
];
$this->submitForm($edit, 'Filter');
$this->assertSession()->linkByHrefNotExists($alias1);
$this->assertSession()->linkByHrefExists($alias2);
$this->assertSession()->linkByHrefNotExists($alias3);
$this->assertSession()->linkByHrefNotExists($path1);
$this->assertSession()->linkByHrefExists($path2);
$this->assertSession()->linkByHrefNotExists($path3);
// Filter by the third alias which has a slash.
$edit = [
'filter' => $alias3,
];
$this->submitForm($edit, 'Filter');
$this->assertSession()->linkByHrefNotExists($alias1);
$this->assertSession()->linkByHrefNotExists($alias2);
$this->assertSession()->linkByHrefExists($alias3);
$this->assertSession()->linkByHrefNotExists($path1);
$this->assertSession()->linkByHrefNotExists($path2);
$this->assertSession()->linkByHrefExists($path3);
// Filter by a random string with a different length.
$edit = [
'filter' => $this->randomMachineName(10),
];
$this->submitForm($edit, 'Filter');
$this->assertSession()->linkByHrefNotExists($alias1);
$this->assertSession()->linkByHrefNotExists($alias2);
$this->assertSession()->linkByHrefNotExists($path1);
$this->assertSession()->linkByHrefNotExists($path2);
// Reset the filter.
$edit = [];
$this->submitForm($edit, 'Reset');
$this->assertSession()->linkByHrefExists($alias1);
$this->assertSession()->linkByHrefExists($alias2);
$this->assertSession()->linkByHrefExists($path1);
$this->assertSession()->linkByHrefExists($path2);
}
}

View File

@ -0,0 +1,458 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\Tests\WaitTerminateTestTrait;
/**
* Tests modifying path aliases from the UI.
*
* @group path
*/
class PathAliasTest extends PathTestBase {
use WaitTerminateTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['path'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
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',
'access content overview',
]);
$this->drupalLogin($web_user);
// The \Drupal\path_alias\AliasPrefixList service performs cache clears
// after Drupal has flushed the response to the client. We use
// WaitTerminateTestTrait to wait for Drupal to do this before continuing.
$this->setWaitForTerminate();
}
/**
* Tests the path cache.
*/
public function testPathCache(): void {
// Create test node.
$node1 = $this->drupalCreateNode();
// Create alias.
$edit = [];
$edit['path[0][value]'] = '/node/' . $node1->id();
$edit['alias[0][value]'] = '/' . $this->randomMachineName(8);
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
// Check the path alias prefix list cache.
$prefix_list = \Drupal::cache('bootstrap')->get('path_alias_prefix_list');
$this->assertTrue($prefix_list->data['node']);
$this->assertFalse($prefix_list->data['admin']);
// Visit the system path for the node and confirm a cache entry is
// created.
\Drupal::cache('data')->deleteAll();
// Make sure the path is not converted to the alias.
$this->drupalGet(trim($edit['path[0][value]'], '/'), ['alias' => TRUE]);
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.');
// Visit the alias for the node and confirm a cache entry is created.
\Drupal::cache('data')->deleteAll();
// @todo Remove this once https://www.drupal.org/node/2480077 lands.
Cache::invalidateTags(['rendered']);
$this->drupalGet(trim($edit['alias[0][value]'], '/'));
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.');
}
/**
* Tests alias functionality through the admin interfaces.
*/
public function testAdminAlias(): void {
// Create test node.
$node1 = $this->drupalCreateNode();
// Create alias.
$edit = [];
$edit['path[0][value]'] = '/node/' . $node1->id();
$edit['alias[0][value]'] = '/' . $this->getRandomGenerator()->word(8);
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
// Confirm that the alias works.
$this->drupalGet($edit['alias[0][value]']);
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->statusCodeEquals(200);
// Confirm that the alias works in a case-insensitive way.
$this->assertTrue(ctype_lower(ltrim($edit['alias[0][value]'], '/')));
$this->drupalGet($edit['alias[0][value]']);
// Lower case.
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet(mb_strtoupper($edit['alias[0][value]']));
// Upper case.
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->statusCodeEquals(200);
// Change alias to one containing "exotic" characters.
$pid = $this->getPID($edit['alias[0][value]']);
$previous = $edit['alias[0][value]'];
// Lower-case letters.
$edit['alias[0][value]'] = '/alias' .
// "Special" ASCII characters.
"- ._~!$'\"()*@[]?&+%#,;=:" .
// Characters that look like a percent-escaped string.
"%23%25%26%2B%2F%3F" .
// Characters from various non-ASCII alphabets.
"中國書۞";
$connection = Database::getConnection();
if ($connection->databaseType() != 'sqlite') {
// When using LIKE for case-insensitivity, the SQLite driver is
// currently unable to find the upper-case versions of non-ASCII
// characters.
// @todo fix this in https://www.drupal.org/node/2607432
// cSpell:disable-next-line
$edit['alias[0][value]'] .= "ïвβéø";
}
$this->drupalGet('admin/config/search/path/edit/' . $pid);
$this->submitForm($edit, 'Save');
// Confirm that the alias works.
$this->drupalGet(mb_strtoupper($edit['alias[0][value]']));
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->statusCodeEquals(200);
$this->container->get('path_alias.manager')->cacheClear();
// Confirm that previous alias no longer works.
$this->drupalGet($previous);
$this->assertSession()->pageTextNotContains($node1->label());
$this->assertSession()->statusCodeEquals(404);
// Create second test node.
$node2 = $this->drupalCreateNode();
// Set alias to second test node.
$edit['path[0][value]'] = '/node/' . $node2->id();
// Leave $edit['alias'] the same
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
// Confirm no duplicate was created.
$this->assertSession()->statusMessageContains("The alias {$edit['alias[0][value]']} is already in use in this language.", 'error');
$edit_upper = $edit;
$edit_upper['alias[0][value]'] = mb_strtoupper($edit['alias[0][value]']);
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit_upper, 'Save');
$this->assertSession()->statusMessageContains("The alias {$edit_upper['alias[0][value]']} could not be added because it is already in use in this language with different capitalization: {$edit['alias[0][value]']}.", 'error');
// Delete alias.
$this->drupalGet('admin/config/search/path/edit/' . $pid);
$this->clickLink('Delete');
$this->assertSession()->pageTextContains("Are you sure you want to delete the URL alias {$edit['alias[0][value]']}?");
$this->submitForm([], 'Delete');
// Confirm that the alias no longer works.
$this->drupalGet($edit['alias[0][value]']);
$this->assertSession()->pageTextNotContains($node1->label());
$this->assertSession()->statusCodeEquals(404);
// Create a really long alias.
$edit = [];
$edit['path[0][value]'] = '/node/' . $node1->id();
$alias = '/' . $this->randomMachineName(128);
$edit['alias[0][value]'] = $alias;
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
// The alias will always be found.
$this->assertSession()->pageTextContains($alias);
// Create third test node.
$node3 = $this->drupalCreateNode();
// Create absolute path alias.
$edit = [];
$edit['path[0][value]'] = '/node/' . $node3->id();
$node3_alias = '/' . $this->randomMachineName(8);
$edit['alias[0][value]'] = $node3_alias;
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
// Create fourth test node.
$node4 = $this->drupalCreateNode();
// Create alias with trailing slash.
$edit = [];
$edit['path[0][value]'] = '/node/' . $node4->id();
$node4_alias = '/' . $this->randomMachineName(8);
$edit['alias[0][value]'] = $node4_alias . '/';
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
// Confirm that the alias with trailing slash is not found.
$this->assertSession()->pageTextNotContains($edit['alias[0][value]']);
// The alias without trailing flash is found.
$this->assertSession()->pageTextContains(trim($edit['alias[0][value]'], '/'));
// Update an existing alias to point to a different source.
$pid = $this->getPID($node4_alias);
$edit = [];
$edit['alias[0][value]'] = $node4_alias;
$edit['path[0][value]'] = '/node/' . $node2->id();
$this->drupalGet('admin/config/search/path/edit/' . $pid);
$this->submitForm($edit, 'Save');
$this->assertSession()->statusMessageContains('The alias has been saved.', 'status');
$this->drupalGet($edit['alias[0][value]']);
// Previous alias should no longer work.
$this->assertSession()->pageTextNotContains($node4->label());
// Alias should work.
$this->assertSession()->pageTextContains($node2->label());
$this->assertSession()->statusCodeEquals(200);
// Update an existing alias to use a duplicate alias.
$pid = $this->getPID($node3_alias);
$edit = [];
$edit['alias[0][value]'] = $node4_alias;
$edit['path[0][value]'] = '/node/' . $node3->id();
$this->drupalGet('admin/config/search/path/edit/' . $pid);
$this->submitForm($edit, 'Save');
$this->assertSession()->statusMessageContains("The alias {$edit['alias[0][value]']} is already in use in this language.", 'error');
// Create an alias without a starting slash.
$node5 = $this->drupalCreateNode();
$edit = [];
$edit['path[0][value]'] = 'node/' . $node5->id();
$node5_alias = $this->randomMachineName(8);
$edit['alias[0][value]'] = $node5_alias . '/';
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$this->assertSession()->addressEquals('admin/config/search/path/add');
$this->assertSession()->statusMessageContains('The source path has to start with a slash.', 'error');
$this->assertSession()->statusMessageContains('The alias path has to start with a slash.', 'error');
}
/**
* Tests alias functionality through the node interfaces.
*/
public function testNodeAlias(): void {
// Create test node.
$node1 = $this->drupalCreateNode();
// Create alias.
$edit = [];
$edit['path[0][alias]'] = '/' . $this->randomMachineName(8);
$this->drupalGet('node/' . $node1->id() . '/edit');
$this->submitForm($edit, 'Save');
// Confirm that the alias works.
$this->drupalGet($edit['path[0][alias]']);
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->statusCodeEquals(200);
// Confirm the 'canonical' and 'shortlink' URLs.
$this->assertSession()->elementExists('xpath', "//link[contains(@rel, 'canonical') and contains(@href, '" . $edit['path[0][alias]'] . "')]");
$this->assertSession()->elementExists('xpath', "//link[contains(@rel, 'shortlink') and contains(@href, 'node/" . $node1->id() . "')]");
$previous = $edit['path[0][alias]'];
// Change alias to one containing "exotic" characters.
// Lower-case letters.
$edit['path[0][alias]'] = '/alias' .
// "Special" ASCII characters.
"- ._~!$'\"()*@[]?&+%#,;=:" .
// Characters that look like a percent-escaped string.
"%23%25%26%2B%2F%3F" .
// Characters from various non-ASCII alphabets.
"中國書۞";
$connection = Database::getConnection();
if ($connection->databaseType() != 'sqlite') {
// When using LIKE for case-insensitivity, the SQLite driver is
// currently unable to find the upper-case versions of non-ASCII
// characters.
// @todo fix this in https://www.drupal.org/node/2607432
// cSpell:disable-next-line
$edit['path[0][alias]'] .= "ïвβéø";
}
$this->drupalGet('node/' . $node1->id() . '/edit');
$this->submitForm($edit, 'Save');
// Confirm that the alias works.
$this->drupalGet(mb_strtoupper($edit['path[0][alias]']));
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->statusCodeEquals(200);
// Make sure that previous alias no longer works.
$this->drupalGet($previous);
$this->assertSession()->pageTextNotContains($node1->label());
$this->assertSession()->statusCodeEquals(404);
// Create second test node.
$node2 = $this->drupalCreateNode();
// Set alias to second test node.
// Leave $edit['path[0][alias]'] the same.
$this->drupalGet('node/' . $node2->id() . '/edit');
$this->submitForm($edit, 'Save');
// Confirm that the alias didn't make a duplicate.
$this->assertSession()->statusMessageContains("The alias {$edit['path[0][alias]']} is already in use in this language.", 'error');
// Delete alias.
$this->drupalGet('node/' . $node1->id() . '/edit');
$this->submitForm(['path[0][alias]' => ''], 'Save');
// Confirm that the alias no longer works.
$this->drupalGet($edit['path[0][alias]']);
$this->assertSession()->pageTextNotContains($node1->label());
$this->assertSession()->statusCodeEquals(404);
// Create third test node.
$node3 = $this->drupalCreateNode();
// Set its path alias to an absolute path.
$edit = ['path[0][alias]' => '/' . $this->randomMachineName(8)];
$this->drupalGet('node/' . $node3->id() . '/edit');
$this->submitForm($edit, 'Save');
// Confirm that the alias was converted to a relative path.
$this->drupalGet(trim($edit['path[0][alias]'], '/'));
$this->assertSession()->pageTextContains($node3->label());
$this->assertSession()->statusCodeEquals(200);
// Create fourth test node.
$node4 = $this->drupalCreateNode();
// Set its path alias to have a trailing slash.
$edit = ['path[0][alias]' => '/' . $this->randomMachineName(8) . '/'];
$this->drupalGet('node/' . $node4->id() . '/edit');
$this->submitForm($edit, 'Save');
// Confirm that the alias was converted to a relative path.
$this->drupalGet(trim($edit['path[0][alias]'], '/'));
$this->assertSession()->pageTextContains($node4->label());
$this->assertSession()->statusCodeEquals(200);
// Create fifth test node.
$node5 = $this->drupalCreateNode();
// Set a path alias.
$edit = ['path[0][alias]' => '/' . $this->randomMachineName(8)];
$this->drupalGet('node/' . $node5->id() . '/edit');
$this->submitForm($edit, 'Save');
// Delete the node and check that the path alias is also deleted.
$node5->delete();
$path_alias = \Drupal::service('path_alias.repository')->lookUpBySystemPath('/node/' . $node5->id(), $node5->language()->getId());
$this->assertNull($path_alias, 'Alias was successfully deleted when the referenced node was deleted.');
// Create sixth test node.
$node6 = $this->drupalCreateNode();
// Test the special case where the alias is '0'.
$edit = ['path[0][alias]' => '0'];
$this->drupalGet($node6->toUrl('edit-form'));
$this->submitForm($edit, 'Save');
$this->assertSession()->statusMessageContains('The alias path has to start with a slash.', 'error');
// 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->drupalGet($node6->toUrl('edit-form'));
$this->submitForm($edit, '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).
$this->assertSession()->elementAttributeContains('xpath', "//a[normalize-space(text())='{$node6->getTitle()}']", 'href', base_path() . $alias);
$this->clickLink($node6->getTitle());
$this->assertSession()->statusCodeEquals(404);
}
/**
* Returns the path ID.
*
* @param string $alias
* A string containing an aliased path.
*
* @return int
* Integer representing the path ID.
*/
public function getPID($alias) {
$result = \Drupal::entityTypeManager()->getStorage('path_alias')->getQuery()
->condition('alias', $alias, '=')
->accessCheck(FALSE)
->execute();
return reset($result);
}
/**
* Tests that duplicate aliases fail validation.
*/
public function testDuplicateNodeAlias(): void {
// Create one node with a random alias.
$node_one = $this->drupalCreateNode();
$edit = [];
$edit['path[0][alias]'] = '/' . $this->randomMachineName();
$this->drupalGet('node/' . $node_one->id() . '/edit');
$this->submitForm($edit, 'Save');
// Now create another node and try to set the same alias.
$node_two = $this->drupalCreateNode();
$this->drupalGet('node/' . $node_two->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->statusMessageContains("The alias {$edit['path[0][alias]']} is already in use in this language.", 'error');
$path_alias = $this->assertSession()->fieldExists('path[0][alias]');
$this->assertSession()->fieldValueEquals('path[0][alias]', $edit['path[0][alias]']);
$this->assertTrue($path_alias->hasClass('error'));
// Behavior here differs with the inline_form_errors module enabled.
// Enable the inline_form_errors module and try this again. This module
// improves validation with a link in the error message(s) to the fields
// which have invalid input.
$this->assertTrue($this->container->get('module_installer')->install(['inline_form_errors'], TRUE), 'Installed inline_form_errors.');
// Attempt to edit the second node again, as before.
$this->drupalGet('node/' . $node_two->id() . '/edit');
$this->submitForm($edit, 'Preview');
// This error should still be present next to the field.
$this->assertSession()->pageTextContains("The alias {$edit['path[0][alias]']} is already in use in this language.");
// The validation error set for the page should include this text.
$this->assertSession()->statusMessageContains('1 error has been found: URL alias', 'error');
// The text 'URL alias' should be a link.
$this->assertSession()->linkExists('URL alias');
// The link should be to the ID of the URL alias field.
$this->assertSession()->linkByHrefExists('#edit-path-0-alias');
}
}

View File

@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
/**
* Tests path aliases with Content Moderation.
*
* @group content_moderation
* @group path
*/
class PathContentModerationTest extends BrowserTestBase {
use ContentModerationTestTrait;
use ContentTranslationTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'path',
'content_moderation',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
static::createLanguageFromLangcode('fr');
$this->rebuildContainer();
// Created a content type.
$this->drupalCreateContentType([
'name' => 'moderated',
'type' => 'moderated',
]);
// Set the content type as moderated.
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated');
$workflow->save();
$this->drupalLogin($this->drupalCreateUser([
'administer workflows',
'access administration pages',
'administer content types',
'administer content translation',
'administer nodes',
'view latest version',
'view any unpublished content',
'access content overview',
'use editorial transition create_new_draft',
'use editorial transition publish',
'use editorial transition archive',
'use editorial transition archived_draft',
'use editorial transition archived_published',
'administer languages',
'administer site configuration',
'administer url aliases',
'create url aliases',
'view the administration theme',
'translate any entity',
'create content translations',
'create moderated content',
'edit own moderated content',
]));
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
// Enable translation for page.
$this->enableContentTranslation('node', 'moderated');
}
/**
* Tests node path aliases on a moderated content type.
*/
public function testNodePathAlias(): void {
// Create some moderated content with a path alias.
$this->drupalGet('node/add/moderated');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$this->submitForm([
'title[0][value]' => 'moderated content',
'path[0][alias]' => '/moderated-content',
'moderation_state[0][state]' => 'published',
], 'Save');
$node = $this->getNodeByTitle('moderated content');
// Add a pending revision with the same alias.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals('path[0][alias]', '/moderated-content');
$this->submitForm([
'title[0][value]' => 'pending revision',
'path[0][alias]' => '/moderated-content',
'moderation_state[0][state]' => 'draft',
], 'Save');
$this->assertSession()->statusMessageNotContains('You can only change the URL alias for the published version of this content.');
// Create some moderated content with no path alias.
$this->drupalGet('node/add/moderated');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$this->submitForm([
'title[0][value]' => 'moderated content 2',
'path[0][alias]' => '',
'moderation_state[0][state]' => 'published',
], 'Save');
$node = $this->getNodeByTitle('moderated content 2');
// Add a pending revision with a new alias.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$this->submitForm([
'title[0][value]' => 'pending revision',
'path[0][alias]' => '/pending-revision',
'moderation_state[0][state]' => 'draft',
], 'Save');
$this->assertSession()->statusMessageContains('You can only change the URL alias for the published version of this content.', 'error');
// Create some moderated content with no path alias.
$this->drupalGet('node/add/moderated');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$this->submitForm([
'title[0][value]' => 'moderated content 3',
'path[0][alias]' => '',
'moderation_state[0][state]' => 'published',
], 'Save');
$node = $this->getNodeByTitle('moderated content 3');
// Add a pending revision with no path alias.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$this->submitForm([
'title[0][value]' => 'pending revision',
'path[0][alias]' => '',
'moderation_state[0][state]' => 'draft',
], 'Save');
$this->assertSession()->statusMessageNotContains('You can only change the URL alias for the published version of this content.');
}
/**
* Tests that translated and moderated node can get new draft revision.
*/
public function testTranslatedModeratedNodeAlias(): void {
// Create one node with a random alias.
$default_node = $this->drupalCreateNode([
'type' => 'moderated',
'langcode' => 'en',
'moderation_state' => 'published',
'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(),
'moderation_state[0][state]' => 'published',
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->submitForm($edit_translation, 'Save (this translation)');
// Confirm that the alias works.
$this->drupalGet('fr' . $edit_translation['path[0][alias]']);
$this->assertSession()->pageTextContains($edit_translation['body[0][value]']);
$default_path = $default_node->path->alias;
$translation_path = 'fr' . $edit_translation['path[0][alias]'];
$this->assertPathsAreAccessible([$default_path, $translation_path]);
// Try to create new draft revision for translation with a new path alias.
$edit_new_translation_draft_with_alias = [
'moderation_state[0][state]' => 'draft',
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalGet('fr/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft_with_alias, 'Save (this translation)');
// Confirm the expected error.
$this->assertSession()->statusMessageContains('You can only change the URL alias for the published version of this content.', 'error');
// Create new draft revision for translation without changing path alias.
$edit_new_translation_draft = [
'body[0][value]' => $this->randomMachineName(),
'moderation_state[0][state]' => 'draft',
];
$this->drupalGet('fr/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft, 'Save (this translation)');
// Confirm that the new draft revision was created.
$this->assertSession()->statusMessageNotContains('You can only change the URL alias for the published version of this content.');
$this->assertSession()->pageTextContains($edit_new_translation_draft['body[0][value]']);
$this->assertPathsAreAccessible([$default_path, $translation_path]);
// Try to create a new draft revision for translation with path alias from
// the original language's default revision.
$edit_new_translation_draft_with_defaults_alias = [
'moderation_state[0][state]' => 'draft',
'path[0][alias]' => $default_node->path->alias,
];
$this->drupalGet('fr/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft_with_defaults_alias, 'Save (this translation)');
// Verify the expected error.
$this->assertSession()->statusMessageContains('You can only change the URL alias for the published version of this content.', 'error');
// Try to create new draft revision for translation with deleted (empty)
// path alias.
$edit_new_translation_draft_empty_alias = [
'body[0][value]' => $this->randomMachineName(),
'moderation_state[0][state]' => 'draft',
'path[0][alias]' => '',
];
$this->drupalGet('fr/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft_empty_alias, 'Save (this translation)');
// Confirm the expected error.
$this->assertSession()->statusMessageContains('You can only change the URL alias for the published version of this content.', 'error');
// Create new default (published) revision for translation with new path
// alias.
$edit_new_translation = [
'body[0][value]' => $this->randomMachineName(),
'moderation_state[0][state]' => 'published',
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalGet('fr/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation, 'Save (this translation)');
// Confirm that the new published revision was created.
$this->assertSession()->statusMessageNotContains('You can only change the URL alias for the published version of this content.');
$this->assertSession()->pageTextContains($edit_new_translation['body[0][value]']);
$this->assertSession()->addressEquals('fr' . $edit_new_translation['path[0][alias]']);
$this->assertPathsAreAccessible([$default_path]);
}
/**
* Helper callback to verify paths are responding with status 200.
*
* @param string[] $paths
* An array of paths to check for.
*
* @internal
*/
public function assertPathsAreAccessible(array $paths): void {
foreach ($paths as $path) {
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
}
}
}

View File

@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
use Drupal\user\Entity\User;
use Drupal\user\Plugin\LanguageNegotiation\LanguageNegotiationUser;
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
/**
* Confirm that paths work with translated nodes.
*
* @group path
*/
class PathLanguageTest extends PathTestBase {
use ContentTranslationTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'path',
'locale',
'locale_test',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permissions to administer content types.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$permissions = [
'access administration pages',
'administer content translation',
'administer content types',
'administer languages',
'administer url aliases',
'create content translations',
'create page content',
'create url aliases',
'edit any page content',
'translate any entity',
];
// Create and log in user.
$this->webUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->webUser);
// Enable French language.
static::createLanguageFromLangcode('fr');
// Enable URL language detection and selection.
$this->container->get('language_negotiator')->saveConfiguration(LanguageInterface::TYPE_URL, [LanguageNegotiationUrl::METHOD_ID => 1]);
// Enable translation for page node.
static::enableContentTranslation('node', 'page');
static::setFieldTranslatable('node', 'page', 'body', TRUE);
$definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page');
$this->assertTrue($definitions['path']->isTranslatable(), 'Node path is translatable.');
$this->assertTrue($definitions['body']->isTranslatable(), 'Node body is translatable.');
}
/**
* Tests alias functionality through the admin interfaces.
*/
public function testAliasTranslation(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$english_node = $this->drupalCreateNode(['type' => 'page', 'langcode' => 'en']);
$english_alias = $this->randomMachineName();
// Edit the node to set language and path.
$edit = [];
$edit['path[0][alias]'] = '/' . $english_alias;
$this->drupalGet('node/' . $english_node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Confirm that the alias works.
$this->drupalGet($english_alias);
$this->assertSession()->pageTextContains($english_node->body->value);
// Translate the node into French.
$this->drupalGet('node/' . $english_node->id() . '/translations');
$this->clickLink('Add');
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$edit['body[0][value]'] = $this->randomMachineName();
$french_alias = $this->randomMachineName();
$edit['path[0][alias]'] = '/' . $french_alias;
$this->submitForm($edit, 'Save (this translation)');
// Clear the path lookup cache.
$this->container->get('path_alias.manager')->cacheClear();
// Languages are cached on many levels, and we need to clear those caches.
$this->container->get('language_manager')->reset();
$this->rebuildContainer();
$languages = $this->container->get('language_manager')->getLanguages();
// Ensure the node was created.
$english_node = $node_storage->load($english_node->id());
$english_node_french_translation = $english_node->getTranslation('fr');
$this->assertTrue($english_node->hasTranslation('fr'), 'Node found in database.');
// Confirm that the alias works.
$this->drupalGet('fr' . $edit['path[0][alias]']);
$this->assertSession()->pageTextContains($english_node_french_translation->body->value);
// Confirm that the alias is returned for the URL. Languages are cached on
// many levels, and we need to clear those caches.
$this->container->get('language_manager')->reset();
$languages = $this->container->get('language_manager')->getLanguages();
$url = $english_node_french_translation->toUrl('canonical', ['language' => $languages['fr']])->toString();
$this->assertStringContainsString($edit['path[0][alias]'], $url, 'URL contains the path alias.');
// Confirm that the alias works even when changing language negotiation
// options. Enable User language detection and selection over URL one.
$this->container->get('language_negotiator')->saveConfiguration(LanguageInterface::TYPE_INTERFACE, [LanguageNegotiationUser::METHOD_ID => 1]);
$this->container->get('language_negotiator')->saveConfiguration(LanguageInterface::TYPE_URL, [LanguageNegotiationUrl::METHOD_ID => 1]);
// Change user language preference.
$user = User::load($this->webUser->id());
$user->set('preferred_langcode', 'fr');
$user->save();
// Check that the English alias works. In this situation French is the
// current UI and content language, while URL language is English (since we
// do not have a path prefix we fall back to the site's default language).
// We need to ensure that the user language preference is not taken into
// account while determining the path alias language, because if this
// happens we have no way to check that the path alias is valid: there is no
// path alias for French matching the english alias. So the alias manager
// needs to use the URL language to check whether the alias is valid.
$this->drupalGet($english_alias);
$this->assertSession()->pageTextContains($english_node_french_translation->body->value);
// Check that the French alias works.
$this->drupalGet("fr/$french_alias");
$this->assertSession()->pageTextContains($english_node_french_translation->body->value);
// Disable URL language negotiation.
$this->container->get('language_negotiator')->saveConfiguration(LanguageInterface::TYPE_URL, [LanguageNegotiationUrl::METHOD_ID => FALSE]);
// Check that the English alias still works.
$this->drupalGet($english_alias);
$this->assertSession()->pageTextContains($english_node_french_translation->body->value);
// Check that the French alias is not available. We check the unprefixed
// alias because we disabled URL language negotiation above. In this
// situation only aliases in the default language and language neutral ones
// should keep working.
$this->drupalGet($french_alias);
$this->assertSession()->statusCodeEquals(404);
// The alias manager has an internal path lookup cache. Check to see that
// it has the appropriate contents at this point.
$this->container->get('path_alias.manager')->cacheClear();
$french_node_path = $this->container->get('path_alias.manager')->getPathByAlias('/' . $french_alias, 'fr');
$this->assertEquals('/node/' . $english_node_french_translation->id(), $french_node_path, 'Normal path works.');
// Second call should return the same path.
$french_node_path = $this->container->get('path_alias.manager')->getPathByAlias('/' . $french_alias, 'fr');
$this->assertEquals('/node/' . $english_node_french_translation->id(), $french_node_path, 'Normal path is the same.');
// Confirm that the alias works.
$french_node_alias = $this->container->get('path_alias.manager')->getAliasByPath('/node/' . $english_node_french_translation->id(), 'fr');
$this->assertEquals('/' . $french_alias, $french_node_alias, 'Alias works.');
// Second call should return the same alias.
$french_node_alias = $this->container->get('path_alias.manager')->getAliasByPath('/node/' . $english_node_french_translation->id(), 'fr');
$this->assertEquals('/' . $french_alias, $french_node_alias, 'Alias is the same.');
// Confirm that the alias is removed if the translation is deleted.
$english_node->removeTranslation('fr');
$english_node->save();
$this->assertPathAliasNotExists('/' . $french_alias, 'fr', NULL, 'Alias for French translation is removed when translation is deleted.');
// Check that the English alias still works.
$this->drupalGet($english_alias);
$this->assertPathAliasExists('/' . $english_alias, 'en', NULL, 'English alias is not deleted when French translation is removed.');
$this->assertSession()->pageTextContains($english_node->body->value);
}
}

View File

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\Core\Language\LanguageInterface;
/**
* Confirm that the Path module user interface works with languages.
*
* @group path
*/
class PathLanguageUiTest extends PathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['path', 'locale', 'locale_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create and log in user.
$web_user = $this->drupalCreateUser([
'edit any page content',
'create page content',
'administer url aliases',
'create url aliases',
'administer languages',
'access administration pages',
]);
$this->drupalLogin($web_user);
// Enable French language.
$edit = [];
$edit['predefined_langcode'] = 'fr';
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
}
/**
* Tests that a language-neutral URL alias works.
*/
public function testLanguageNeutralUrl(): void {
$name = $this->randomMachineName(8);
$edit = [];
$edit['path[0][value]'] = '/admin/config/search/path';
$edit['alias[0][value]'] = '/' . $name;
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$this->drupalGet($name);
$this->assertSession()->pageTextContains('Filter aliases');
}
/**
* Tests that a default language URL alias works.
*/
public function testDefaultLanguageUrl(): void {
$name = $this->randomMachineName(8);
$edit = [];
$edit['path[0][value]'] = '/admin/config/search/path';
$edit['alias[0][value]'] = '/' . $name;
$edit['langcode[0][value]'] = 'en';
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$this->drupalGet($name);
$this->assertSession()->pageTextContains('Filter aliases');
}
/**
* Tests that a non-default language URL alias works.
*/
public function testNonDefaultUrl(): void {
$name = $this->randomMachineName(8);
$edit = [];
$edit['path[0][value]'] = '/admin/config/search/path';
$edit['alias[0][value]'] = '/' . $name;
$edit['langcode[0][value]'] = 'fr';
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$this->drupalGet('fr/' . $name);
$this->assertSession()->pageTextContains('Filter aliases');
}
/**
* Tests language unspecific aliases are shown and saved in the node form.
*/
public function testNotSpecifiedNode(): void {
// Create test node.
$node = $this->drupalCreateNode();
// Create a language-unspecific alias in the admin UI, ensure that is
// displayed and the langcode is not changed when saving.
$edit = [
'path[0][value]' => '/node/' . $node->id(),
'alias[0][value]' => '/' . $this->getRandomGenerator()->word(8),
'langcode[0][value]' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
];
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$this->drupalGet($node->toUrl('edit-form'));
$this->assertSession()->fieldValueEquals('path[0][alias]', $edit['alias[0][value]']);
$this->submitForm([], 'Save');
$this->drupalGet('admin/config/search/path');
$this->assertSession()->pageTextContains('None');
$this->assertSession()->pageTextNotContains('English');
// Create another node, with no alias, to ensure non-language specific
// aliases are loaded correctly.
$node = $this->drupalCreateNode();
$this->drupalGet($node->toUrl('edit-form'));
$this->submitForm([], 'Save');
$this->assertSession()->pageTextNotContains('The alias is already in use.');
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\media\Entity\MediaType;
/**
* Tests the path media form UI.
*
* @group path
*/
class PathMediaFormTest extends PathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['media', 'media_test_source'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create test user and log in.
$web_user = $this->drupalCreateUser(['create media', 'create url aliases']);
$this->drupalLogin($web_user);
}
/**
* Tests the media form UI.
*/
public function testMediaForm(): void {
$assert_session = $this->assertSession();
// Create media type.
$media_type_id = 'foo';
$media_type = MediaType::create([
'id' => $media_type_id,
'label' => $media_type_id,
'source' => 'test',
'source_configuration' => [],
'field_map' => [],
'new_revision' => FALSE,
]);
$media_type->save();
$this->drupalGet('media/add/' . $media_type_id);
// Make sure we have a vertical tab fieldset and 'Path' field.
$assert_session->elementContains('css', '.js-form-type-vertical-tabs #edit-path-0 summary', 'URL alias');
$assert_session->fieldExists('path[0][alias]');
// Disable the 'Path' field for this content type.
\Drupal::service('entity_display.repository')->getFormDisplay('media', $media_type_id, 'default')
->removeComponent('path')
->save();
$this->drupalGet('media/add/' . $media_type_id);
// See if the whole fieldset is gone now.
$assert_session->elementNotExists('css', '.js-form-type-vertical-tabs #edit-path-0');
$assert_session->fieldNotExists('path[0][alias]');
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
/**
* Tests the Path Node form UI.
*
* @group path
*/
class PathNodeFormTest extends PathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'path', 'path_test_misc'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create test user and log in.
$web_user = $this->drupalCreateUser([
'create page content',
'create url aliases',
]);
$this->drupalLogin($web_user);
}
/**
* Tests the node form ui.
*/
public function testNodeForm(): void {
$assert_session = $this->assertSession();
$this->drupalGet('node/add/page');
// Make sure we have a vertical tab fieldset and 'Path' fields.
$assert_session->elementContains('css', '.js-form-type-vertical-tabs #edit-path-0 summary', 'URL alias');
$assert_session->fieldExists('path[0][alias]');
// Disable the 'Path' field for this content type.
\Drupal::service('entity_display.repository')->getFormDisplay('node', 'page', 'default')
->removeComponent('path')
->save();
$this->drupalGet('node/add/page');
// See if the whole fieldset is gone now.
$assert_session->elementNotExists('css', '.js-form-type-vertical-tabs #edit-path-0');
$assert_session->fieldNotExists('path[0][alias]');
}
/**
* Tests that duplicate path aliases don't get created.
*/
public function testAliasDuplicationPrevention(): void {
$this->drupalGet('node/add/page');
$edit['title[0][value]'] = 'path duplication test';
$edit['path[0][alias]'] = '/my-alias';
$this->submitForm($edit, 'Save');
// Test that PathItem::postSave detects if a path alias exists
// before creating one.
$aliases = \Drupal::entityTypeManager()
->getStorage('path_alias')
->loadMultiple();
static::assertCount(1, $aliases);
$node = Node::load(1);
static::assertInstanceOf(NodeInterface::class, $node);
// This updated title gets set in PathTestMiscHooks::nodePresave. This
// is a way of ensuring that bit of test code runs.
static::assertEquals('path duplication test ran', $node->getTitle());
}
}

View File

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests URL aliases for taxonomy terms.
*
* @group path
*/
class PathTaxonomyTermTest extends PathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['taxonomy'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a Tags vocabulary for the Article node type.
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
]);
$vocabulary->save();
// Create and log in user.
$web_user = $this->drupalCreateUser([
'administer url aliases',
'administer taxonomy',
'access administration pages',
]);
$this->drupalLogin($web_user);
}
/**
* Tests alias functionality through the admin interfaces.
*/
public function testTermAlias(): void {
// Create a term in the default 'Tags' vocabulary with URL alias.
$vocabulary = Vocabulary::load('tags');
$description = $this->randomMachineName();
$edit = [
'name[0][value]' => $this->randomMachineName(),
'description[0][value]' => $description,
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary->id() . '/add');
$this->submitForm($edit, 'Save');
$tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE)
->condition('name', $edit['name[0][value]'])
->condition('default_langcode', 1)
->execute();
$tid = reset($tids);
// Confirm that the alias works.
$this->drupalGet($edit['path[0][alias]']);
$this->assertSession()->pageTextContains($description);
// Confirm the 'canonical' and 'shortlink' URLs.
$this->assertSession()->elementExists('xpath', "//link[contains(@rel, 'canonical') and contains(@href, '" . $edit['path[0][alias]'] . "')]");
$this->assertSession()->elementExists('xpath', "//link[contains(@rel, 'shortlink') and contains(@href, 'taxonomy/term/" . $tid . "')]");
// Change the term's URL alias.
$edit2 = [];
$edit2['path[0][alias]'] = '/' . $this->randomMachineName();
$this->drupalGet('taxonomy/term/' . $tid . '/edit');
$this->submitForm($edit2, 'Save');
// Confirm that the changed alias works.
$this->drupalGet(trim($edit2['path[0][alias]'], '/'));
$this->assertSession()->pageTextContains($description);
// Confirm that the old alias no longer works.
$this->drupalGet(trim($edit['path[0][alias]'], '/'));
$this->assertSession()->pageTextNotContains($description);
$this->assertSession()->statusCodeEquals(404);
// Remove the term's URL alias.
$edit3 = [];
$edit3['path[0][alias]'] = '';
$this->drupalGet('taxonomy/term/' . $tid . '/edit');
$this->submitForm($edit3, 'Save');
// Confirm that the alias no longer works.
$this->drupalGet(trim($edit2['path[0][alias]'], '/'));
$this->assertSession()->pageTextNotContains($description);
$this->assertSession()->statusCodeEquals(404);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Provides a base class for testing the Path module.
*/
abstract class PathTestBase extends BrowserTestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'path'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Functional;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
/**
* Confirm that paths work with node access grants implementations.
*
* @group path
*/
class PathWithNodeAccessGrantsTest extends PathTestBase {
use ContentTranslationTestTrait;
use ContentModerationTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'path',
'locale',
'locale_test',
'content_translation',
'content_moderation',
'path_test_node_grants',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a workflow for basic page.
$workflow = $this->createEditorialWorkflow();
$this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'page');
// Login as admin user to configure language detection and selection.
$admin_user = $this->drupalCreateUser([
'edit any page content',
'create page content',
'administer url aliases',
'create url aliases',
'administer languages',
'access administration pages',
]);
$this->drupalLogin($admin_user);
// Enable French language.
static::createLanguageFromLangcode('fr');
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
// Enable translation for page node.
static::enableContentTranslation('node', 'page');
static::setFieldTranslatable('node', 'page', 'body', TRUE);
$definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page');
$this->assertTrue($definitions['path']->isTranslatable(), 'Node path is translatable.');
$this->assertTrue($definitions['body']->isTranslatable(), 'Node body is translatable.');
}
/**
* Tests alias functionality through the admin interfaces.
*/
public function testAliasTranslation() : void {
// Rebuild the permissions to update 'node_access' table.
node_access_rebuild();
$alias = $this->randomMachineName();
$permissions = [
'access administration pages',
'view any unpublished content',
'use editorial transition create_new_draft',
'use editorial transition publish',
'create content translations',
'create page content',
'create url aliases',
'edit any page content',
'translate any entity',
];
$this->drupalLogin($this->drupalCreateUser($permissions));
// Create a node, add URL alias and publish it.
$this->drupalGet('node/add/page');
$edit['title[0][value]'] = 'test';
$edit['path[0][alias]'] = '/' . $alias;
$edit['moderation_state[0][state]'] = 'published';
$this->submitForm($edit, 'Save');
// Add french translation.
$this->drupalGet('node/1/translations');
$this->clickLink('Add');
$this->submitForm(['moderation_state[0][state]' => 'published'], 'Save (this translation)');
// Translation should be saved.
$this->assertSession()->pageTextContains('Basic page test has been updated.');
// There shouldn't be any validation errors.
$this->assertSession()->pageTextNotContains("Either the path '/node/1' is invalid or you do not have access to it.");
// Translation should be saved with the given alias.
$this->container->get('path_alias.manager')->cacheClear();
$translation_alias = $this->container->get('path_alias.manager')->getAliasByPath('/node/1', 'fr');
$this->assertSame('/' . $alias, $translation_alias);
}
}

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel\Migrate\d6;
use Drupal\path_alias\PathAliasInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\Core\Database\Database;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* URL alias migration.
*
* @group #slow
* @group migrate_drupal_6
*/
class MigrateUrlAliasTest extends MigrateDrupal6TestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'language',
'content_translation',
'path',
'path_alias',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('path_alias');
$this->installConfig(['node']);
$this->installSchema('node', ['node_access']);
$this->migrateUsers(FALSE);
$this->migrateFields();
$this->executeMigrations([
'language',
'd6_node_settings',
]);
}
/**
* Asserts that a path alias matches a set of conditions.
*
* @param int $pid
* The path alias ID.
* @param array $conditions
* The path conditions.
* @param \Drupal\path_alias\PathAliasInterface $path_alias
* The path alias.
*
* @internal
*/
private function assertPath(int $pid, array $conditions, PathAliasInterface $path_alias): void {
$this->assertSame($pid, (int) $path_alias->id());
$this->assertSame($conditions['alias'], $path_alias->getAlias());
$this->assertSame($conditions['langcode'], $path_alias->get('langcode')->value);
$this->assertSame($conditions['path'], $path_alias->getPath());
}
/**
* Tests the URL alias migration.
*/
public function testUrlAlias(): void {
$this->executeMigrations([
'd6_node',
'd6_node_translation',
'd6_url_alias',
]);
$this->checkUrlMigration();
}
/**
* Tests the URL alias migration using the node complete migration.
*/
public function testNodeCompleteUrlAlias(): void {
$this->executeMigrations([
'd6_node_complete',
'd6_url_alias',
]);
$this->checkUrlMigration();
}
/**
* Checks the migration results.
*/
protected function checkUrlMigration(): void {
$id_map = $this->getMigration('d6_url_alias')->getIdMap();
// Test that the field exists.
$conditions = [
'path' => '/node/1',
'alias' => '/alias-one',
'langcode' => 'af',
];
$path_alias = $this->loadPathAliasByConditions($conditions);
$this->assertPath(1, $conditions, $path_alias);
$this->assertSame([['1']], $id_map->lookupDestinationIds([$path_alias->id()]), "Test IdMap");
$conditions = [
'path' => '/node/2',
'alias' => '/alias-two',
'langcode' => 'en',
];
$path_alias = $this->loadPathAliasByConditions($conditions);
$this->assertPath(2, $conditions, $path_alias);
// Test that we can re-import using the UrlAlias destination.
Database::getConnection('default', 'migrate')
->update('url_alias')
->fields(['dst' => 'new-url-alias'])
->condition('src', 'node/2')
->execute();
\Drupal::database()
->update($id_map->mapTableName())
->fields(['source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE])
->execute();
$migration = $this->getMigration('d6_url_alias');
$this->executeMigration($migration);
$path_alias = $this->loadPathAliasByConditions(['id' => $path_alias->id()]);
$conditions['alias'] = '/new-url-alias';
$this->assertPath(2, $conditions, $path_alias);
$conditions = [
'path' => '/node/3',
'alias' => '/alias-three',
'langcode' => 'und',
];
$path_alias = $this->loadPathAliasByConditions($conditions);
$this->assertPath(3, $conditions, $path_alias);
$path_alias = $this->loadPathAliasByConditions(['alias' => '/source-noslash']);
$conditions = [
'path' => '/admin',
'alias' => '/source-noslash',
'langcode' => 'und',
];
$this->assertPath(8, $conditions, $path_alias);
// Tests the URL alias migration with translated nodes.
// Alias for the 'The Real McCoy' node in English.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/the-real-mccoy']);
$this->assertSame('/node/10', $path_alias->getPath());
$this->assertSame('en', $path_alias->get('langcode')->value);
// Alias for the 'The Real McCoy' French translation,
// which should now point to node/10 instead of node/11.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/le-vrai-mccoy']);
$this->assertSame('/node/10', $path_alias->getPath());
$this->assertSame('fr', $path_alias->get('langcode')->value);
// Alias for the 'Abantu zulu' node in Zulu.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/abantu-zulu']);
$this->assertSame('/node/12', $path_alias->getPath());
$this->assertSame('zu', $path_alias->get('langcode')->value);
// Alias for the 'Abantu zulu' English translation,
// which should now point to node/12 instead of node/13.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/the-zulu-people']);
$this->assertSame('/node/12', $path_alias->getPath());
$this->assertSame('en', $path_alias->get('langcode')->value);
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel\Migrate\d7;
/**
* Tests URL alias migration.
*
* @group path
*/
class MigrateUrlAliasNoTranslationTest extends MigrateUrlAliasTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d7_url_alias');
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel\Migrate\d7;
/**
* Tests URL alias migration.
*
* @group path
*/
class MigrateUrlAliasTest extends MigrateUrlAliasTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'path_alias',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'd7_node_translation',
'd7_url_alias',
]);
}
/**
* Tests the URL alias migration with translated nodes.
*/
public function testUrlAliasWithTranslatedNodes(): void {
// Alias for the 'The thing about Deep Space 9' node in English.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/deep-space-9']);
$this->assertSame('/node/2', $path_alias->getPath());
$this->assertSame('en', $path_alias->get('langcode')->value);
// Alias for the 'The thing about Deep Space 9' Icelandic translation,
// which should now point to node/2 instead of node/3.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/deep-space-9-is']);
$this->assertSame('/node/2', $path_alias->getPath());
$this->assertSame('is', $path_alias->get('langcode')->value);
// Alias for the 'The thing about Firefly' node in Icelandic.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/firefly-is']);
$this->assertSame('/node/4', $path_alias->getPath());
$this->assertSame('is', $path_alias->get('langcode')->value);
// Alias for the 'The thing about Firefly' English translation,
// which should now point to node/4 instead of node/5.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/firefly']);
$this->assertSame('/node/4', $path_alias->getPath());
$this->assertSame('en', $path_alias->get('langcode')->value);
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Tests URL alias migration.
*
* @group path
*/
abstract class MigrateUrlAliasTestBase extends MigrateDrupal7TestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'language',
'menu_ui',
'node',
'path',
'path_alias',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('path_alias');
$this->installConfig('node');
$this->installSchema('node', ['node_access']);
$this->migrateUsers(FALSE);
$this->migrateContentTypes();
$this->executeMigrations([
'language',
'd7_node',
]);
}
/**
* Tests the URL alias migration.
*/
public function testUrlAlias(): void {
$path_alias = $this->loadPathAliasByConditions([
'path' => '/taxonomy/term/4',
'alias' => '/term33',
'langcode' => 'und',
]);
$this->assertSame('/taxonomy/term/4', $path_alias->getPath());
$this->assertSame('/term33', $path_alias->getAlias());
$this->assertSame('und', $path_alias->language()->getId());
// Alias with no slash.
$path_alias = $this->loadPathAliasByConditions(['alias' => '/source-noSlash']);
$this->assertSame('/admin', $path_alias->getPath());
$this->assertSame('und', $path_alias->language()->getId());
}
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
// cspell:ignore furchtbar
/**
* Tests loading and storing data using PathItem.
*
* @group path
*/
class PathItemTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'path',
'path_alias',
'node',
'user',
'system',
'language',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('path_alias');
$this->installSchema('node', ['node_access']);
NodeType::create([
'type' => 'foo',
'name' => 'Foo',
])->save();
$this->installConfig(['language']);
ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
* Tests creating, loading, updating and deleting aliases through PathItem.
*/
public function testPathItem(): void {
/** @var \Drupal\path_alias\AliasRepositoryInterface $alias_repository */
$alias_repository = \Drupal::service('path_alias.repository');
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$node = Node::create([
'title' => 'Testing create()',
'type' => 'foo',
'path' => ['alias' => '/foo'],
]);
$this->assertFalse($node->get('path')->isEmpty());
$this->assertEquals('/foo', $node->get('path')->alias);
$node->save();
$this->assertFalse($node->get('path')->isEmpty());
$this->assertEquals('/foo', $node->get('path')->alias);
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_alias['alias']);
$node_storage->resetCache();
/** @var \Drupal\node\NodeInterface $loaded_node */
$loaded_node = $node_storage->load($node->id());
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foo', $loaded_node->get('path')->alias);
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertEquals('/foo', $loaded_node->get('path')[0]->get('alias')->getValue());
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$values = $loaded_node->get('path')->getValue();
$this->assertEquals('/foo', $values[0]['alias']);
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertEquals('/foo', $loaded_node->path->alias);
// Add a translation, verify it is being saved as expected.
$translation = $loaded_node->addTranslation('de', $loaded_node->toArray());
$translation->get('path')->alias = '/furchtbar';
$translation->save();
// Assert the alias on the English node, the German translation, and the
// stored aliases.
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertEquals('/foo', $loaded_node->path->alias);
$translation = $loaded_node->getTranslation('de');
$this->assertEquals('/furchtbar', $translation->path->alias);
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_alias['alias']);
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $translation->language()->getId());
$this->assertEquals('/furchtbar', $stored_alias['alias']);
$loaded_node->get('path')->alias = '/bar';
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$loaded_node->get('path')->alias = '/bar';
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/bar', $stored_alias['alias']);
$old_alias = $alias_repository->lookupByAlias('/foo', $node->language()->getId());
$this->assertNull($old_alias);
// Reload the node to make sure that it is possible to set a value
// immediately after loading.
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$loaded_node->get('path')->alias = '/foobar';
$loaded_node->save();
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foobar', $loaded_node->get('path')->alias);
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foobar', $stored_alias['alias']);
$old_alias = $alias_repository->lookupByAlias('/bar', $node->language()->getId());
$this->assertNull($old_alias);
$loaded_node->get('path')->alias = '';
$this->assertEquals('', $loaded_node->get('path')->alias);
$loaded_node->save();
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertNull($stored_alias);
// Check that reading, updating and reading the computed alias again in the
// same request works without clearing any caches in between.
$loaded_node = $node_storage->load($node->id());
$loaded_node->get('path')->alias = '/foo';
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foo', $loaded_node->get('path')->alias);
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_alias['alias']);
$loaded_node->get('path')->alias = '/foobar';
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foobar', $loaded_node->get('path')->alias);
$stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foobar', $stored_alias['alias']);
// Check that \Drupal\Core\Field\FieldItemList::equals() for the path field
// type.
$node = Node::create([
'title' => $this->randomString(),
'type' => 'foo',
'path' => ['alias' => '/foo'],
]);
$second_node = Node::create([
'title' => $this->randomString(),
'type' => 'foo',
'path' => ['alias' => '/foo'],
]);
$this->assertTrue($node->get('path')->equals($second_node->get('path')));
// Change the alias for the second node to a different one and try again.
$second_node->get('path')->alias = '/foobar';
$this->assertFalse($node->get('path')->equals($second_node->get('path')));
// Test the generateSampleValue() method.
$node = Node::create([
'title' => $this->randomString(),
'type' => 'foo',
'path' => ['alias' => '/foo'],
]);
$node->save();
$path_field = $node->get('path');
$path_field->generateSampleItems();
$node->save();
$this->assertStringStartsWith('/', $node->get('path')->alias);
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel;
use Drupal\content_translation_test\Entity\EntityTestTranslatableUISkip;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests path alias deletion when there is no canonical link template.
*
* @group path
*/
class PathNoCanonicalLinkTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'path',
'content_translation_test',
'language',
'entity_test',
'user',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('entity_test_mul');
// Adding german language.
ConfigurableLanguage::createFromLangcode('de')->save();
$this->config('language.types')->setData([
'configurable' => ['language_interface'],
'negotiation' => ['language_interface' => ['enabled' => ['language-url' => 0]]],
])->save();
}
/**
* Tests for no canonical link templates.
*/
public function testNoCanonicalLinkTemplate(): void {
$entity_type = EntityTestTranslatableUISkip::create([
'name' => 'name english',
'language' => 'en',
]);
$entity_type->save();
$entity_type->addTranslation('de', ['name' => 'name german']);
$entity_type->save();
$this->assertCount(2, $entity_type->getTranslationLanguages());
$entity_type->removeTranslation('de');
$entity_type->save();
$this->assertCount(1, $entity_type->getTranslationLanguages());
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the d6_url_alias source plugin.
*
* @covers \Drupal\path\Plugin\migrate\source\d6\UrlAlias
* @group path
*/
class UrlAliasTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal', 'path'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['url_alias'] = [
[
'pid' => 1,
'src' => 'node/1',
'dst' => 'test-article',
'language' => 'en',
],
[
'pid' => 2,
'src' => 'node/2',
'dst' => 'another-alias',
'language' => 'en',
],
];
// The expected results.
$tests[0]['expected_data'] = $tests[0]['source_data']['url_alias'];
return $tests;
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the d7_url_alias source plugin.
*
* @covers \Drupal\path\Plugin\migrate\source\d7\UrlAlias
* @group path
*/
class UrlAliasTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal', 'path'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['url_alias'] = [
[
'pid' => 1,
'source' => 'node/1',
'alias' => 'test-article',
'language' => 'en',
],
[
'pid' => 2,
'source' => 'node/2',
'alias' => 'another-alias',
'language' => 'en',
],
];
// The expected results.
$tests[0]['expected_data'] = $tests[0]['source_data']['url_alias'];
return $tests;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Unit\Field;
use Drupal\Tests\Core\Field\BaseFieldDefinitionTestBase;
/**
* @coversDefaultClass \Drupal\Core\Field\BaseFieldDefinition
* @group path
*/
class PathFieldDefinitionTest extends BaseFieldDefinitionTestBase {
/**
* {@inheritdoc}
*/
protected function getPluginId(): string {
return 'path';
}
/**
* {@inheritdoc}
*/
protected function getModuleAndPath(): array {
return ['path', dirname(__DIR__, 4)];
}
/**
* @covers ::getColumns
* @covers ::getSchema
*/
public function testGetColumns(): void {
$this->assertSame([], $this->definition->getColumns());
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path\Unit\migrate\process;
use Drupal\path\Plugin\migrate\process\PathSetTranslated;
use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase;
/**
* Tests the path_set_translated process plugin.
*
* @group path
* @coversDefaultClass \Drupal\path\Plugin\migrate\process\PathSetTranslated
*/
class PathSetTranslatedTest extends MigrateProcessTestCase {
/**
* Tests the transform method.
*
* @param string $path
* The path to test.
* @param mixed $node_translation
* The translated node value to test.
* @param string $expected_result
* The expected result.
*
* @covers ::transform
*
* @dataProvider transformDataProvider
*/
public function testTransform($path, $node_translation, $expected_result): void {
$plugin = new PathSetTranslated([], 'path_set_translated', []);
$this->assertSame($expected_result, $plugin->transform([$path, $node_translation], $this->migrateExecutable, $this->row, 'destination_property'));
}
/**
* Provides data for the testTransform method.
*
* @return array
* The data.
*/
public static function transformDataProvider() {
return [
'non-node-path' => [
'path' => '/non-node-path',
'node_translation' => [1, 'en'],
'expected_result' => '/non-node-path',
],
'no_translated_node_1' => [
'path' => '/node/1',
'node_translation' => 'INVALID_NID',
'expected_result' => '/node/1',
],
'no_translated_node_2' => [
'path' => '/node/1',
'node_translation' => NULL,
'expected_result' => '/node/1',
],
'no_translated_node_3' => [
'path' => '/node/1',
'node_translation' => FALSE,
'expected_result' => '/node/1',
],
'valid_transform' => [
'path' => '/node/1',
'node_translation' => [3, 'en'],
'expected_result' => '/node/3',
],
];
}
}