Move all files to 2017/
This commit is contained in:
parent
ac7370f67f
commit
2875863330
15717 changed files with 0 additions and 0 deletions
134
2017/web/core/modules/path/src/Controller/PathController.php
Normal file
134
2017/web/core/modules/path/src/Controller/PathController.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
33
2017/web/core/modules/path/src/Form/AddForm.php
Normal file
33
2017/web/core/modules/path/src/Form/AddForm.php
Normal 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,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
92
2017/web/core/modules/path/src/Form/DeleteForm.php
Normal file
92
2017/web/core/modules/path/src/Form/DeleteForm.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
61
2017/web/core/modules/path/src/Form/EditForm.php
Normal file
61
2017/web/core/modules/path/src/Form/EditForm.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
71
2017/web/core/modules/path/src/Form/PathFilterForm.php
Normal file
71
2017/web/core/modules/path/src/Form/PathFilterForm.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
219
2017/web/core/modules/path/src/Form/PathFormBase.php
Normal file
219
2017/web/core/modules/path/src/Form/PathFormBase.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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'];
|
||||
}
|
||||
|
||||
}
|
|
@ -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.';
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
32
2017/web/core/modules/path/src/Tests/PathTestBase.php
Normal file
32
2017/web/core/modules/path/src/Tests/PathTestBase.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue