Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

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,39 @@
id: d6_url_alias
label: URL aliases
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_url_alias
constants:
slash: '/'
process:
source:
plugin: concat
source:
- constants/slash
- src
alias:
plugin: concat
source:
- constants/slash
- dst
langcode:
plugin: d6_url_alias_language
source: language
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_translation
destination:
plugin: url_alias

View file

@ -0,0 +1,37 @@
id: d7_url_alias
label: URL aliases
migration_tags:
- Drupal 7
- Content
source:
plugin: d7_url_alias
constants:
slash: '/'
process:
source:
plugin: concat
source:
- constants/slash
- source
alias:
plugin: concat
source:
- constants/slash
- alias
langcode: language
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
destination:
plugin: url_alias

View file

@ -0,0 +1,66 @@
<?php
/**
* @file
* Hooks provided by the Path module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Respond to a path being inserted.
*
* @param array $path
* The array structure is identical to that of the return value of
* \Drupal\Core\Path\AliasStorageInterface::save().
*
* @see \Drupal\Core\Path\AliasStorageInterface::save()
*/
function hook_path_insert($path) {
db_insert('mytable')
->fields([
'alias' => $path['alias'],
'pid' => $path['pid'],
])
->execute();
}
/**
* Respond to a path being updated.
*
* @param array $path
* The array structure is identical to that of the return value of
* \Drupal\Core\Path\AliasStorageInterface::save().
*
* @see \Drupal\Core\Path\AliasStorageInterface::save()
*/
function hook_path_update($path) {
if ($path['alias'] != $path['original']['alias']) {
db_update('mytable')
->fields(['alias' => $path['alias']])
->condition('pid', $path['pid'])
->execute();
}
}
/**
* Respond to a path being deleted.
*
* @param array $path
* The array structure is identical to that of the return value of
* \Drupal\Core\Path\AliasStorageInterface::save().
*
* @see \Drupal\Core\Path\AliasStorageInterface::delete()
*/
function hook_path_delete($path) {
db_delete('mytable')
->condition('pid', $path['pid'])
->execute();
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,27 @@
/**
* @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 path = $('.js-form-item-path-0-alias input').val();
return path
? Drupal.t('Alias: @alias', { '@alias': path })
: Drupal.t('No alias');
});
},
};
})(jQuery, Drupal);

View file

@ -0,0 +1,7 @@
name: Path
type: module
description: 'Allows users to rename URLs.'
package: Core
version: VERSION
core: 8.x
configure: path.admin_overview

View file

@ -0,0 +1,23 @@
<?php
/**
* @file
* Update functions for the path module.
*/
/**
* Change the path field to computed for node and taxonomy_term.
*/
function path_update_8200() {
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
foreach (['node', 'taxonomy_term'] as $entity_type_id) {
if ($entity_definition_update_manager->getEntityType($entity_type_id)) {
// Computed field definitions are not tracked by the entity definition
// update manager, so remove them.
$storage_definition = $entity_definition_update_manager->getFieldStorageDefinition('path', $entity_type_id);
if ($storage_definition) {
$entity_definition_update_manager->uninstallFieldStorageDefinition($storage_definition);
}
}
}
}

View file

@ -0,0 +1,18 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal) {
Drupal.behaviors.pathDetailsSummaries = {
attach: function attach(context) {
$(context).find('.path-form').drupalSetSummary(function (context) {
var path = $('.js-form-item-path-0-alias input').val();
return path ? Drupal.t('Alias: @alias', { '@alias': path }) : Drupal.t('No alias');
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,8 @@
drupal.path:
version: VERSION
js:
path.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupal.form

View file

@ -0,0 +1,5 @@
path.admin_add:
route_name: path.admin_add
title: 'Add alias'
appears_on:
- path.admin_overview

View file

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

View file

@ -0,0 +1,4 @@
path.admin_overview:
title: List
route_name: path.admin_overview
base_route: path.admin_overview

View file

@ -0,0 +1,70 @@
<?php
/**
* @file
* Enables users to rename URLs.
*/
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function path_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.path':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . 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 .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Creating aliases') . '</dt>';
$output .= '<dd>' . 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' => \Drupal::url('path.admin_overview'), ':permissions' => \Drupal::url('user.admin_permissions', [], ['fragment' => 'module-path'])]) . '</dd>';
$output .= '<dt>' . t('Managing aliases') . '</dt>';
$output .= '<dd>' . 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' => \Drupal::url('path.admin_overview')]) . '</dd>';
$output .= '</dl>';
return $output;
case 'path.admin_overview':
return '<p>' . 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 'path.admin_add':
return '<p>' . t('Enter the path you wish to create the alias for, followed by the name of the new alias.') . '</p>';
}
}
/**
* Implements hook_entity_base_field_info().
*/
function path_entity_base_field_info(EntityTypeInterface $entity_type) {
if (in_array($entity_type->id(), ['taxonomy_term', 'node', 'media'], TRUE)) {
$fields['path'] = BaseFieldDefinition::create('path')
->setLabel(t('URL alias'))
->setTranslatable(TRUE)
->setDisplayOptions('form', [
'type' => 'path',
'weight' => 30,
])
->setDisplayConfigurable('form', TRUE)
->setComputed(TRUE);
return $fields;
}
}
/**
* Implements hook_entity_translation_create().
*/
function path_entity_translation_create(ContentEntityInterface $translation) {
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;
}
}
}

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,40 @@
path.delete:
path: '/admin/config/search/path/delete/{pid}'
defaults:
_form: '\Drupal\path\Form\DeleteForm'
_title: 'Delete alias'
requirements:
_permission: 'administer url aliases'
path.admin_overview:
path: '/admin/config/search/path'
defaults:
_title: 'URL aliases'
_controller: '\Drupal\path\Controller\PathController::adminOverview'
keys: NULL
requirements:
_permission: 'administer url aliases'
path.admin_overview_filter:
path: '/admin/config/search/path/filter'
defaults:
_title: 'URL aliases'
_controller: '\Drupal\path\Controller\PathController::adminOverview'
requirements:
_permission: 'administer url aliases'
path.admin_add:
path: '/admin/config/search/path/add'
defaults:
_title: 'Add alias'
_form: '\Drupal\path\Form\AddForm'
requirements:
_permission: 'administer url aliases'
path.admin_edit:
path: '/admin/config/search/path/edit/{pid}'
defaults:
_title: 'Edit alias'
_form: '\Drupal\path\Form\EditForm'
requirements:
_permission: 'administer url aliases'

View file

@ -0,0 +1,134 @@
<?php
namespace Drupal\path\Controller;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Path\AliasStorageInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller routines for path routes.
*/
class PathController extends ControllerBase {
/**
* The path alias storage.
*
* @var \Drupal\Core\Path\AliasStorageInterface
*/
protected $aliasStorage;
/**
* The path alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* Constructs a new PathController.
*
* @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
* The path alias storage.
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* The path alias manager.
*/
public function __construct(AliasStorageInterface $alias_storage, AliasManagerInterface $alias_manager) {
$this->aliasStorage = $alias_storage;
$this->aliasManager = $alias_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('path.alias_storage'),
$container->get('path.alias_manager')
);
}
/**
* Displays the path administration overview page.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function adminOverview(Request $request) {
$keys = $request->query->get('search');
// Add the filter form above the overview table.
$build['path_admin_filter_form'] = $this->formBuilder()->getForm('Drupal\path\Form\PathFilterForm', $keys);
// Enable language column if language.module is enabled or if we have any
// alias with a language.
$multilanguage = ($this->moduleHandler()->moduleExists('language') || $this->aliasStorage->languageAliasExists());
$header = [];
$header[] = ['data' => $this->t('Alias'), 'field' => 'alias', 'sort' => 'asc'];
$header[] = ['data' => $this->t('System'), 'field' => 'source'];
if ($multilanguage) {
$header[] = ['data' => $this->t('Language'), 'field' => 'langcode'];
}
$header[] = $this->t('Operations');
$rows = [];
$destination = $this->getDestinationArray();
foreach ($this->aliasStorage->getAliasesForAdminListing($header, $keys) as $data) {
$row = [];
// @todo Should Path module store leading slashes? See
// https://www.drupal.org/node/2430593.
$row['data']['alias'] = $this->l(Unicode::truncate($data->alias, 50, FALSE, TRUE), Url::fromUserInput($data->source, [
'attributes' => ['title' => $data->alias],
]));
$row['data']['source'] = $this->l(Unicode::truncate($data->source, 50, FALSE, TRUE), Url::fromUserInput($data->source, [
'alias' => TRUE,
'attributes' => ['title' => $data->source],
]));
if ($multilanguage) {
$row['data']['language_name'] = $this->languageManager()->getLanguageName($data->langcode);
}
$operations = [];
$operations['edit'] = [
'title' => $this->t('Edit'),
'url' => Url::fromRoute('path.admin_edit', ['pid' => $data->pid], ['query' => $destination]),
];
$operations['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('path.delete', ['pid' => $data->pid], ['query' => $destination]),
];
$row['data']['operations'] = [
'data' => [
'#type' => 'operations',
'#links' => $operations,
],
];
// If the system path maps to a different URL alias, highlight this table
// row to let the user know of old aliases.
if ($data->alias != $this->aliasManager->getAliasByPath($data->source, $data->langcode)) {
$row['class'] = ['warning'];
}
$rows[] = $row;
}
$build['path_table'] = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No URL aliases available. <a href=":link">Add URL alias</a>.', [':link' => $this->url('path.admin_add')]),
];
$build['path_pager'] = ['#type' => 'pager'];
return $build;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\path\Form;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides the path add form.
*
* @internal
*/
class AddForm extends PathFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'path_admin_add';
}
/**
* {@inheritdoc}
*/
protected function buildPath($pid) {
return [
'source' => '',
'alias' => '',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'pid' => NULL,
];
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\path\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Path\AliasStorageInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds the form to delete a path alias.
*
* @internal
*/
class DeleteForm extends ConfirmFormBase {
/**
* The alias storage service.
*
* @var \Drupal\Core\Path\AliasStorageInterface
*/
protected $aliasStorage;
/**
* The path alias being deleted.
*
* @var array
*/
protected $pathAlias;
/**
* Constructs a \Drupal\path\Form\DeleteForm object.
*
* @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
* The alias storage service.
*/
public function __construct(AliasStorageInterface $alias_storage) {
$this->aliasStorage = $alias_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('path.alias_storage')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'path_alias_delete';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to delete path alias %title?', ['%title' => $this->pathAlias['alias']]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('path.admin_overview');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $pid = NULL) {
$this->pathAlias = $this->aliasStorage->load(['pid' => $pid]);
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->aliasStorage->delete(['pid' => $this->pathAlias['pid']]);
$form_state->setRedirect('path.admin_overview');
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\path\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the path edit form.
*
* @internal
*/
class EditForm extends PathFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'path_admin_edit';
}
/**
* {@inheritdoc}
*/
protected function buildPath($pid) {
return $this->aliasStorage->load(['pid' => $pid]);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $pid = NULL) {
$form = parent::buildForm($form, $form_state, $pid);
$form['#title'] = $this->path['alias'];
$form['pid'] = [
'#type' => 'hidden',
'#value' => $this->path['pid'],
];
$url = new Url('path.delete', [
'pid' => $this->path['pid'],
]);
if ($this->getRequest()->query->has('destination')) {
$url->setOption('query', $this->getDestinationArray());
}
$form['actions']['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
'#url' => $url,
'#attributes' => [
'class' => ['button', 'button--danger'],
],
];
return $form;
}
}

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('path.admin_overview_filter', [], [
'query' => ['search' => trim($form_state->getValue('filter'))],
]);
}
/**
* Resets the filter selections.
*/
public function resetForm(array &$form, FormStateInterface $form_state) {
$form_state->setRedirect('path.admin_overview');
}
}

View file

@ -0,0 +1,219 @@
<?php
namespace Drupal\path\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Path\AliasStorageInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Routing\RequestContext;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for path add/edit forms.
*/
abstract class PathFormBase extends FormBase {
/**
* An array containing the path ID, source, alias, and language code.
*
* @var array
*/
protected $path;
/**
* The path alias storage.
*
* @var \Drupal\Core\Path\AliasStorageInterface
*/
protected $aliasStorage;
/**
* The path alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* The request context.
*
* @var \Drupal\Core\Routing\RequestContext
*/
protected $requestContext;
/**
* Constructs a new PathController.
*
* @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
* The path alias storage.
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* The path alias manager.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The path validator.
* @param \Drupal\Core\Routing\RequestContext $request_context
* The request context.
*/
public function __construct(AliasStorageInterface $alias_storage, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, RequestContext $request_context) {
$this->aliasStorage = $alias_storage;
$this->aliasManager = $alias_manager;
$this->pathValidator = $path_validator;
$this->requestContext = $request_context;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('path.alias_storage'),
$container->get('path.alias_manager'),
$container->get('path.validator'),
$container->get('router.request_context')
);
}
/**
* Builds the path used by the form.
*
* @param int|null $pid
* Either the unique path ID, or NULL if a new one is being created.
*/
abstract protected function buildPath($pid);
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $pid = NULL) {
$this->path = $this->buildPath($pid);
$form['source'] = [
'#type' => 'textfield',
'#title' => $this->t('Existing system path'),
'#default_value' => $this->path['source'],
'#maxlength' => 255,
'#size' => 45,
'#description' => $this->t('Specify the existing path you wish to alias. For example: /node/28, /forum/1, /taxonomy/term/1.'),
'#field_prefix' => $this->requestContext->getCompleteBaseUrl(),
'#required' => TRUE,
];
$form['alias'] = [
'#type' => 'textfield',
'#title' => $this->t('Path alias'),
'#default_value' => $this->path['alias'],
'#maxlength' => 255,
'#size' => 45,
'#description' => $this->t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.'),
'#field_prefix' => $this->requestContext->getCompleteBaseUrl(),
'#required' => TRUE,
];
// A hidden value unless language.module is enabled.
if (\Drupal::moduleHandler()->moduleExists('language')) {
$languages = \Drupal::languageManager()->getLanguages();
$language_options = [];
foreach ($languages as $langcode => $language) {
$language_options[$langcode] = $language->getName();
}
$form['langcode'] = [
'#type' => 'select',
'#title' => $this->t('Language'),
'#options' => $language_options,
'#empty_value' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'#empty_option' => $this->t('- None -'),
'#default_value' => $this->path['langcode'],
'#weight' => -10,
'#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>- None -</em>.'),
];
}
else {
$form['langcode'] = [
'#type' => 'value',
'#value' => $this->path['langcode'],
];
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$source = &$form_state->getValue('source');
$source = $this->aliasManager->getPathByAlias($source);
$alias = &$form_state->getValue('alias');
// Trim the submitted value of whitespace and slashes. Ensure to not trim
// the slash on the left side.
$alias = rtrim(trim(trim($alias), ''), "\\/");
if ($source[0] !== '/') {
$form_state->setErrorByName('source', 'The source path has to start with a slash.');
}
if ($alias[0] !== '/') {
$form_state->setErrorByName('alias', 'The alias path has to start with a slash.');
}
// Language is only set if language.module is enabled, otherwise save for all
// languages.
$langcode = $form_state->getValue('langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED);
if ($this->aliasStorage->aliasExists($alias, $langcode, $this->path['source'])) {
$stored_alias = $this->aliasStorage->load(['alias' => $alias, 'langcode' => $langcode]);
if ($stored_alias['alias'] !== $alias) {
// The alias already exists with different capitalization as the default
// implementation of AliasStorageInterface::aliasExists is
// case-insensitive.
$form_state->setErrorByName('alias', t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [
'%alias' => $alias,
'%stored_alias' => $stored_alias['alias'],
]));
}
else {
$form_state->setErrorByName('alias', t('The alias %alias is already in use in this language.', ['%alias' => $alias]));
}
}
if (!$this->pathValidator->isValid(trim($source, '/'))) {
$form_state->setErrorByName('source', t("Either the path '@link_path' is invalid or you do not have access to it.", ['@link_path' => $source]));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Remove unnecessary values.
$form_state->cleanValues();
$pid = $form_state->getValue('pid', 0);
$source = $form_state->getValue('source');
$alias = $form_state->getValue('alias');
// Language is only set if language.module is enabled, otherwise save for all
// languages.
$langcode = $form_state->getValue('langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED);
$this->aliasStorage->save($source, $alias, $langcode, $pid);
$this->messenger()->addStatus($this->t('The alias has been saved.'));
$form_state->setRedirect('path.admin_overview');
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\path\Plugin\Field\FieldType;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Language\LanguageInterface;
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()) {
$conditions = [
'source' => '/' . $entity->toUrl()->getInternalPath(),
'langcode' => $this->getLangcode(),
];
$alias = \Drupal::service('path.alias_storage')->load($conditions);
if ($alias === FALSE) {
// Fall back to non-specific language.
if ($this->getLangcode() !== LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$conditions['langcode'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
$alias = \Drupal::service('path.alias_storage')->load($conditions);
}
}
if ($alias) {
$value = $alias;
}
}
$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();
$conditions = [
'source' => '/' . $entity->toUrl()->getInternalPath(),
'langcode' => $entity->language()->getId(),
];
\Drupal::service('path.alias_storage')->delete($conditions);
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace Drupal\path\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\TypedData\DataDefinition;
/**
* Defines the 'path' entity field type.
*
* @FieldType(
* id = "path",
* label = @Translation("Path"),
* description = @Translation("An entity field containing a path alias and related data."),
* no_ui = TRUE,
* default_widget = "path",
* list_class = "\Drupal\path\Plugin\Field\FieldType\PathFieldItemList",
* 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() {
return ($this->alias === NULL || $this->alias === '') && ($this->pid === NULL || $this->pid === '') && ($this->langcode === NULL || $this->langcode === '');
}
/**
* {@inheritdoc}
*/
public function preSave() {
if ($this->alias !== NULL) {
$this->alias = trim($this->alias);
}
}
/**
* {@inheritdoc}
*/
public function postSave($update) {
// If specified, rely on the langcode property for the language, so that the
// existing language of an alias can be kept. That could for example be
// unspecified even if the field/entity has a specific langcode.
$alias_langcode = ($this->langcode && $this->pid) ? $this->langcode : $this->getLangcode();
if (!$update) {
if ($this->alias) {
$entity = $this->getEntity();
if ($path = \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $alias_langcode)) {
$this->pid = $path['pid'];
}
}
}
else {
// Delete old alias if user erased it.
if ($this->pid && !$this->alias) {
\Drupal::service('path.alias_storage')->delete(['pid' => $this->pid]);
}
// Only save a non-empty alias.
elseif ($this->alias) {
$entity = $this->getEntity();
\Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $alias_langcode, $this->pid);
}
}
}
/**
* {@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,109 @@
<?php
namespace Drupal\path\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Plugin implementation of the 'path' widget.
*
* @FieldWidget(
* id = "path",
* label = @Translation("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' => [[get_class($this), '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' => 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 (!empty($alias)) {
$form_state->setValueForElement($element['alias'], $alias);
// Validate that the submitted alias does not exist yet.
$is_exists = \Drupal::service('path.alias_storage')->aliasExists($alias, $element['langcode']['#value'], $element['source']['#value']);
if ($is_exists) {
$form_state->setError($element['alias'], t('The alias is already in use.'));
}
}
if ($alias && $alias[0] !== '/') {
$form_state->setError($element['alias'], t('The alias needs to start with a slash.'));
}
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
return $element['alias'];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\path\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Validation constraint for changing path aliases in pending revisions.
*
* @Constraint(
* id = "PathAlias",
* label = @Translation("Path alias.", context = "Validation"),
* )
*/
class PathAliasConstraint extends Constraint {
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) {
$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,101 @@
<?php
namespace Drupal\path\Plugin\migrate\destination;
use Drupal\Core\Path\AliasStorage;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* @MigrateDestination(
* id = "url_alias"
* )
*/
class UrlAlias extends DestinationBase implements ContainerFactoryPluginInterface {
/**
* The alias storage service.
*
* @var \Drupal\Core\Path\AliasStorage
*/
protected $aliasStorage;
/**
* Constructs an entity destination plugin.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Path\AliasStorage $alias_storage
* The alias storage service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, AliasStorage $alias_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->aliasStorage = $alias_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('path.alias_storage')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$source = $row->getDestinationProperty('source');
$alias = $row->getDestinationProperty('alias');
$langcode = $row->getDestinationProperty('langcode');
$pid = $old_destination_id_values ? $old_destination_id_values[0] : NULL;
// Check if this alias is for a node and if that node is a translation.
if (preg_match('/^\/node\/\d+$/', $source) && $row->hasDestinationProperty('node_translation')) {
// Replace the alias source with the translation source path.
$node_translation = $row->getDestinationProperty('node_translation');
$source = '/node/' . $node_translation[0];
$langcode = $node_translation[1];
}
$path = $this->aliasStorage->save($source, $alias, $langcode, $pid);
return [$path['pid']];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['pid']['type'] = 'integer';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
return [
'pid' => 'The path id',
'source' => 'The source path.',
'alias' => 'The URL alias.',
'langcode' => 'The language code for the URL.',
];
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\path\Plugin\migrate\process\d6;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Drupal\Core\Language\LanguageInterface;
/**
* Url alias language code process.
*
* @MigrateProcessPlugin(
* id = "d6_url_alias_language"
* )
*/
class UrlAliasLanguage extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$langcode = ($value === '') ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $value;
return $langcode;
}
}

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\Core\Path\AliasStorage::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,27 @@
<?php
namespace Drupal\path\Plugin\migrate\source\d6;
use Drupal\path\Plugin\migrate\source\UrlAliasBase;
/**
* URL aliases source from database.
*
* @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,27 @@
<?php
namespace Drupal\path\Plugin\migrate\source\d7;
use Drupal\path\Plugin\migrate\source\UrlAliasBase;
/**
* URL aliases source from database.
*
* @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,32 @@
<?php
namespace Drupal\path\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Provides a base class for testing the Path module.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use \Drupal\Tests\path\Functional\PathTestBase instead.
*/
abstract class PathTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'path'];
protected function setUp() {
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,100 @@
<?php
namespace Drupal\Tests\path\Functional;
/**
* Tests the Path admin UI.
*
* @group path
*/
class PathAdminTest extends PathTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path'];
protected function setUp() {
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() {
// Create test nodes.
$node1 = $this->drupalCreateNode();
$node2 = $this->drupalCreateNode();
$node3 = $this->drupalCreateNode();
// Create aliases.
$alias1 = '/' . $this->randomMachineName(8);
$edit = [
'source' => '/node/' . $node1->id(),
'alias' => $alias1,
];
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$alias2 = '/' . $this->randomMachineName(8);
$edit = [
'source' => '/node/' . $node2->id(),
'alias' => $alias2,
];
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$alias3 = '/' . $this->randomMachineName(4) . '/' . $this->randomMachineName(4);
$edit = [
'source' => '/node/' . $node3->id(),
'alias' => $alias3,
];
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
// Filter by the first alias.
$edit = [
'filter' => $alias1,
];
$this->drupalPostForm(NULL, $edit, t('Filter'));
$this->assertLinkByHref($alias1);
$this->assertNoLinkByHref($alias2);
$this->assertNoLinkByHref($alias3);
// Filter by the second alias.
$edit = [
'filter' => $alias2,
];
$this->drupalPostForm(NULL, $edit, t('Filter'));
$this->assertNoLinkByHref($alias1);
$this->assertLinkByHref($alias2);
$this->assertNoLinkByHref($alias3);
// Filter by the third alias which has a slash.
$edit = [
'filter' => $alias3,
];
$this->drupalPostForm(NULL, $edit, t('Filter'));
$this->assertNoLinkByHref($alias1);
$this->assertNoLinkByHref($alias2);
$this->assertLinkByHref($alias3);
// Filter by a random string with a different length.
$edit = [
'filter' => $this->randomMachineName(10),
];
$this->drupalPostForm(NULL, $edit, t('Filter'));
$this->assertNoLinkByHref($alias1);
$this->assertNoLinkByHref($alias2);
// Reset the filter.
$edit = [];
$this->drupalPostForm(NULL, $edit, t('Reset'));
$this->assertLinkByHref($alias1);
$this->assertLinkByHref($alias2);
}
}

View file

@ -0,0 +1,407 @@
<?php
namespace Drupal\Tests\path\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
/**
* Add, edit, delete, and change alias and verify its consistency in the
* database.
*
* @group path
*/
class PathAliasTest extends PathTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path'];
protected function setUp() {
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);
}
/**
* Tests the path cache.
*/
public function testPathCache() {
// Create test node.
$node1 = $this->drupalCreateNode();
// Create alias.
$edit = [];
$edit['source'] = '/node/' . $node1->id();
$edit['alias'] = '/' . $this->randomMachineName(8);
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
// Check the path alias whitelist cache.
$whitelist = \Drupal::cache('bootstrap')->get('path_alias_whitelist');
$this->assertTrue($whitelist->data['node']);
$this->assertFalse($whitelist->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['source'], '/'), ['alias' => TRUE]);
$this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), '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'], '/'));
$this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.');
}
/**
* Tests alias functionality through the admin interfaces.
*/
public function testAdminAlias() {
// Create test node.
$node1 = $this->drupalCreateNode();
// Create alias.
$edit = [];
$edit['source'] = '/node/' . $node1->id();
$edit['alias'] = '/' . $this->getRandomGenerator()->word(8);
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
// Confirm that the alias works.
$this->drupalGet($edit['alias']);
$this->assertText($node1->label(), 'Alias works.');
$this->assertResponse(200);
// Confirm that the alias works in a case-insensitive way.
$this->assertTrue(ctype_lower(ltrim($edit['alias'], '/')));
$this->drupalGet($edit['alias']);
$this->assertText($node1->label(), 'Alias works lower case.');
$this->assertResponse(200);
$this->drupalGet(mb_strtoupper($edit['alias']));
$this->assertText($node1->label(), 'Alias works upper case.');
$this->assertResponse(200);
// Change alias to one containing "exotic" characters.
$pid = $this->getPID($edit['alias']);
$previous = $edit['alias'];
// Lower-case letters.
$edit['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
$edit['alias'] .= "ïвβéø";
}
$this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save'));
// Confirm that the alias works.
$this->drupalGet(mb_strtoupper($edit['alias']));
$this->assertText($node1->label(), 'Changed alias works.');
$this->assertResponse(200);
$this->container->get('path.alias_manager')->cacheClear();
// Confirm that previous alias no longer works.
$this->drupalGet($previous);
$this->assertNoText($node1->label(), 'Previous alias no longer works.');
$this->assertResponse(404);
// Create second test node.
$node2 = $this->drupalCreateNode();
// Set alias to second test node.
$edit['source'] = '/node/' . $node2->id();
// leave $edit['alias'] the same
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
// Confirm no duplicate was created.
$this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias']]), 'Attempt to move alias was rejected.');
$edit_upper = $edit;
$edit_upper['alias'] = mb_strtoupper($edit['alias']);
$this->drupalPostForm('admin/config/search/path/add', $edit_upper, t('Save'));
$this->assertRaw(t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [
'%alias' => $edit_upper['alias'],
'%stored_alias' => $edit['alias'],
]), 'Attempt to move upper-case alias was rejected.');
// Delete alias.
$this->drupalGet('admin/config/search/path/edit/' . $pid);
$this->clickLink(t('Delete'));
$this->assertRaw(t('Are you sure you want to delete path alias %name?', ['%name' => $edit['alias']]));
$this->drupalPostForm(NULL, [], t('Confirm'));
// Confirm that the alias no longer works.
$this->drupalGet($edit['alias']);
$this->assertNoText($node1->label(), 'Alias was successfully deleted.');
$this->assertResponse(404);
// Create a really long alias.
$edit = [];
$edit['source'] = '/node/' . $node1->id();
$alias = '/' . $this->randomMachineName(128);
$edit['alias'] = $alias;
// The alias is shortened to 50 characters counting the ellipsis.
$truncated_alias = substr($alias, 0, 47);
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$this->assertNoText($alias, 'The untruncated alias was not found.');
// The 'truncated' alias will always be found.
$this->assertText($truncated_alias, 'The truncated alias was found.');
// Create third test node.
$node3 = $this->drupalCreateNode();
// Create absolute path alias.
$edit = [];
$edit['source'] = '/node/' . $node3->id();
$node3_alias = '/' . $this->randomMachineName(8);
$edit['alias'] = $node3_alias;
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
// Create fourth test node.
$node4 = $this->drupalCreateNode();
// Create alias with trailing slash.
$edit = [];
$edit['source'] = '/node/' . $node4->id();
$node4_alias = '/' . $this->randomMachineName(8);
$edit['alias'] = $node4_alias . '/';
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
// Confirm that the alias with trailing slash is not found.
$this->assertNoText($edit['alias'], 'The absolute alias was not found.');
// The alias without trailing flash is found.
$this->assertText(trim($edit['alias'], '/'), 'The alias without trailing slash was found.');
// Update an existing alias to point to a different source.
$pid = $this->getPID($node4_alias);
$edit = [];
$edit['alias'] = $node4_alias;
$edit['source'] = '/node/' . $node2->id();
$this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save'));
$this->assertText('The alias has been saved.');
$this->drupalGet($edit['alias']);
$this->assertNoText($node4->label(), 'Previous alias no longer works.');
$this->assertText($node2->label(), 'Alias works.');
$this->assertResponse(200);
// Update an existing alias to use a duplicate alias.
$pid = $this->getPID($node3_alias);
$edit = [];
$edit['alias'] = $node4_alias;
$edit['source'] = '/node/' . $node3->id();
$this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save'));
$this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias']]));
// Create an alias without a starting slash.
$node5 = $this->drupalCreateNode();
$edit = [];
$edit['source'] = 'node/' . $node5->id();
$node5_alias = $this->randomMachineName(8);
$edit['alias'] = $node5_alias . '/';
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$this->assertUrl('admin/config/search/path/add');
$this->assertText('The source path has to start with a slash.');
$this->assertText('The alias path has to start with a slash.');
}
/**
* Tests alias functionality through the node interfaces.
*/
public function testNodeAlias() {
// Create test node.
$node1 = $this->drupalCreateNode();
// Create alias.
$edit = [];
$edit['path[0][alias]'] = '/' . $this->randomMachineName(8);
$this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save'));
// Confirm that the alias works.
$this->drupalGet($edit['path[0][alias]']);
$this->assertText($node1->label(), 'Alias works.');
$this->assertResponse(200);
// Confirm the 'canonical' and 'shortlink' URLs.
$elements = $this->xpath("//link[contains(@rel, 'canonical') and contains(@href, '" . $edit['path[0][alias]'] . "')]");
$this->assertTrue(!empty($elements), 'Page contains canonical link URL.');
$elements = $this->xpath("//link[contains(@rel, 'shortlink') and contains(@href, 'node/" . $node1->id() . "')]");
$this->assertTrue(!empty($elements), 'Page contains shortlink URL.');
$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
$edit['path[0][alias]'] .= "ïвβéø";
}
$this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save'));
// Confirm that the alias works.
$this->drupalGet(mb_strtoupper($edit['path[0][alias]']));
$this->assertText($node1->label(), 'Changed alias works.');
$this->assertResponse(200);
// Make sure that previous alias no longer works.
$this->drupalGet($previous);
$this->assertNoText($node1->label(), 'Previous alias no longer works.');
$this->assertResponse(404);
// Create second test node.
$node2 = $this->drupalCreateNode();
// Set alias to second test node.
// Leave $edit['path[0][alias]'] the same.
$this->drupalPostForm('node/' . $node2->id() . '/edit', $edit, t('Save'));
// Confirm that the alias didn't make a duplicate.
$this->assertText(t('The alias is already in use.'), 'Attempt to moved alias was rejected.');
// Delete alias.
$this->drupalPostForm('node/' . $node1->id() . '/edit', ['path[0][alias]' => ''], t('Save'));
// Confirm that the alias no longer works.
$this->drupalGet($edit['path[0][alias]']);
$this->assertNoText($node1->label(), 'Alias was successfully deleted.');
$this->assertResponse(404);
// Create third test node.
$node3 = $this->drupalCreateNode();
// Set its path alias to an absolute path.
$edit = ['path[0][alias]' => '/' . $this->randomMachineName(8)];
$this->drupalPostForm('node/' . $node3->id() . '/edit', $edit, t('Save'));
// Confirm that the alias was converted to a relative path.
$this->drupalGet(trim($edit['path[0][alias]'], '/'));
$this->assertText($node3->label(), 'Alias became relative.');
$this->assertResponse(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->drupalPostForm('node/' . $node4->id() . '/edit', $edit, t('Save'));
// Confirm that the alias was converted to a relative path.
$this->drupalGet(trim($edit['path[0][alias]'], '/'));
$this->assertText($node4->label(), 'Alias trimmed trailing slash.');
$this->assertResponse(200);
// Create fifth test node.
$node5 = $this->drupalCreateNode();
// Set a path alias.
$edit = ['path[0][alias]' => '/' . $this->randomMachineName(8)];
$this->drupalPostForm('node/' . $node5->id() . '/edit', $edit, t('Save'));
// Delete the node and check that the path alias is also deleted.
$node5->delete();
$path_alias = \Drupal::service('path.alias_storage')->lookupPathAlias('/node/' . $node5->id(), $node5->language()->getId());
$this->assertFalse($path_alias, 'Alias was successfully deleted when the referenced node was deleted.');
// Create sixth test node.
$node6 = $this->drupalCreateNode();
// Create an invalid alias with two leading slashes and verify that the
// extra slash is removed when the link is generated. This ensures that URL
// aliases cannot be used to inject external URLs.
// @todo The user interface should either display an error message or
// automatically trim these invalid aliases, rather than allowing them to
// be silently created, at which point the functional aspects of this
// test will need to be moved elsewhere and switch to using a
// programmatically-created alias instead.
$alias = $this->randomMachineName(8);
$edit = ['path[0][alias]' => '//' . $alias];
$this->drupalPostForm($node6->toUrl('edit-form'), $edit, t('Save'));
$this->drupalGet(Url::fromRoute('system.admin_content'));
// This checks the link href before clicking it, rather than using
// \Drupal\Tests\BrowserTestBase::assertSession()->addressEquals() after
// clicking it, because the test browser does not always preserve the
// correct number of slashes in the URL when it visits internal links;
// using \Drupal\Tests\BrowserTestBase::assertSession()->addressEquals()
// would actually make the test pass unconditionally on the testbot (or
// anywhere else where Drupal is installed in a subdirectory).
$link_xpath = $this->xpath('//a[normalize-space(text())=:label]', [':label' => $node6->getTitle()]);
$link_href = $link_xpath[0]->getAttribute('href');
$this->assertEquals($link_href, base_path() . $alias);
$this->clickLink($node6->getTitle());
$this->assertResponse(404);
}
/**
* Returns the path ID.
*
* @param string $alias
* A string containing an aliased path.
*
* @return int
* Integer representing the path ID.
*/
public function getPID($alias) {
return db_query("SELECT pid FROM {url_alias} WHERE alias = :alias", [':alias' => $alias])->fetchField();
}
/**
* Tests that duplicate aliases fail validation.
*/
public function testDuplicateNodeAlias() {
// Create one node with a random alias.
$node_one = $this->drupalCreateNode();
$edit = [];
$edit['path[0][alias]'] = '/' . $this->randomMachineName();
$this->drupalPostForm('node/' . $node_one->id() . '/edit', $edit, t('Save'));
// Now create another node and try to set the same alias.
$node_two = $this->drupalCreateNode();
$this->drupalPostForm('node/' . $node_two->id() . '/edit', $edit, t('Save'));
$this->assertText(t('The alias is already in use.'));
$this->assertFieldByXPath("//input[@name='path[0][alias]' and contains(@class, 'error')]", $edit['path[0][alias]'], 'Textfield exists and has the error class.');
// 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->drupalPostForm('node/' . $node_two->id() . '/edit', $edit, t('Preview'));
// This error should still be present next to the field.
$this->assertSession()->pageTextContains(t('The alias is already in use.'), 'Field error found with expected text.');
// The validation error set for the page should include this text.
$this->assertSession()->pageTextContains(t('1 error has been found: URL alias'), 'Form error found with expected text.');
// 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,233 @@
<?php
namespace Drupal\Tests\path\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
/**
* Tests path aliases with Content Moderation.
*
* @group content_moderation
* @group path
*/
class PathContentModerationTest extends BrowserTestBase {
use ContentModerationTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = [
'node',
'path',
'content_moderation',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
ConfigurableLanguage::createFromLangcode('fr')->save();
$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->rootUser);
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, 'Save settings');
// Enable translation for moderated node.
$edit = [
'entity_types[node]' => 1,
'settings[node][moderated][translatable]' => 1,
'settings[node][moderated][fields][path]' => 1,
'settings[node][moderated][fields][body]' => 1,
'settings[node][moderated][settings][language][language_alterable]' => 1,
];
$this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
\Drupal::entityTypeManager()->clearCachedDefinitions();
}
/**
* Tests node path aliases on a moderated content type.
*/
public function testNodePathAlias() {
// Create some moderated content with a path alias.
$this->drupalGet('node/add/moderated');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$this->drupalPostForm(NULL, [
'title[0][value]' => 'moderated content',
'path[0][alias]' => '/moderated-content',
'moderation_state[0][state]' => 'published',
], t('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->drupalPostForm(NULL, [
'title[0][value]' => 'pending revision',
'path[0][alias]' => '/moderated-content',
'moderation_state[0][state]' => 'draft',
], t('Save'));
$this->assertSession()->pageTextNotContains('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->drupalPostForm(NULL, [
'title[0][value]' => 'moderated content 2',
'path[0][alias]' => '',
'moderation_state[0][state]' => 'published',
], t('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->drupalPostForm(NULL, [
'title[0][value]' => 'pending revision',
'path[0][alias]' => '/pending-revision',
'moderation_state[0][state]' => 'draft',
], t('Save'));
$this->assertSession()->pageTextContains('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->drupalPostForm(NULL, [
'title[0][value]' => 'moderated content 3',
'path[0][alias]' => '',
'moderation_state[0][state]' => 'published',
], t('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->drupalPostForm(NULL, [
'title[0][value]' => 'pending revision',
'path[0][alias]' => '',
'moderation_state[0][state]' => 'draft',
], t('Save'));
$this->assertSession()->pageTextNotContains('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() {
// 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->drupalPostForm(NULL, $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->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_alias, 'Save (this translation)');
// Confirm the expected error.
$this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
// 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->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft, t('Save (this translation)'));
// Confirm that the new draft revision was created.
$this->assertSession()->pageTextNotContains('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->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_defaults_alias, 'Save (this translation)');
// Verify the expected error.
$this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
// 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->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_empty_alias, 'Save (this translation)');
// Confirm the expected error.
$this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
// 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->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation, 'Save (this translation)');
// Confirm that the new published revision was created.
$this->assertSession()->pageTextNotContains('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.
*/
public function assertPathsAreAccessible(array $paths) {
foreach ($paths as $path) {
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
}
}
}

View file

@ -0,0 +1,196 @@
<?php
namespace Drupal\Tests\path\Functional;
/**
* Confirm that paths work with translated nodes.
*
* @group path
*/
class PathLanguageTest extends PathTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path', 'locale', 'locale_test', 'content_translation'];
/**
* An user with permissions to administer content types.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
protected function setUp() {
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.
$edit = [];
$edit['predefined_langcode'] = 'fr';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
// Enable translation for page node.
$edit = [
'entity_types[node]' => 1,
'settings[node][page][translatable]' => 1,
'settings[node][page][fields][path]' => 1,
'settings[node][page][fields][body]' => 1,
'settings[node][page][settings][language][language_alterable]' => 1,
];
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
\Drupal::entityManager()->clearCachedDefinitions();
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'page');
$this->assertTrue($definitions['path']->isTranslatable(), 'Node path is translatable.');
$this->assertTrue($definitions['body']->isTranslatable(), 'Node body is translatable.');
}
/**
* Test alias functionality through the admin interfaces.
*/
public function testAliasTranslation() {
$node_storage = $this->container->get('entity.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->drupalPostForm('node/' . $english_node->id() . '/edit', $edit, t('Save'));
// Confirm that the alias works.
$this->drupalGet($english_alias);
$this->assertText($english_node->body->value, 'Alias works.');
// Translate the node into French.
$this->drupalGet('node/' . $english_node->id() . '/translations');
$this->clickLink(t('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->drupalPostForm(NULL, $edit, t('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.
$node_storage->resetCache([$english_node->id()]);
$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->assertText($english_node_french_translation->body->value, 'Alias for French translation works.');
// 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->url('canonical', ['language' => $languages['fr']]);
$this->assertTrue(strpos($url, $edit['path[0][alias]']), '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.
$edit = [
'language_interface[enabled][language-user]' => 1,
'language_interface[weight][language-user]' => -9,
'language_interface[enabled][language-url]' => 1,
'language_interface[weight][language-url]' => -8,
];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
// Change user language preference.
$edit = ['preferred_langcode' => 'fr'];
$this->drupalPostForm("user/" . $this->webUser->id() . "/edit", $edit, t('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->assertText($english_node_french_translation->body->value, 'English alias, but French preferred by the user: French translation.');
// Check that the French alias works.
$this->drupalGet("fr/$french_alias");
$this->assertText($english_node_french_translation->body->value, 'Alias for French translation works.');
// Disable URL language negotiation.
$edit = ['language_interface[enabled][language-url]' => FALSE];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
// Check that the English alias still works.
$this->drupalGet($english_alias);
$this->assertText($english_node_french_translation->body->value, 'English alias, but French preferred by the user: French translation.');
// 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->assertResponse(404, 'Alias for French translation is unavailable when URL language negotiation is disabled.');
// 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->assertEqual($french_node_path, '/node/' . $english_node_french_translation->id(), 'Normal path works.');
// Second call should return the same path.
$french_node_path = $this->container->get('path.alias_manager')->getPathByAlias('/' . $french_alias, 'fr');
$this->assertEqual($french_node_path, '/node/' . $english_node_french_translation->id(), '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->assertEqual($french_node_alias, '/' . $french_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->assertEqual($french_node_alias, '/' . $french_alias, 'Alias is the same.');
// Confirm that the alias is removed if the translation is deleted.
$english_node->removeTranslation('fr');
$english_node->save();
$this->assertFalse($this->container->get('path.alias_storage')->aliasExists('/' . $french_alias, 'fr'), 'Alias for French translation is removed when translation is deleted.');
// Check that the English alias still works.
$this->drupalGet($english_alias);
$this->assertTrue($this->container->get('path.alias_storage')->aliasExists('/' . $english_alias, 'en'), 'English alias is not deleted when French translation is removed.');
$this->assertText($english_node->body->value, 'English alias still works');
}
}

View file

@ -0,0 +1,115 @@
<?php
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 {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path', 'locale', 'locale_test'];
protected function setUp() {
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->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
}
/**
* Tests that a language-neutral URL alias works.
*/
public function testLanguageNeutralUrl() {
$name = $this->randomMachineName(8);
$edit = [];
$edit['source'] = '/admin/config/search/path';
$edit['alias'] = '/' . $name;
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$this->drupalGet($name);
$this->assertText(t('Filter aliases'), 'Language-neutral URL alias works');
}
/**
* Tests that a default language URL alias works.
*/
public function testDefaultLanguageUrl() {
$name = $this->randomMachineName(8);
$edit = [];
$edit['source'] = '/admin/config/search/path';
$edit['alias'] = '/' . $name;
$edit['langcode'] = 'en';
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$this->drupalGet($name);
$this->assertText(t('Filter aliases'), 'English URL alias works');
}
/**
* Tests that a non-default language URL alias works.
*/
public function testNonDefaultUrl() {
$name = $this->randomMachineName(8);
$edit = [];
$edit['source'] = '/admin/config/search/path';
$edit['alias'] = '/' . $name;
$edit['langcode'] = 'fr';
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$this->drupalGet('fr/' . $name);
$this->assertText(t('Filter aliases'), 'Foreign URL alias works');
}
/**
* Test that language unspecific aliases are shown and saved in the node form.
*/
public function testNotSpecifiedNode() {
// 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 = [
'source' => '/node/' . $node->id(),
'alias' => '/' . $this->getRandomGenerator()->word(8),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
];
$this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
$this->drupalGet($node->toUrl('edit-form'));
$this->assertSession()->fieldValueEquals('path[0][alias]', $edit['alias']);
$this->drupalPostForm(NULL, [], t('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->drupalPostForm(NULL, [], t('Save'));
$this->assertSession()->pageTextNotContains(t('The alias is already in use.'));
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Drupal\Tests\path\Functional;
use Drupal\media\Entity\MediaType;
/**
* Tests the path media form UI.
*
* @group path
*/
class PathMediaFormTest extends PathTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['media', 'media_test_source'];
/**
* {@inheritdoc}
*/
protected function setUp() {
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() {
$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', '.form-type-vertical-tabs #edit-path-0 summary', 'URL alias');
$assert_session->fieldExists('path[0][alias]');
// Disable the 'Path' field for this content type.
entity_get_form_display('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', '.form-type-vertical-tabs #edit-path-0');
$assert_session->fieldNotExists('path[0][alias]');
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\Tests\path\Functional;
/**
* Tests the Path Node form UI.
*
* @group path
*/
class PathNodeFormTest extends PathTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'path'];
protected function setUp() {
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() {
$assert_session = $this->assertSession();
$this->drupalGet('node/add/page');
// Make sure we have a vertical tab fieldset and 'Path' fields.
$assert_session->elementContains('css', '.form-type-vertical-tabs #edit-path-0 summary', 'URL alias');
$assert_session->fieldExists('path[0][alias]');
// Disable the 'Path' field for this content type.
entity_get_form_display('node', 'page', 'default')
->removeComponent('path')
->save();
$this->drupalGet('node/add/page');
// See if the whole fieldset is gone now.
$assert_session->elementNotExists('css', '.form-type-vertical-tabs #edit-path-0');
$assert_session->fieldNotExists('path[0][alias]');
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Drupal\Tests\path\Functional;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests URL aliases for taxonomy terms.
*
* @group path
*/
class PathTaxonomyTermTest extends PathTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['taxonomy'];
protected function setUp() {
parent::setUp();
// Create a Tags vocabulary for the Article node type.
$vocabulary = Vocabulary::create([
'name' => t('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() {
// 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->drupalPostForm('admin/structure/taxonomy/manage/' . $vocabulary->id() . '/add', $edit, t('Save'));
$tid = db_query("SELECT tid FROM {taxonomy_term_field_data} WHERE name = :name AND default_langcode = 1", [':name' => $edit['name[0][value]']])->fetchField();
// Confirm that the alias works.
$this->drupalGet($edit['path[0][alias]']);
$this->assertText($description, 'Term can be accessed on URL alias.');
// Confirm the 'canonical' and 'shortlink' URLs.
$elements = $this->xpath("//link[contains(@rel, 'canonical') and contains(@href, '" . $edit['path[0][alias]'] . "')]");
$this->assertTrue(!empty($elements), 'Term page contains canonical link URL.');
$elements = $this->xpath("//link[contains(@rel, 'shortlink') and contains(@href, 'taxonomy/term/" . $tid . "')]");
$this->assertTrue(!empty($elements), 'Term page contains shortlink URL.');
// Change the term's URL alias.
$edit2 = [];
$edit2['path[0][alias]'] = '/' . $this->randomMachineName();
$this->drupalPostForm('taxonomy/term/' . $tid . '/edit', $edit2, t('Save'));
// Confirm that the changed alias works.
$this->drupalGet(trim($edit2['path[0][alias]'], '/'));
$this->assertText($description, 'Term can be accessed on changed URL alias.');
// Confirm that the old alias no longer works.
$this->drupalGet(trim($edit['path[0][alias]'], '/'));
$this->assertNoText($description, 'Old URL alias has been removed after altering.');
$this->assertResponse(404, 'Old URL alias returns 404.');
// Remove the term's URL alias.
$edit3 = [];
$edit3['path[0][alias]'] = '';
$this->drupalPostForm('taxonomy/term/' . $tid . '/edit', $edit3, t('Save'));
// Confirm that the alias no longer works.
$this->drupalGet(trim($edit2['path[0][alias]'], '/'));
$this->assertNoText($description, 'Old URL alias has been removed after altering.');
$this->assertResponse(404, 'Old URL alias returns 404.');
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\path\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Provides a base class for testing the Path module.
*/
abstract class PathTestBase extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'path'];
protected function setUp() {
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,152 @@
<?php
namespace Drupal\Tests\path\Kernel\Migrate\d6;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\Core\Database\Database;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* URL alias migration.
*
* @group migrate_drupal_6
*/
class MigrateUrlAliasTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'language',
'content_translation',
'path',
'menu_ui',
// Required for translation migrations.
'migrate_drupal_multilingual',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(['node']);
$this->installSchema('node', ['node_access']);
$this->migrateUsers(FALSE);
$this->migrateFields();
$this->executeMigrations([
'language',
'd6_node_settings',
'd6_node',
'd6_node_translation',
'd6_url_alias',
]);
}
/**
* Assert a path.
*
* @param string $pid
* The path id.
* @param array $conditions
* The path conditions.
* @param array $path
* The path.
*/
private function assertPath($pid, $conditions, $path) {
$this->assertTrue($path, "Path alias for " . $conditions['source'] . " successfully loaded.");
$this->assertIdentical($conditions['alias'], $path['alias']);
$this->assertIdentical($conditions['langcode'], $path['langcode']);
$this->assertIdentical($conditions['source'], $path['source']);
}
/**
* Test the url alias migration.
*/
public function testUrlAlias() {
$id_map = $this->getMigration('d6_url_alias')->getIdMap();
// Test that the field exists.
$conditions = [
'source' => '/node/1',
'alias' => '/alias-one',
'langcode' => 'af',
];
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertPath('1', $conditions, $path);
$this->assertIdentical($id_map->lookupDestinationId([$path['pid']]), ['1'], "Test IdMap");
$conditions = [
'source' => '/node/2',
'alias' => '/alias-two',
'langcode' => 'en',
];
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertPath('2', $conditions, $path);
// 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 = \Drupal::service('path.alias_storage')->load(['pid' => $path['pid']]);
$conditions['alias'] = '/new-url-alias';
$this->assertPath('2', $conditions, $path);
$conditions = [
'source' => '/node/3',
'alias' => '/alias-three',
'langcode' => 'und',
];
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertPath('3', $conditions, $path);
$path = \Drupal::service('path.alias_storage')->load(['alias' => '/source-noslash']);
$conditions = [
'source' => '/admin',
'alias' => '/source-noslash',
'langcode' => 'und',
];
$this->assertPath('2', $conditions, $path);
}
/**
* Test the URL alias migration with translated nodes.
*/
public function testUrlAliasWithTranslatedNodes() {
$alias_storage = $this->container->get('path.alias_storage');
// Alias for the 'The Real McCoy' node in English.
$path = $alias_storage->load(['alias' => '/the-real-mccoy']);
$this->assertSame('/node/10', $path['source']);
$this->assertSame('en', $path['langcode']);
// Alias for the 'The Real McCoy' French translation,
// which should now point to node/10 instead of node/11.
$path = $alias_storage->load(['alias' => '/le-vrai-mccoy']);
$this->assertSame('/node/10', $path['source']);
$this->assertSame('fr', $path['langcode']);
// Alias for the 'Abantu zulu' node in Zulu.
$path = $alias_storage->load(['alias' => '/abantu-zulu']);
$this->assertSame('/node/12', $path['source']);
$this->assertSame('zu', $path['langcode']);
// Alias for the 'Abantu zulu' English translation,
// which should now point to node/12 instead of node/13.
$path = $alias_storage->load(['alias' => '/the-zulu-people']);
$this->assertSame('/node/12', $path['source']);
$this->assertSame('en', $path['langcode']);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\Tests\path\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests URL alias migration.
*
* @group path
*/
class MigrateUrlAliasTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_translation',
'language',
'menu_ui',
// Required for translation migrations.
'migrate_drupal_multilingual',
'node',
'path',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig('node');
$this->installSchema('node', ['node_access']);
$this->executeMigrations([
'language',
'd7_user_role',
'd7_user',
'd7_node_type',
'd7_node',
'd7_node_translation',
'd7_url_alias',
]);
}
/**
* Test the URL alias migration.
*/
public function testUrlAlias() {
$alias_storage = $this->container->get('path.alias_storage');
$path = $alias_storage->load([
'source' => '/taxonomy/term/4',
'alias' => '/term33',
'langcode' => 'und',
]);
$this->assertIdentical('/taxonomy/term/4', $path['source']);
$this->assertIdentical('/term33', $path['alias']);
$this->assertIdentical('und', $path['langcode']);
// Alias with no slash.
$path = $alias_storage->load(['alias' => '/source-noslash']);
$this->assertSame('/admin', $path['source']);
$this->assertSame('und', $path['langcode']);
}
/**
* Test the URL alias migration with translated nodes.
*/
public function testUrlAliasWithTranslatedNodes() {
$alias_storage = $this->container->get('path.alias_storage');
// Alias for the 'The thing about Deep Space 9' node in English.
$path = $alias_storage->load(['alias' => '/deep-space-9']);
$this->assertSame('/node/2', $path['source']);
$this->assertSame('en', $path['langcode']);
// Alias for the 'The thing about Deep Space 9' Icelandic translation,
// which should now point to node/2 instead of node/3.
$path = $alias_storage->load(['alias' => '/deep-space-9-is']);
$this->assertSame('/node/2', $path['source']);
$this->assertSame('is', $path['langcode']);
// Alias for the 'The thing about Firefly' node in Icelandic.
$path = $alias_storage->load(['alias' => '/firefly-is']);
$this->assertSame('/node/4', $path['source']);
$this->assertSame('is', $path['langcode']);
// Alias for the 'The thing about Firefly' English translation,
// which should now point to node/4 instead of node/5.
$path = $alias_storage->load(['alias' => '/firefly']);
$this->assertSame('/node/4', $path['source']);
$this->assertSame('en', $path['langcode']);
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace Drupal\Tests\path\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
/**
* Tests loading and storing data using PathItem.
*
* @group path
*/
class PathItemTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path', 'node', 'user', 'system', 'language', 'content_translation'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installSchema('node', ['node_access']);
$node_type = NodeType::create(['type' => 'foo']);
$node_type->save();
$this->installConfig(['language']);
ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
* Test creating, loading, updating and deleting aliases through PathItem.
*/
public function testPathItem() {
/** @var \Drupal\Core\Path\AliasStorageInterface $alias_storage */
$alias_storage = \Drupal::service('path.alias_storage');
$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_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_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_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $translation->language()->getId());
$this->assertEquals('/furchtbar', $stored_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_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/bar', $stored_alias);
$old_alias = $alias_storage->lookupPathSource('/foo', $node->language()->getId());
$this->assertFalse($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_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foobar', $stored_alias);
$old_alias = $alias_storage->lookupPathSource('/bar', $node->language()->getId());
$this->assertFalse($old_alias);
$loaded_node->get('path')->alias = '';
$this->assertEquals('', $loaded_node->get('path')->alias);
$loaded_node->save();
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertFalse($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_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_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_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foobar', $stored_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,58 @@
<?php
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 {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path', 'content_translation_test', 'language', 'entity_test', 'user', 'system'];
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('entity_test_mul');
\Drupal::service('router.builder')->rebuild();
// Adding german language.
ConfigurableLanguage::create(['id' => '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() {
$entity_type = EntityTestTranslatableUISkip::create([
'name' => 'name english',
'language' => 'en',
]);
$entity_type->save();
$entity_type->addTranslation('de', ['name' => 'name german']);
$entity_type->save();
$this->assertEqual(count($entity_type->getTranslationLanguages()), 2);
$entity_type->removeTranslation('de');
$entity_type->save();
$this->assertEqual(count($entity_type->getTranslationLanguages()), 1);
}
}

View file

@ -0,0 +1,48 @@
<?php
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}
*/
public static $modules = ['migrate_drupal', 'path'];
/**
* {@inheritdoc}
*/
public 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,48 @@
<?php
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}
*/
public static $modules = ['migrate_drupal', 'path'];
/**
* {@inheritdoc}
*/
public 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,35 @@
<?php
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() {
return 'path';
}
/**
* {@inheritdoc}
*/
protected function getModuleAndPath() {
return ['path', dirname(dirname(dirname(dirname(__DIR__))))];
}
/**
* @covers ::getColumns
* @covers ::getSchema
*/
public function testGetColumns() {
$this->assertSame([], $this->definition->getColumns());
}
}