first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
name: 'Custom Menu Links'
type: module
description: 'Allows users to create menu links.'
package: Core
# version: VERSION
dependencies:
- drupal:link
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,13 @@
<?php
/**
* @file
* Install, update and uninstall functions for the menu_link_content module.
*/
/**
* Implements hook_update_last_removed().
*/
function menu_link_content_update_last_removed() {
return 8601;
}

View File

@@ -0,0 +1,4 @@
menu_link_content:
class: \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent
form_class: \Drupal\menu_link_content\Form\MenuLinkContentForm
deriver: \Drupal\menu_link_content\Plugin\Deriver\MenuLinkContentDeriver

View File

@@ -0,0 +1,4 @@
entity.menu_link_content.canonical:
route_name: entity.menu_link_content.canonical
base_route: entity.menu_link_content.canonical
title: Edit

View File

@@ -0,0 +1,129 @@
<?php
/**
* @file
* Allows administrators to create custom menu links.
*/
use Drupal\Core\Url;
use Drupal\Core\Entity\EntityInterface;
use Drupal\path_alias\PathAliasInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\system\MenuInterface;
/**
* Implements hook_help().
*/
function menu_link_content_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.menu_link_content':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Custom Menu Links module allows users to create menu links. These links can be translated if multiple languages are used for the site.');
if (\Drupal::moduleHandler()->moduleExists('menu_ui')) {
$output .= ' ' . t('It is required by the Menu UI module, which provides an interface for managing menus and menu links. For more information, see the <a href=":menu-help">Menu UI module help page</a> and the <a href=":drupal-org-help">online documentation for the Custom Menu Links module</a>.', [':menu-help' => Url::fromRoute('help.page', ['name' => 'menu_ui'])->toString(), ':drupal-org-help' => 'https://www.drupal.org/documentation/modules/menu_link']);
}
else {
$output .= ' ' . t('For more information, see the <a href=":drupal-org-help">online documentation for the Custom Menu Links module</a>. If you install the Menu UI module, it provides an interface for managing menus and menu links.', [':drupal-org-help' => 'https://www.drupal.org/documentation/modules/menu_link']);
}
$output .= '</p>';
return $output;
}
}
/**
* Implements hook_entity_type_alter().
*/
function menu_link_content_entity_type_alter(array &$entity_types) {
// @todo Moderation is disabled for custom menu links until when we have an UI
// for them.
// @see https://www.drupal.org/project/drupal/issues/2350939
$entity_types['menu_link_content']->setHandlerClass('moderation', '');
}
/**
* Implements hook_menu_delete().
*/
function menu_link_content_menu_delete(MenuInterface $menu) {
$storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
$menu_links = $storage->loadByProperties(['menu_name' => $menu->id()]);
$storage->delete($menu_links);
}
/**
* Implements hook_ENTITY_TYPE_insert() for 'path_alias'.
*/
function menu_link_content_path_alias_insert(PathAliasInterface $path_alias) {
_menu_link_content_update_path_alias($path_alias->getAlias());
}
/**
* Helper function to update plugin definition using internal scheme.
*
* @param string $path
* The path alias.
*/
function _menu_link_content_update_path_alias($path) {
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
/** @var \Drupal\menu_link_content\MenuLinkContentInterface[] $entities */
$entities = \Drupal::entityTypeManager()
->getStorage('menu_link_content')
->loadByProperties(['link.uri' => 'internal:' . $path]);
foreach ($entities as $menu_link) {
$menu_link_manager->updateDefinition($menu_link->getPluginId(), $menu_link->getPluginDefinition(), FALSE);
}
}
/**
* Implements hook_ENTITY_TYPE_update() for 'path_alias'.
*/
function menu_link_content_path_alias_update(PathAliasInterface $path_alias) {
if ($path_alias->getAlias() != $path_alias->original->getAlias()) {
_menu_link_content_update_path_alias($path_alias->getAlias());
_menu_link_content_update_path_alias($path_alias->original->getAlias());
}
elseif ($path_alias->getPath() != $path_alias->original->getPath()) {
_menu_link_content_update_path_alias($path_alias->getAlias());
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'path_alias'.
*/
function menu_link_content_path_alias_delete(PathAliasInterface $path_alias) {
_menu_link_content_update_path_alias($path_alias->getAlias());
}
/**
* Implements hook_entity_predelete().
*/
function menu_link_content_entity_predelete(EntityInterface $entity) {
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$entity_type_id = $entity->getEntityTypeId();
foreach ($entity->uriRelationships() as $rel) {
$url = $entity->toUrl($rel);
// Entities can provide uri relationships that are not routed, in this case
// getRouteParameters() will throw an exception.
if (!$url->isRouted()) {
continue;
}
$route_parameters = $url->getRouteParameters();
if (!isset($route_parameters[$entity_type_id])) {
// Do not delete links which do not relate to this exact entity. For
// example, "collection", "add-form", etc.
continue;
}
// Delete all MenuLinkContent links that point to this entity route.
$result = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $route_parameters);
if ($result) {
foreach ($result as $id => $instance) {
if ($instance->isDeletable() && str_starts_with($id, 'menu_link_content:')) {
$instance->deleteLink();
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* Post update functions for the Menu link content module.
*/
/**
* Implements hook_removed_post_updates().
*/
function menu_link_content_removed_post_updates() {
return [
'menu_link_content_post_update_make_menu_link_content_revisionable' => '9.0.0',
];
}

View File

@@ -0,0 +1,31 @@
entity.menu.add_link_form:
path: '/admin/structure/menu/manage/{menu}/add'
defaults:
_controller: '\Drupal\menu_link_content\Controller\MenuController::addLink'
_title: 'Add menu link'
requirements:
_entity_create_access: 'menu_link_content'
entity.menu_link_content.canonical:
path: '/admin/structure/menu/item/{menu_link_content}/edit'
defaults:
_entity_form: 'menu_link_content.default'
_title: 'Edit menu link'
requirements:
_entity_access: 'menu_link_content.update'
entity.menu_link_content.edit_form:
path: '/admin/structure/menu/item/{menu_link_content}/edit'
defaults:
_entity_form: 'menu_link_content.default'
_title: 'Edit menu link'
requirements:
_entity_access: 'menu_link_content.update'
entity.menu_link_content.delete_form:
path: '/admin/structure/menu/item/{menu_link_content}/delete'
defaults:
_entity_form: 'menu_link_content.delete'
_title: 'Delete menu link'
requirements:
_entity_access: 'menu_link_content.delete'

View File

@@ -0,0 +1,70 @@
# cspell:ignore mlid plid
id: d6_menu_links
label: Menu links
audit: true
migration_tags:
- Drupal 6
- Content
source:
plugin: menu_link
process:
skip_localized:
-
plugin: callback
callable: is_null
source: is_localized
-
plugin: skip_on_empty
method: row
id: mlid
title: link_title
description: description
menu_name:
-
plugin: migration_lookup
# The menu migration is in the system module.
migration: d6_menu
source: menu_name
-
plugin: skip_on_empty
method: row
-
plugin: static_map
map:
management: admin
bypass: true
'link/uri':
plugin: link_uri
source: link_path
'link/options':
plugin: link_options
source: options
route:
plugin: route
source:
- link_path
- options
route_name: '@route/route_name'
route_parameters: '@route/route_parameters'
url: '@route/url'
options: '@route/options'
external: external
weight: weight
expanded: expanded
enabled: enabled
parent:
plugin: menu_link_parent
source:
- plid
- '@menu_name'
- parent_link_path
changed: updated
destination:
plugin: entity:menu_link_content
default_bundle: menu_link_content
no_stub: true
migration_dependencies:
required:
- d6_menu
optional:
- d6_node

View File

@@ -0,0 +1,64 @@
# cspell:ignore mlid plid
id: d7_menu_links
label: Menu links
audit: true
migration_tags:
- Drupal 7
- Content
source:
plugin: menu_link
constants:
bundle: menu_link_content
process:
skip_translation:
plugin: skip_on_empty
method: row
source: skip_translation
id: mlid
langcode:
plugin: default_value
source: language
default_value: und
bundle: 'constants/bundle'
title: link_title
description: description
menu_name:
-
plugin: migration_lookup
migration: d7_menu
source: menu_name
-
plugin: skip_on_empty
method: row
'link/uri':
plugin: link_uri
source: link_path
'link/options': options
route:
plugin: route
source:
- link_path
- options
route_name: '@route/route_name'
route_parameters: '@route/route_parameters'
url: '@route/url'
options: '@route/options'
external: external
weight: weight
expanded: expanded
enabled: enabled
parent:
plugin: menu_link_parent
source:
- plid
- '@menu_name'
- parent_link_path
changed: updated
destination:
plugin: entity:menu_link_content
no_stub: true
migration_dependencies:
required:
- d7_menu
optional:
- d7_node

View File

@@ -0,0 +1,5 @@
finished:
6:
menu: menu_link_content
7:
menu: menu_link_content

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\menu_link_content\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\system\MenuInterface;
/**
* Defines a route controller for a form for menu link content entity creation.
*/
class MenuController extends ControllerBase {
/**
* Provides the menu link creation form.
*
* @param \Drupal\system\MenuInterface $menu
* An entity representing a custom menu.
*
* @return array
* Returns the menu link creation form.
*/
public function addLink(MenuInterface $menu) {
$menu_link = $this->entityTypeManager()
->getStorage('menu_link_content')
->create([
'menu_name' => $menu->id(),
]);
return $this->entityFormBuilder()->getForm($menu_link);
}
}

View File

@@ -0,0 +1,428 @@
<?php
namespace Drupal\menu_link_content\Entity;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\link\LinkItemInterface;
use Drupal\menu_link_content\MenuLinkContentInterface;
/**
* Defines the menu link content entity class.
*
* @property \Drupal\Core\Field\FieldItemList $link
* @property \Drupal\Core\Field\FieldItemList $rediscover
*
* @ContentEntityType(
* id = "menu_link_content",
* label = @Translation("Custom menu link"),
* label_collection = @Translation("Custom menu links"),
* label_singular = @Translation("custom menu link"),
* label_plural = @Translation("custom menu links"),
* label_count = @PluralTranslation(
* singular = "@count custom menu link",
* plural = "@count custom menu links",
* ),
* handlers = {
* "storage" = "\Drupal\menu_link_content\MenuLinkContentStorage",
* "storage_schema" = "Drupal\menu_link_content\MenuLinkContentStorageSchema",
* "access" = "Drupal\menu_link_content\MenuLinkContentAccessControlHandler",
* "form" = {
* "default" = "Drupal\menu_link_content\Form\MenuLinkContentForm",
* "delete" = "Drupal\menu_link_content\Form\MenuLinkContentDeleteForm"
* },
* "list_builder" = "Drupal\menu_link_content\MenuLinkListBuilder"
* },
* admin_permission = "administer menu",
* base_table = "menu_link_content",
* data_table = "menu_link_content_data",
* revision_table = "menu_link_content_revision",
* revision_data_table = "menu_link_content_field_revision",
* translatable = TRUE,
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
* "label" = "title",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "bundle" = "bundle",
* "published" = "enabled",
* },
* revision_metadata_keys = {
* "revision_user" = "revision_user",
* "revision_created" = "revision_created",
* "revision_log_message" = "revision_log_message",
* },
* links = {
* "canonical" = "/admin/structure/menu/item/{menu_link_content}/edit",
* "edit-form" = "/admin/structure/menu/item/{menu_link_content}/edit",
* "delete-form" = "/admin/structure/menu/item/{menu_link_content}/delete",
* },
* constraints = {
* "MenuTreeHierarchy" = {}
* },
* )
*/
class MenuLinkContent extends EditorialContentEntityBase implements MenuLinkContentInterface {
/**
* A flag for whether this entity is wrapped in a plugin instance.
*
* @var bool
*/
protected $insidePlugin = FALSE;
/**
* {@inheritdoc}
*/
public function setInsidePlugin() {
$this->insidePlugin = TRUE;
}
/**
* {@inheritdoc}
*/
public function getTitle() {
return $this->get('title')->value;
}
/**
* {@inheritdoc}
*/
public function getUrlObject() {
return $this->link->first()->getUrl();
}
/**
* {@inheritdoc}
*/
public function getMenuName() {
return $this->get('menu_name')->value;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->get('description')->value;
}
/**
* {@inheritdoc}
*/
public function getPluginId() {
return 'menu_link_content:' . $this->uuid();
}
/**
* {@inheritdoc}
*/
public function isEnabled() {
return (bool) $this->get('enabled')->value;
}
/**
* {@inheritdoc}
*/
public function isExpanded() {
return (bool) $this->get('expanded')->value;
}
/**
* {@inheritdoc}
*/
public function getParentId() {
// Cast the parent ID to a string, only an empty string means no parent,
// NULL keeps the existing parent.
return (string) $this->get('parent')->value;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return (int) $this->get('weight')->value;
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
$definition = [];
$definition['class'] = 'Drupal\menu_link_content\Plugin\Menu\MenuLinkContent';
$definition['menu_name'] = $this->getMenuName();
if ($url_object = $this->getUrlObject()) {
$definition['url'] = NULL;
$definition['route_name'] = NULL;
$definition['route_parameters'] = [];
if (!$url_object->isRouted()) {
$definition['url'] = $url_object->getUri();
}
else {
$definition['route_name'] = $url_object->getRouteName();
$definition['route_parameters'] = $url_object->getRouteParameters();
}
$definition['options'] = $url_object->getOptions();
}
$definition['title'] = $this->getTitle();
$definition['description'] = $this->getDescription();
$definition['weight'] = $this->getWeight();
$definition['id'] = $this->getPluginId();
$definition['metadata'] = ['entity_id' => $this->id()];
$definition['form_class'] = '\Drupal\menu_link_content\Form\MenuLinkContentForm';
$definition['enabled'] = $this->isEnabled() ? 1 : 0;
$definition['expanded'] = $this->isExpanded() ? 1 : 0;
$definition['provider'] = 'menu_link_content';
$definition['discovered'] = 0;
$definition['parent'] = $this->getParentId();
return $definition;
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage, array &$values) {
$values += ['bundle' => 'menu_link_content'];
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if (parse_url($this->link->uri, PHP_URL_SCHEME) === 'internal') {
$this->setRequiresRediscovery(TRUE);
}
else {
$this->setRequiresRediscovery(FALSE);
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// Don't update the menu tree if a pending revision was saved.
if (!$this->isDefaultRevision()) {
return;
}
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
// The menu link can just be updated if there is already a menu link entry
// on both entity and menu link plugin level.
$definition = $this->getPluginDefinition();
// Even when $update is FALSE, for top level links it is possible the link
// already is in the storage because of the getPluginDefinition() call
// above, see https://www.drupal.org/node/2605684#comment-10515450 for the
// call chain. Because of this the $update flag is ignored and only the
// existence of the definition (equals to being in the tree storage) is
// checked.
if ($menu_link_manager->getDefinition($this->getPluginId(), FALSE)) {
// When the entity is saved via a plugin instance, we should not call
// the menu tree manager to update the definition a second time.
if (!$this->insidePlugin) {
$menu_link_manager->updateDefinition($this->getPluginId(), $definition, FALSE);
}
}
else {
$menu_link_manager->addDefinition($this->getPluginId(), $definition);
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
foreach ($entities as $menu_link) {
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */
$menu_link_manager->removeDefinition($menu_link->getPluginId(), FALSE);
// Children get re-attached to the menu link's parent.
$parent_plugin_id = $menu_link->getParentId();
$children = $storage->loadByProperties(['parent' => $menu_link->getPluginId()]);
foreach ($children as $child) {
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $child */
$child->set('parent', $parent_plugin_id)->save();
}
}
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
// Add the publishing status field.
$fields += static::publishedBaseFieldDefinitions($entity_type);
$fields['id']->setLabel(t('Entity ID'))
->setDescription(t('The entity ID for this menu link content entity.'));
$fields['uuid']->setDescription(t('The content menu link UUID.'));
$fields['langcode']->setDescription(t('The menu link language code.'));
$fields['bundle']
->setDescription(t('The content menu link bundle.'))
->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH)
->setSetting('is_ascii', TRUE);
$fields['title'] = BaseFieldDefinition::create('string')
->setLabel(t('Menu link title'))
->setDescription(t('The text to be used for this link in the menu.'))
->setRequired(TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE);
$fields['description'] = BaseFieldDefinition::create('string')
->setLabel(t('Description'))
->setDescription(t('Shown when hovering over the menu link.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => 0,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => 0,
]);
$fields['menu_name'] = BaseFieldDefinition::create('string')
->setLabel(t('Menu name'))
->setDescription(t('The menu name. All links with the same menu name (such as "tools") are part of the same menu.'))
->setDefaultValue('tools')
->setSetting('is_ascii', TRUE);
$fields['link'] = BaseFieldDefinition::create('link')
->setLabel(t('Link'))
->setDescription(t('The location this menu link points to.'))
->setRevisionable(TRUE)
->setRequired(TRUE)
->setSettings([
'link_type' => LinkItemInterface::LINK_GENERIC,
'title' => DRUPAL_DISABLED,
])
->setDisplayOptions('form', [
'type' => 'link_default',
'weight' => -2,
]);
$fields['external'] = BaseFieldDefinition::create('boolean')
->setLabel(t('External'))
->setDescription(t('A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).'))
->setDefaultValue(FALSE)
->setRevisionable(TRUE);
$fields['rediscover'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Indicates whether the menu link should be rediscovered'))
->setDefaultValue(FALSE);
$fields['weight'] = BaseFieldDefinition::create('integer')
->setLabel(t('Weight'))
->setDescription(t('Link weight among links in the same menu at the same depth. In the menu, the links with high weight will sink and links with a low weight will be positioned nearer the top.'))
->setDefaultValue(0)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'number_integer',
'weight' => 0,
])
->setDisplayOptions('form', [
'type' => 'number',
'weight' => 20,
]);
$fields['expanded'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Show as expanded'))
->setDescription(t('If selected and this menu link has children, the menu will always appear expanded. This option may be overridden for the entire menu tree when placing a menu block.'))
->setDefaultValue(FALSE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'boolean',
'weight' => 0,
])
->setDisplayOptions('form', [
'settings' => ['display_label' => TRUE],
'weight' => 0,
]);
// Override some properties of the published field added by
// \Drupal\Core\Entity\EntityPublishedTrait::publishedBaseFieldDefinitions().
$fields['enabled']->setLabel(t('Enabled'));
$fields['enabled']->setDescription(t('A flag for whether the link should be enabled in menus or hidden.'));
$fields['enabled']->setTranslatable(FALSE);
$fields['enabled']->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'boolean',
'weight' => 0,
]);
$fields['enabled']->setDisplayOptions('form', [
'settings' => ['display_label' => TRUE],
'weight' => -1,
]);
$fields['parent'] = BaseFieldDefinition::create('string')
->setLabel(t('Parent plugin ID'))
->setDescription(t('The ID of the parent menu link plugin, or empty string when at the top level of the hierarchy.'));
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the menu link was last edited.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE);
// @todo Keep this field hidden until we have a revision UI for menu links.
// @see https://www.drupal.org/project/drupal/issues/2350939
$fields['revision_log_message']->setDisplayOptions('form', [
'region' => 'hidden',
]);
return $fields;
}
/**
* {@inheritdoc}
*/
public function requiresRediscovery() {
return $this->get('rediscover')->value;
}
/**
* {@inheritdoc}
*/
public function setRequiresRediscovery($rediscovery) {
$this->set('rediscover', $rediscovery);
return $this;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\menu_link_content\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Url;
/**
* Provides a delete form for content menu links.
*
* @internal
*/
class MenuLinkContentDeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
if ($this->moduleHandler->moduleExists('menu_ui')) {
return new Url('entity.menu.edit_form', ['menu' => $this->entity->getMenuName()]);
}
return $this->entity->toUrl();
}
/**
* {@inheritdoc}
*/
protected function getRedirectUrl() {
return $this->getCancelUrl();
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
return $this->t('The menu link %title has been deleted.', ['%title' => $this->entity->label()]);
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace Drupal\menu_link_content\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\MenuParentFormSelectorInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\system\MenuInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form to add/update content menu links.
*
* @internal
*/
class MenuLinkContentForm extends ContentEntityForm {
use DeprecatedServicePropertyTrait;
/**
* The deprecated properties and services on this class.
*/
protected array $deprecatedProperties = ['languageManager' => 'language_manager'];
/**
* The content menu link.
*
* @var \Drupal\menu_link_content\MenuLinkContentInterface
*/
protected $entity;
/**
* The parent form selector service.
*/
protected MenuParentFormSelectorInterface $menuParentSelector;
/**
* The path validator.
*/
protected PathValidatorInterface $pathValidator;
/**
* Constructs a MenuLinkContentForm object.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector
* The menu parent form selector service.
* @param \Drupal\Core\Path\PathValidatorInterface|\Drupal\Core\Language\LanguageManagerInterface $path_validator
* The path validator.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\Drupal\Core\Path\PathValidatorInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface|\Drupal\Core\Entity\EntityTypeBundleInfoInterface $time
* The time service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, MenuParentFormSelectorInterface $menu_parent_selector, PathValidatorInterface|LanguageManagerInterface $path_validator, EntityTypeBundleInfoInterface|PathValidatorInterface|null $entity_type_bundle_info = NULL, TimeInterface|EntityTypeBundleInfoInterface|null $time = NULL) {
if ($path_validator instanceof LanguageManagerInterface) {
$path_validator = func_get_arg(3);
$entity_type_bundle_info = func_get_arg(4);
$time = func_get_arg(5);
@trigger_error('Calling ' . __CLASS__ . '::__construct() with the $language_manager argument is deprecated in drupal:10.2.0 and is removed in drupal:11.0.0. See https://www.drupal.org/node/3325178', E_USER_DEPRECATED);
}
$this->menuParentSelector = $menu_parent_selector;
$this->pathValidator = $path_validator;
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('menu.parent_form_selector'),
$container->get('path.validator'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$parent_id = $this->entity->getParentId() ?: $this->getRequest()->query->get('parent');
$default = $this->entity->getMenuName() . ':' . $parent_id;
$id = $this->entity->isNew() ? '' : $this->entity->getPluginId();
$menu_id = $this->entity->getMenuName();
$menu = $this->entityTypeManager->getStorage('menu')->load($menu_id);
if ($menu instanceof MenuInterface && $this->entity->isNew()) {
$form['menu_parent'] = $this->menuParentSelector->parentSelectElement($default, $id, [
$menu_id => $menu->label(),
]);
}
else {
$form['menu_parent'] = $this->menuParentSelector->parentSelectElement($default, $id);
}
$form['menu_parent']['#weight'] = 10;
$form['menu_parent']['#title'] = $this->t('Parent link');
$form['menu_parent']['#description'] = $this->t('The maximum depth for a link and all its children is fixed. Some menu links may not be available as parents if selecting them would exceed this limit.');
$form['menu_parent']['#attributes']['class'][] = 'menu-title-select';
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$element = parent::actions($form, $form_state);
$element['submit']['#button_type'] = 'primary';
$element['delete']['#access'] = $this->entity->access('delete');
return $element;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
$entity = parent::buildEntity($form, $form_state);
[$menu_name, $parent] = explode(':', $form_state->getValue('menu_parent'), 2);
$entity->parent->value = $parent;
$entity->menu_name->value = $menu_name;
$entity->enabled->value = (!$form_state->isValueEmpty(['enabled', 'value']));
$entity->expanded->value = (!$form_state->isValueEmpty(['expanded', 'value']));
return $entity;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
// The entity is rebuilt in parent::submit().
$menu_link = $this->entity;
$menu_link->save();
$this->messenger()->addStatus($this->t('The menu link has been saved.'));
$form_state->setRedirectUrl($menu_link->toUrl('canonical'));
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access control handler for the menu link content entity type.
*/
class MenuLinkContentAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
/**
* The access manager to check routes by name.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* Creates a new MenuLinkContentAccessControlHandler.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager to check routes by name.
*/
public function __construct(EntityTypeInterface $entity_type, AccessManagerInterface $access_manager) {
parent::__construct($entity_type);
$this->accessManager = $access_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static($entity_type, $container->get('access_manager'));
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'view':
// There is no direct viewing of a menu link, but still for purposes of
// content_translation we need a generic way to check access.
return AccessResult::allowedIfHasPermission($account, 'administer menu');
case 'update':
if (!$account->hasPermission('administer menu')) {
return AccessResult::neutral("The 'administer menu' permission is required.")->cachePerPermissions();
}
else {
// Assume that access is allowed.
$access = AccessResult::allowed()->cachePerPermissions()->addCacheableDependency($entity);
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
// If the link is routed determine whether the user has access unless
// they have the 'link to any page' permission.
if (!$account->hasPermission('link to any page') && ($url_object = $entity->getUrlObject()) && $url_object->isRouted()) {
$link_access = $this->accessManager->checkNamedRoute($url_object->getRouteName(), $url_object->getRouteParameters(), $account, TRUE);
$access = $access->andIf($link_access);
}
return $access;
}
case 'delete':
return AccessResult::allowedIfHasPermission($account, 'administer menu')
->andIf(AccessResult::allowedIf(!$entity->isNew())->addCacheableDependency($entity));
default:
return parent::checkAccess($entity, $operation, $account);
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionLogInterface;
/**
* Defines an interface for custom menu links.
*/
interface MenuLinkContentInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface, RevisionLogInterface {
/**
* Flags this instance as being wrapped in a menu link plugin instance.
*/
public function setInsidePlugin();
/**
* Gets the title of the menu link.
*
* @return string
* The title of the link.
*/
public function getTitle();
/**
* Gets the URL object pointing to the URL of the menu link content entity.
*
* @return \Drupal\Core\Url
* A Url object instance.
*/
public function getUrlObject();
/**
* Gets the menu name of the custom menu link.
*
* @return string
* The menu ID.
*/
public function getMenuName();
/**
* Gets the description of the menu link for the UI.
*
* @return string
* The description to use on admin pages or as a title attribute.
*/
public function getDescription();
/**
* Gets the menu plugin ID associated with this entity.
*
* @return string
* The plugin ID.
*/
public function getPluginId();
/**
* Returns whether the menu link is marked as enabled.
*
* @return bool
* TRUE if is enabled, otherwise FALSE.
*/
public function isEnabled();
/**
* Returns whether the menu link is marked as always expanded.
*
* @return bool
* TRUE for expanded, FALSE otherwise.
*/
public function isExpanded();
/**
* Gets the plugin ID of the parent menu link.
*
* @return string
* A plugin ID, or empty string if this link is at the top level.
*/
public function getParentId();
/**
* Returns the weight of the menu link content entity.
*
* @return int
* A weight for use when ordering links.
*/
public function getWeight();
/**
* Builds up the menu link plugin definition for this entity.
*
* @return array
* The plugin definition corresponding to this entity.
*
* @see \Drupal\Core\Menu\MenuLinkTree::$defaults
*/
public function getPluginDefinition();
/**
* Returns whether the menu link requires rediscovery.
*
* If a menu-link points to a user-supplied path such as /blog then the route
* this resolves to needs to be rediscovered as the module or route providing
* a given path might change over time.
*
* For example: at the time a menu-link is created, the /blog path might be
* provided by a route in Views module, but later this path may be served by
* the Panels module. Flagging a link as requiring rediscovery ensures that if
* the route that provides a user-entered path changes over time, the link is
* flexible enough to update to reflect these changes.
*
* @return bool
* TRUE if the menu link requires rediscovery during route rebuilding.
*/
public function requiresRediscovery();
/**
* Flags a link as requiring rediscovery.
*
* @param bool $rediscovery
* Whether or not the link requires rediscovery.
*
* @return $this
* The instance on which the method was called.
*
* @see \Drupal\menu_link_content\MenuLinkContentInterface::requiresRediscovery()
*/
public function setRequiresRediscovery($rediscovery);
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Storage handler for menu_link_content entities.
*/
class MenuLinkContentStorage extends SqlContentEntityStorage implements MenuLinkContentStorageInterface {
/**
* {@inheritdoc}
*/
public function getMenuLinkIdsWithPendingRevisions() {
$table_mapping = $this->getTableMapping();
$id_field = $table_mapping->getColumnNames($this->entityType->getKey('id'))['value'];
$revision_field = $table_mapping->getColumnNames($this->entityType->getKey('revision'))['value'];
$rta_field = $table_mapping->getColumnNames($this->entityType->getKey('revision_translation_affected'))['value'];
$langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value'];
$revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value'];
$query = $this->database->select($this->getRevisionDataTable(), 'mlfr');
$query->fields('mlfr', [$id_field]);
$query->addExpression("MAX([mlfr].[$revision_field])", $revision_field);
$query->join($this->getRevisionTable(), 'mlr', "[mlfr].[$revision_field] = [mlr].[$revision_field] AND [mlr].[$revision_default_field] = 0");
$inner_select = $this->database->select($this->getRevisionDataTable(), 't');
$inner_select->condition("t.$rta_field", '1');
$inner_select->fields('t', [$id_field, $langcode_field]);
$inner_select->addExpression("MAX([t].[$revision_field])", $revision_field);
$inner_select
->groupBy("t.$id_field")
->groupBy("t.$langcode_field");
$query->join($inner_select, 'mr', "[mlfr].[$revision_field] = [mr].[$revision_field] AND [mlfr].[$langcode_field] = [mr].[$langcode_field]");
$query->groupBy("mlfr.$id_field");
return $query->execute()->fetchAllKeyed(1, 0);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for menu_link_content entity storage classes.
*/
interface MenuLinkContentStorageInterface extends ContentEntityStorageInterface {
/**
* Gets a list of menu link IDs with pending revisions.
*
* @return int[]
* An array of menu link IDs which have pending revisions, keyed by their
* revision IDs.
*
* @internal
*/
public function getMenuLinkIdsWithPendingRevisions();
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the menu_link_content schema handler.
*/
class MenuLinkContentStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == $this->storage->getBaseTable()) {
switch ($field_name) {
case 'rediscover':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
}
}
return $schema;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a menu link list builder.
*/
class MenuLinkListBuilder extends EntityListBuilder {
/**
* The redirect destination.
*
* @var \Drupal\Core\Routing\RedirectDestinationInterface
*/
protected $redirectDestination;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('redirect.destination')
);
}
/**
* Constructs a new instance.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
* The redirect destination.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, RedirectDestinationInterface $redirect_destination) {
parent::__construct($entity_type, $storage);
$this->redirectDestination = $redirect_destination;
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
$destination = $this->redirectDestination->get();
foreach ($operations as $key => $operation) {
$operations[$key]['query']['destination'] = $destination;
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function render() {
throw new \LogicException('This list builder can only provide operations. It does not build lists.');
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Drupal\menu_link_content\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a deriver for user entered paths of menu links.
*
* The assumption is that the number of manually entered menu links are lower
* compared to entity referenced ones.
*/
class MenuLinkContentDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* Constructs a MenuLinkContentDeriver instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, MenuLinkManagerInterface $menu_link_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->menuLinkManager = $menu_link_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('plugin.manager.menu.link')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
// Get all custom menu links which should be rediscovered.
$entity_ids = $this->entityTypeManager->getStorage('menu_link_content')->getQuery()
->accessCheck(FALSE)
->condition('rediscover', TRUE)
->execute();
$plugin_definitions = [];
$menu_link_content_entities = $this->entityTypeManager->getStorage('menu_link_content')->loadMultiple($entity_ids);
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link_content */
foreach ($menu_link_content_entities as $menu_link_content) {
$plugin_definitions[$menu_link_content->uuid()] = $menu_link_content->getPluginDefinition();
}
return $plugin_definitions;
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace Drupal\menu_link_content\Plugin\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\MenuLinkBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the menu link plugin for content menu links.
*/
class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInterface {
/**
* Entities IDs to load.
*
* It is an array of entity IDs keyed by entity IDs.
*
* @var array
*/
protected static $entityIdsToLoad = [];
/**
* {@inheritdoc}
*/
protected $overrideAllowed = [
'menu_name' => 1,
'parent' => 1,
'weight' => 1,
'expanded' => 1,
'enabled' => 1,
'title' => 1,
'description' => 1,
'route_name' => 1,
'route_parameters' => 1,
'url' => 1,
'options' => 1,
];
/**
* The menu link content entity connected to this plugin instance.
*
* @var \Drupal\menu_link_content\MenuLinkContentInterface
*/
protected $entity;
/**
* An array of entity operations links.
*
* @var array
*/
protected $listBuilderOperations;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new MenuLinkContent.
*
* @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\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, EntityRepositoryInterface $entity_repository) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
if (!empty($this->pluginDefinition['metadata']['entity_id'])) {
$entity_id = $this->pluginDefinition['metadata']['entity_id'];
// Builds a list of entity IDs to take advantage of the more efficient
// EntityStorageInterface::loadMultiple() in getEntity() at render time.
static::$entityIdsToLoad[$entity_id] = $entity_id;
}
$this->entityTypeManager = $entity_type_manager;
$this->languageManager = $language_manager;
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('language_manager'),
$container->get('entity.repository')
);
}
/**
* Loads the entity associated with this menu link.
*
* @return \Drupal\menu_link_content\MenuLinkContentInterface
* The menu link content entity.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the entity ID and UUID are both invalid or missing.
*/
public function getEntity(): MenuLinkContentInterface {
if (empty($this->entity)) {
$entity = NULL;
$storage = $this->entityTypeManager->getStorage('menu_link_content');
if (!empty($this->pluginDefinition['metadata']['entity_id'])) {
$entity_id = $this->pluginDefinition['metadata']['entity_id'];
// Make sure the current ID is in the list, since each plugin empties
// the list after calling loadMultiple(). Note that the list may include
// multiple IDs added earlier in each plugin's constructor.
static::$entityIdsToLoad[$entity_id] = $entity_id;
$entities = $storage->loadMultiple(array_values(static::$entityIdsToLoad));
$entity = $entities[$entity_id] ?? NULL;
static::$entityIdsToLoad = [];
}
if (!$entity) {
// Fallback to the loading by the UUID.
$uuid = $this->getUuid();
$entity = $this->entityRepository->loadEntityByUuid('menu_link_content', $uuid);
}
if (!$entity) {
throw new PluginException("Entity not found through the menu link plugin definition and could not fallback on UUID '$uuid'");
}
// Clone the entity object to avoid tampering with the static cache.
$this->entity = clone $entity;
$the_entity = $this->entityRepository->getTranslationFromContext($this->entity);
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $the_entity */
$this->entity = $the_entity;
$this->entity->setInsidePlugin();
}
return $this->entity;
}
/**
* {@inheritdoc}
*/
public function getTitle() {
// We only need to get the title from the actual entity if it may be a
// translation based on the current language context. This can only happen
// if the site is configured to be multilingual.
if ($this->languageManager->isMultilingual()) {
return $this->getEntity()->getTitle();
}
return $this->pluginDefinition['title'];
}
/**
* {@inheritdoc}
*/
public function getDescription() {
// We only need to get the description from the actual entity if it may be a
// translation based on the current language context. This can only happen
// if the site is configured to be multilingual.
if ($this->languageManager->isMultilingual()) {
return $this->getEntity()->getDescription();
}
return $this->pluginDefinition['description'];
}
/**
* {@inheritdoc}
*/
public function getDeleteRoute() {
$operations = $this->getListBuilderOperations();
return isset($operations['delete']) ? $operations['delete']['url'] : NULL;
}
/**
* {@inheritdoc}
*/
public function getEditRoute() {
$operations = $this->getListBuilderOperations();
return isset($operations['edit']) ? $operations['edit']['url'] : NULL;
}
/**
* {@inheritdoc}
*/
public function getTranslateRoute() {
$operations = $this->getListBuilderOperations();
return isset($operations['translate']) ? $operations['translate']['url'] : NULL;
}
/**
* Load entity operations from the list builder.
*
* @return array
* An array of operations.
*/
protected function getListBuilderOperations() {
if (is_null($this->listBuilderOperations)) {
$this->listBuilderOperations = $this->entityTypeManager
->getListBuilder($this->getEntity()->getEntityTypeId())
->getOperations($this->getEntity());
}
return $this->listBuilderOperations;
}
/**
* {@inheritdoc}
*/
public function getOperations(): array {
return $this->getListBuilderOperations();
}
/**
* Returns the unique ID representing the menu link.
*
* @return string
* The menu link ID.
*/
protected function getUuid() {
return $this->getDerivativeId();
}
/**
* {@inheritdoc}
*/
public function updateLink(array $new_definition_values, $persist) {
// Filter the list of updates to only those that are allowed.
$overrides = array_intersect_key($new_definition_values, $this->overrideAllowed);
// Update the definition.
$this->pluginDefinition = $overrides + $this->getPluginDefinition();
if ($persist) {
$entity = $this->getEntity();
foreach ($overrides as $key => $value) {
$entity->{$key}->value = $value;
}
$entity->save();
}
return $this->pluginDefinition;
}
/**
* {@inheritdoc}
*/
public function isDeletable() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
return $this->getEntity()->isTranslatable();
}
/**
* {@inheritdoc}
*/
public function deleteLink() {
$this->getEntity()->delete();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\menu_link_content\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
/**
* Validation constraint for changing the menu hierarchy in pending revisions.
*/
#[Constraint(
id: 'MenuTreeHierarchy',
label: new TranslatableMarkup('Menu tree hierarchy.', [], ['context' => 'Validation'])
)]
class MenuTreeHierarchyConstraint extends CompositeConstraintBase {
/**
* The default violation message.
*
* @var string
*/
public $message = 'You can only change the hierarchy for the <em>published</em> version of this menu link.';
/**
* {@inheritdoc}
*/
public function coversFields() {
return ['parent', 'weight'];
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Drupal\menu_link_content\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 menu link parents in pending revisions.
*/
class MenuTreeHierarchyConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* Creates a new MenuTreeHierarchyConstraintValidator 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($entity, Constraint $constraint) {
if ($entity && !$entity->isNew() && !$entity->isDefaultRevision()) {
$original = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
// Ensure that empty items do not affect the comparison checks below.
// @todo Remove this filtering when
// https://www.drupal.org/project/drupal/issues/3039031 is fixed.
$entity->parent->filterEmptyItems();
if (($entity->parent->isEmpty() !== $original->parent->isEmpty()) || !$entity->parent->equals($original->parent)) {
$this->context->buildViolation($constraint->message)
->atPath('menu_parent')
->addViolation();
}
if (!$entity->weight->equals($original->weight)) {
$this->context->buildViolation($constraint->message)
->atPath('weight')
->addViolation();
}
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Converts links options.
*
* Examples:
*
* @code
* process:
* link/options:
* plugin: link_options
* source: options
* @endcode
*
* This will convert the query options of the link.
*/
#[MigrateProcess(
id: "link_options",
handle_multiples: TRUE,
)]
class LinkOptions extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (isset($value['query'])) {
// If the query parameters are stored as a string (as in D6), convert it
// into an array.
if (is_string($value['query'])) {
parse_str($value['query'], $old_query);
}
else {
$old_query = $value['query'];
}
$value['query'] = $old_query;
}
return $value;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\process;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Generates an internal URI from the source value.
*
* Converts the source path value to an 'entity:', 'internal:' or 'base:' URI.
*
* Available configuration keys:
* - source: A source path to be converted into an URI.
* - validate_route: (optional) Whether the plugin should validate that the URI
* derived from the source link path has a valid Drupal route.
* - TRUE: Throw a MigrateException if the resulting URI is not routed. This
* value is the default.
* - FALSE: Return the URI for the unrouted path.
*
* Examples:
*
* @code
* process:
* link/uri:
* plugin: link_uri
* validate_route: false
* source: link_path
* @endcode
*
* This will set the uri property of link to the internal notation of link_path
* without validating if the resulting URI is valid. For example, if the
* 'link_path' property is 'node/12', the uri property value of link will be
* 'entity:node/12'.
*/
#[MigrateProcess('link_uri')]
class LinkUri extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The entity type manager, used to fetch entity link templates.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a LinkUri object.
*
* @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\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager, used to fetch entity link templates.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
$configuration += [
'validate_route' => TRUE,
];
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$path = ltrim($value, '/');
if (parse_url($path, PHP_URL_SCHEME) === NULL) {
if ($path == '<front>') {
$path = '';
}
elseif ($path == '<nolink>') {
return 'route:<nolink>';
}
$path = 'internal:/' . $path;
// Convert entity URIs to the entity scheme, if the path matches a route
// of the form "entity.$entity_type_id.canonical".
// @see \Drupal\Core\Url::fromEntityUri()
$url = Url::fromUri($path);
if ($url->isRouted()) {
$route_name = $url->getRouteName();
foreach (array_keys($this->entityTypeManager->getDefinitions()) as $entity_type_id) {
if ($route_name == "entity.$entity_type_id.canonical" && isset($url->getRouteParameters()[$entity_type_id])) {
return "entity:$entity_type_id/" . $url->getRouteParameters()[$entity_type_id];
}
}
}
else {
// If the URL is not routed, we might want to get something back to do
// other processing. If this is the case, the "validate_route"
// configuration option can be set to FALSE to return the URI.
if (!$this->configuration['validate_route']) {
return $url->getUri();
}
else {
throw new MigrateException(sprintf('The path "%s" failed validation.', $path));
}
}
}
return $path;
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\source;
use Drupal\Component\Utility\Unicode;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;
// cspell:ignore mlid objectid plid textgroup tsid
/**
* Drupal 6/7 menu link source from database.
*
* Available configuration keys:
* - menu_name: (optional) The menu name(s) to filter menu links from the source
* can be a string or an array. If not declared then menu links of all menus
* are retrieved.
*
* Examples:
*
* @code
* source:
* plugin: menu_link
* menu_name: main-menu
* @endcode
*
* In this example menu links of main-menu are retrieved from the source
* database.
*
* @code
* source:
* plugin: menu_link
* menu_name: [main-menu, navigation]
* @endcode
*
* In this example menu links of main-menu and navigation menus are retrieved
* from the source database.
*
* For additional configuration keys, refer to the parent classes:
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "menu_link",
* source_module = "menu"
* )
*/
class MenuLink extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('menu_links', 'ml')
->fields('ml')
// Shortcut set links are migrated by the d7_shortcut migration.
// Shortcuts are not used in Drupal 6.
// @see Drupal\shortcut\Plugin\migrate\source\d7\Shortcut::query()
->condition('ml.menu_name', 'shortcut-set-%', 'NOT LIKE');
$and = $query->andConditionGroup()
->condition('ml.module', 'menu')
->condition('ml.router_path', ['admin/build/menu-customize/%', 'admin/structure/menu/manage/%'], 'NOT IN');
$condition = $query->orConditionGroup()
->condition('ml.customized', 1)
->condition($and);
$query->condition($condition);
if (isset($this->configuration['menu_name'])) {
$query->condition('ml.menu_name', (array) $this->configuration['menu_name'], 'IN');
}
$query->leftJoin('menu_links', 'pl', '[ml].[plid] = [pl].[mlid]');
$query->addField('pl', 'link_path', 'parent_link_path');
$query->orderBy('ml.depth');
$query->orderby('ml.mlid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'menu_name' => $this->t("The menu name. All links with the same menu name (such as 'navigation') are part of the same menu."),
'mlid' => $this->t('The menu link ID (mlid) is the integer primary key.'),
'plid' => $this->t('The parent link ID (plid) is the mlid of the link above in the hierarchy, or zero if the link is at the top level in its menu.'),
'link_path' => $this->t('The Drupal path or external path this link points to.'),
'router_path' => $this->t('For links corresponding to a Drupal path (external = 0), this connects the link to a {menu_router}.path for joins.'),
'link_title' => $this->t('The text displayed for the link, which may be modified by a title callback stored in {menu_router}.'),
'options' => $this->t('A serialized array of options to set on the URL, such as a query string or HTML attributes.'),
'module' => $this->t('The name of the module that generated this link.'),
'hidden' => $this->t('A flag for whether the link should be rendered in menus. (1 = a disabled menu link that may be shown on admin screens, -1 = a menu callback, 0 = a normal, visible link)'),
'external' => $this->t('A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).'),
'has_children' => $this->t('Flag indicating whether any links have this link as a parent (1 = children exist, 0 = no children).'),
'expanded' => $this->t('Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded)'),
'weight' => $this->t('Link weight among links in the same menu at the same depth.'),
'depth' => $this->t('The depth relative to the top level. A link with plid == 0 will have depth == 1.'),
'customized' => $this->t('A flag to indicate that the user has manually created or edited the link (1 = customized, 0 = not customized).'),
'p1' => $this->t('The first mlid in the materialized path. If N = depth, then pN must equal the mlid. If depth > 1 then p(N-1) must equal the plid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.'),
'p2' => $this->t('The second mlid in the materialized path. See p1.'),
'p3' => $this->t('The third mlid in the materialized path. See p1.'),
'p4' => $this->t('The fourth mlid in the materialized path. See p1.'),
'p5' => $this->t('The fifth mlid in the materialized path. See p1.'),
'p6' => $this->t('The sixth mlid in the materialized path. See p1.'),
'p7' => $this->t('The seventh mlid in the materialized path. See p1.'),
'p8' => $this->t('The eighth mlid in the materialized path. See p1.'),
'p9' => $this->t('The ninth mlid in the materialized path. See p1.'),
'updated' => $this->t('Flag that indicates that this link was generated during the update from Drupal 5.'),
];
// The database connection may not exist, for example, when building
// the Migrate Message form.
if ($source_database = $this->database) {
$schema = $source_database->schema();
if ($schema->fieldExists('menu_links', 'language')) {
$fields['language'] = $this->t("Menu link language code.");
}
if ($schema->fieldExists('menu_links', 'i18n_tsid')) {
$fields['i18n_tsid'] = $this->t("Translation set id.");
}
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// In Drupal 7 a language neutral menu_link can be translated. The menu
// link is treated as if it is in the site default language. So, here
// we look to see if this menu link has a translation and if so, the
// language is changed to the default language. With the language set
// the entity API will allow the saving of the translations.
if ($row->hasSourceProperty('language') &&
$row->getSourceProperty('language') == 'und' &&
$this->hasTranslation($row->getSourceProperty('mlid'))) {
$default_language = $this->variableGet('language_default', (object) ['language' => 'und']);
$default_language = $default_language->language;
$row->setSourceProperty('language', $default_language);
}
// If this menu link is part of translation set skip the translations. The
// translations are migrated in d7_menu_link_localized.yml.
$row->setSourceProperty('skip_translation', TRUE);
if ($row->hasSourceProperty('i18n_tsid') && $row->getSourceProperty('i18n_tsid') != 0) {
$source_mlid = $this->select('menu_links', 'ml')
->fields('ml', ['mlid'])
->condition('i18n_tsid', $row->getSourceProperty('i18n_tsid'))
->orderBy('mlid')
->range(0, 1)
->execute()
->fetchField();
if ($source_mlid !== $row->getSourceProperty('mlid')) {
$row->setSourceProperty('skip_translation', FALSE);
}
}
// In Drupal 6 the language for the menu is in the options array. Set
// property 'is_localized' so that the process pipeline can determine if
// the menu link is localize or not.
$row->setSourceProperty('is_localized', NULL);
$default_language = $this->variableGet('language_default', (object) ['language' => 'und']);
$default_language = $default_language->language;
$options = unserialize($row->getSourceProperty('options'));
if (isset($options['langcode'])) {
if ($options['langcode'] != $default_language) {
$row->setSourceProperty('language', $options['langcode']);
$row->setSourceProperty('is_localized', 'localized');
}
}
$row->setSourceProperty('options', unserialize($row->getSourceProperty('options')));
$row->setSourceProperty('enabled', !$row->getSourceProperty('hidden'));
$description = $row->getSourceProperty('options/attributes/title');
if ($description !== NULL) {
$row->setSourceProperty('description', Unicode::truncate($description, 255));
}
return parent::prepareRow($row);
}
/**
* Determines if this menu_link has an i18n translation.
*
* @param string $mlid
* The menu id.
*
* @return bool
* True if the menu_link has an i18n translation.
*/
public function hasTranslation($mlid) {
if ($this->getDatabase()->schema()->tableExists('i18n_string')) {
$results = $this->select('i18n_string', 'i18n')
->fields('i18n')
->condition('textgroup', 'menu')
->condition('type', 'item')
->condition('objectid', $mlid)
->execute()
->fetchAll();
if ($results) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['mlid']['type'] = 'integer';
$ids['mlid']['alias'] = 'ml';
return $ids;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\source\d6;
use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
use Drupal\migrate\Row;
use Drupal\menu_link_content\Plugin\migrate\source\MenuLink;
// cspell:ignore mlid
/**
* Drupal 6 i18n menu link translations source from database.
*
* @MigrateSource(
* id = "d6_menu_link_translation",
* source_module = "i18nmenu"
* )
*/
class MenuLinkTranslation extends MenuLink {
use I18nQueryTrait;
/**
* Drupal 6 table names.
*/
const I18N_STRING_TABLE = 'i18n_strings';
/**
* {@inheritdoc}
*/
public function query() {
// Ideally, the query would return rows for each language for each menu link
// with the translations for both the title and description or just the
// title translation or just the description translation. That query quickly
// became complex and would be difficult to maintain.
// Therefore, build a query based on i18nstrings table where each row has
// the translation for only one property, either title or description. The
// method prepareRow() is then used to obtain the translation for the other
// property.
// The query starts with the same query as menu_link.
$query = parent::query();
// Add in the property, which is either title or description. Cast the mlid
// to text so PostgreSQL can make the join.
$query->leftJoin(static::I18N_STRING_TABLE, 'i18n', 'CAST([ml].[mlid] AS CHAR(255)) = [i18n].[objectid]');
$query->addField('i18n', 'lid');
$query->addField('i18n', 'property');
// Add in the translation for the property.
$query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]');
$query->addField('lt', 'language');
$query->addField('lt', 'translation');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
if (!parent::prepareRow($row)) {
return FALSE;
}
// Save the translation for this property.
$property_in_row = $row->getSourceProperty('property');
// Set the i18n string table for use in I18nQueryTrait.
$this->i18nStringTable = static::I18N_STRING_TABLE;
// Get the translation for the property not already in the row and save it
// in the row.
$property_not_in_row = ($property_in_row == 'title') ? 'description' : 'title';
return $this->getPropertyNotInRowTranslation($row, $property_not_in_row, 'mlid', $this->idMap);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'language' => $this->t('Language for this menu.'),
'title_translated' => $this->t('Menu link title translation.'),
'description_translated' => $this->t('Menu link description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
return parent::getIds() + $ids;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\source\d7;
use Drupal\menu_link_content\Plugin\migrate\source\MenuLink;
use Drupal\migrate\Row;
// cspell:ignore mlid tsid
/**
* Drupal 7 localized menu link translations source from database.
*
* @MigrateSource(
* id = "d7_menu_link_localized",
* source_module = "i18n_menu"
* )
*/
class MenuLinkLocalized extends MenuLink {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query->condition('ml.i18n_tsid', '0', '<>');
// The first row in a translation set is the source.
$query->orderBy('ml.i18n_tsid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'ml_language' => $this->t('Menu link ID of the source language menu link.'),
'skip_source_translation' => $this->t('Menu link description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('skip_source_translation', TRUE);
// Get the mlid for the source menu_link.
$source_mlid = $this->select('menu_links', 'ml')
->fields('ml', ['mlid'])
->condition('i18n_tsid', $row->getSourceProperty('i18n_tsid'))
->orderBy('mlid')
->range(0, 1)
->execute()
->fetchField();
if ($source_mlid == $row->getSourceProperty('mlid')) {
$row->setSourceProperty('skip_source_translation', FALSE);
}
$row->setSourceProperty('mlid', $source_mlid);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'ml';
return parent::getIds() + $ids;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\source\d7;
use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
use Drupal\migrate\Row;
use Drupal\menu_link_content\Plugin\migrate\source\MenuLink;
// cspell:ignore mlid objectid textgroup
/**
* Drupal 7 i18n menu link translations source from database.
*
* @MigrateSource(
* id = "d7_menu_link_translation",
* source_module = "i18n_menu"
* )
*/
class MenuLinkTranslation extends MenuLink {
use I18nQueryTrait;
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
// Add in the property, which is either title or description. Cast the mlid
// to text so PostgreSQL can make the join.
$query->leftJoin('i18n_string', 'i18n', 'CAST([ml].[mlid] AS CHAR(255)) = [i18n].[objectid]');
$query->fields('i18n', ['lid', 'objectid', 'property', 'textgroup'])
->condition('i18n.textgroup', 'menu')
->condition('i18n.type', 'item');
// Add in the translation for the property.
$query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]');
$query->addField('lt', 'language', 'lt_language');
$query->fields('lt', ['translation']);
$query->isNotNull('lt.language');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
if (!parent::prepareRow($row)) {
return FALSE;
}
// Put the language on the row as 'language'.
$row->setSourceProperty('language', $row->getSourceProperty('lt_language'));
// Save the translation for this property.
$property_in_row = $row->getSourceProperty('property');
// Set the i18n string table for use in I18nQueryTrait.
$this->i18nStringTable = 'i18n_string';
// Get the translation for the property not already in the row and save it
// in the row.
$property_not_in_row = ($property_in_row == 'title') ? 'description' : 'title';
return $this->getPropertyNotInRowTranslation($row, $property_not_in_row, 'mlid', $this->idMap);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'title_translated' => $this->t('Menu link title translation.'),
'description_translated' => $this->t('Menu link description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
return parent::getIds() + $ids;
}
}

View File

@@ -0,0 +1,10 @@
name: 'Menu link content dynamic route'
type: module
hidden: true
dependencies:
- drupal:menu_link_content
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,2 @@
route_callbacks:
- 'Drupal\menu_link_content_dynamic_route\Routes::dynamic'

View File

@@ -0,0 +1,14 @@
<?php
namespace Drupal\menu_link_content_dynamic_route;
/**
* Provides dynamic routes for test purposes.
*/
class Routes {
public function dynamic() {
return \Drupal::state()->get('menu_link_content_dynamic_route.routes', []);
}
}

View File

@@ -0,0 +1,12 @@
name: 'Menu Operations Link Test'
type: module
hidden: true
description: "Provides testing hooks to alter menu link content entity's operations links."
dependencies:
- drupal:menu_link_content
- drupal:menu_ui
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,36 @@
<?php
/**
* @file
* Primary module hooks for Menu Operations Link Test module.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Core\Url;
/**
* Implements hook_entity_operation_alter().
*/
function menu_operations_link_test_entity_operation_alter(array &$operations, EntityInterface $entity) {
if (!$entity instanceof MenuLinkContent) {
return;
}
// Alter the title of the edit link appearing in operations menu.
$operations['edit']['title'] = t('Altered Edit Title');
}
/**
* Implements hook_entity_operation().
*/
function menu_operations_link_test_entity_operation(EntityInterface $entity) {
if (!$entity instanceof MenuLinkContent) {
return;
}
$operations['custom_operation'] = [
'title' => t('Custom Home'),
'weight' => 20,
'url' => Url::fromRoute('<front>'),
];
return $operations;
}

View File

@@ -0,0 +1,8 @@
name: 'Outbound route/path processing'
type: module
hidden: true
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,5 @@
outbound_processing_test.route.csrf:
path: '/outbound_processing_test/route/csrf'
requirements:
_access: 'TRUE'
_csrf_token: 'TRUE'

View File

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

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\system\Entity\Menu;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the menu link content delete UI.
*
* @group Menu
*/
class MenuLinkContentDeleteFormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['menu_link_content'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$web_user = $this->drupalCreateUser(['administer menu']);
$this->drupalLogin($web_user);
}
/**
* Tests the MenuLinkContentDeleteForm class.
*/
public function testMenuLinkContentDeleteForm(): void {
// Add new menu item.
$this->drupalGet('admin/structure/menu/manage/admin/add');
$this->submitForm([
'title[0][value]' => 'Front page',
'link[0][uri]' => '<front>',
], 'Save');
$this->assertSession()->pageTextContains('The menu link has been saved.');
$menu_link = MenuLinkContent::load(1);
$this->drupalGet($menu_link->toUrl('delete-form'));
$this->assertSession()->pageTextContains("Are you sure you want to delete the custom menu link {$menu_link->label()}?");
$this->assertSession()->linkExists('Cancel');
// Make sure cancel link points to link edit
$this->assertSession()->linkByHrefExists($menu_link->toUrl('edit-form')->toString());
\Drupal::service('module_installer')->install(['menu_ui']);
// Make sure cancel URL points to menu_ui route now.
$this->drupalGet($menu_link->toUrl('delete-form'));
$menu = Menu::load($menu_link->getMenuName());
$this->assertSession()->linkByHrefExists($menu->toUrl('edit-form')->toString());
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The menu link {$menu_link->label()} has been deleted.");
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the menu link content UI.
*
* @group Menu
*/
class MenuLinkContentFormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'menu_link_content',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with 'administer menu' and 'link to any page' permission.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* User with only 'administer menu' permission.
*
* @var \Drupal\user\Entity\User
*/
protected $basicUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer menu',
'link to any page',
]);
$this->basicUser = $this->drupalCreateUser(['administer menu']);
$this->drupalLogin($this->adminUser);
}
/**
* Tests the 'link to any page' permission for a restricted page.
*/
public function testMenuLinkContentFormLinkToAnyPage(): void {
$menu_link = MenuLinkContent::create([
'title' => 'Menu link test',
'provider' => 'menu_link_content',
'menu_name' => 'admin',
'link' => ['uri' => 'internal:/user/login'],
]);
$menu_link->save();
// The user should be able to edit a menu link to the page, even though
// the user cannot access the page itself.
$this->drupalGet('/admin/structure/menu/item/' . $menu_link->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
// Test that other menus are available when editing existing menu link.
$this->assertSession()->optionExists('edit-menu-parent', 'main:');
$this->drupalLogin($this->basicUser);
$this->drupalGet('/admin/structure/menu/item/' . $menu_link->id() . '/edit');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests the MenuLinkContentForm class.
*/
public function testMenuLinkContentForm(): void {
$this->drupalGet('admin/structure/menu/manage/admin/add');
// Test that other menus are not available when creating a new menu link.
$this->assertSession()->optionNotExists('edit-menu-parent', 'main:');
$option = $this->assertSession()->optionExists('edit-menu-parent', 'admin:');
$this->assertTrue($option->isSelected());
// Test that the field description is present.
$this->assertSession()->pageTextContains('The location this menu link points to.');
$this->submitForm([
'title[0][value]' => 'Front page',
'link[0][uri]' => '<front>',
], 'Save');
$this->assertSession()->pageTextContains('The menu link has been saved.');
}
/**
* Tests validation for the MenuLinkContentForm class.
*/
public function testMenuLinkContentFormValidation(): void {
$this->drupalGet('admin/structure/menu/manage/admin/add');
$this->submitForm([
'title[0][value]' => 'Test page',
'link[0][uri]' => '<test>',
], 'Save');
$this->assertSession()->pageTextContains('Manually entered paths should start with one of the following characters: / ? #');
}
/**
* Tests the operations links alter related functional for menu_link_content.
*/
public function testMenuLinkContentOperationsLink(): void {
\Drupal::service('module_installer')->install(['menu_operations_link_test']);
$menu_link = MenuLinkContent::create([
'title' => 'Menu link test',
'provider' => 'menu_link_content',
'menu_name' => 'main',
'link' => ['uri' => 'internal:/user/login'],
]);
$menu_link->save();
// When we are on the listing page, we should be able to see the altered
// values by alter hook in the operations link menu.
$this->drupalGet('/admin/structure/menu/manage/main');
$this->assertSession()->linkExists('Altered Edit Title');
$this->assertSession()->linkExists('Custom Home');
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional;
use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
* Tests the menu link content translation UI.
*
* @group Menu
*/
class MenuLinkContentTranslationUITest extends ContentTranslationUITestBase {
/**
* {@inheritdoc}
*/
protected $defaultCacheContexts = ['languages:language_interface', 'session', 'theme', 'url.path', 'url.query_args', 'user.permissions', 'user.roles:authenticated'];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'language',
'content_translation',
'menu_link_content',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->entityTypeId = 'menu_link_content';
$this->bundle = 'menu_link_content';
parent::setUp();
$this->doSetup();
}
/**
* {@inheritdoc}
*/
protected function getTranslatorPermissions() {
return array_merge(parent::getTranslatorPermissions(), ['administer menu']);
}
/**
* {@inheritdoc}
*/
protected function getAdministratorPermissions() {
return array_merge(parent::getAdministratorPermissions(), ['administer themes', 'view the administration theme']);
}
/**
* {@inheritdoc}
*/
protected function createEntity($values, $langcode, $bundle_name = NULL) {
$values['menu_name'] = 'tools';
$values['link']['uri'] = 'internal:/admin/structure/menu';
$values['title'] = 'Test title';
return parent::createEntity($values, $langcode, $bundle_name);
}
/**
* Ensure that a translate link can be found on the menu edit form.
*/
public function testTranslationLinkOnMenuEditForm(): void {
$this->drupalGet('admin/structure/menu/manage/tools');
$this->assertSession()->linkNotExists('Translate');
$menu_link_content = MenuLinkContent::create([
'menu_name' => 'tools',
'link' => ['uri' => 'internal:/admin/structure/menu'],
'title' => 'Link test',
]);
$menu_link_content->save();
$this->drupalGet('admin/structure/menu/manage/tools');
$this->assertSession()->linkExists('Translate');
}
/**
* Tests that translation page inherits admin status of edit page.
*/
public function testTranslationLinkTheme(): void {
$this->drupalLogin($this->administrator);
$entityId = $this->createEntity([], 'en');
// Set up the default admin theme to test.
$this->container->get('theme_installer')->install(['claro']);
$edit = [];
$edit['admin_theme'] = 'claro';
$this->drupalGet('admin/appearance');
$this->submitForm($edit, 'Save configuration');
// Check that edit uses the admin theme.
$this->drupalGet('admin/structure/menu/item/' . $entityId . '/edit');
$this->assertSession()->responseContains('core/themes/claro/css/base/elements.css');
// Check that translation uses admin theme as well.
$this->drupalGet('admin/structure/menu/item/' . $entityId . '/edit/translations');
$this->assertSession()->responseContains('core/themes/claro/css/base/elements.css');
}
/**
* {@inheritdoc}
*/
protected function doTestTranslationEdit() {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
foreach ($this->langcodes as $langcode) {
// We only want to test the title for non-english translations.
if ($langcode != 'en') {
$options = ['language' => $languages[$langcode]];
$url = $entity->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->assertSession()->pageTextContains("{$entity->getTranslation($langcode)->label()} [{$languages[$langcode]->getName()} translation]");
}
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class MenuLinkContentJsonAnonTest extends MenuLinkContentResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class MenuLinkContentJsonBasicAuthTest extends MenuLinkContentResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class MenuLinkContentJsonCookieTest extends MenuLinkContentResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional\Rest;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* ResourceTestBase for MenuLinkContent entity.
*/
abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_link_content'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'menu_link_content';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'changed' => NULL,
];
/**
* The MenuLinkContent entity.
*
* @var \Drupal\menu_link_content\MenuLinkContentInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
case 'POST':
case 'PATCH':
case 'DELETE':
$this->grantPermissionsToTestedRole(['administer menu']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$menu_link = MenuLinkContent::create([
'id' => 'llama',
'title' => 'Llama Gabilondo',
'description' => 'Llama Gabilondo',
'link' => [
'uri' => 'https://nl.wikipedia.org/wiki/Llama',
'options' => [
'fragment' => 'a-fragment',
'attributes' => [
'class' => ['example-class'],
],
],
],
'weight' => 0,
'menu_name' => 'main',
]);
$menu_link->save();
return $menu_link;
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'title' => [
[
'value' => 'Drama llama',
],
],
'link' => [
[
'uri' => 'http://www.urbandictionary.com/define.php?term=drama%20llama',
'options' => [
'fragment' => 'a-fragment',
'attributes' => [
'class' => ['example-class'],
],
],
],
],
'bundle' => [
[
'value' => 'menu_link_content',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'uuid' => [
[
'value' => $this->entity->uuid(),
],
],
'id' => [
[
'value' => 1,
],
],
'revision_id' => [
[
'value' => 1,
],
],
'title' => [
[
'value' => 'Llama Gabilondo',
],
],
'link' => [
[
'uri' => 'https://nl.wikipedia.org/wiki/Llama',
'title' => NULL,
'options' => [
'fragment' => 'a-fragment',
'attributes' => [
'class' => ['example-class'],
],
],
],
],
'weight' => [
[
'value' => 0,
],
],
'menu_name' => [
[
'value' => 'main',
],
],
'langcode' => [
[
'value' => 'en',
],
],
'bundle' => [
[
'value' => 'menu_link_content',
],
],
'description' => [
[
'value' => 'Llama Gabilondo',
],
],
'external' => [
[
'value' => FALSE,
],
],
'rediscover' => [
[
'value' => FALSE,
],
],
'expanded' => [
[
'value' => FALSE,
],
],
'enabled' => [
[
'value' => TRUE,
],
],
'changed' => [
[
'value' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())
->setTimezone(new \DateTimeZone('UTC'))
->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'default_langcode' => [
[
'value' => TRUE,
],
],
'parent' => [],
'revision_created' => [
[
'value' => (new \DateTime())->setTimestamp((int) $this->entity->getRevisionCreationTime())
->setTimezone(new \DateTimeZone('UTC'))
->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'revision_user' => [],
'revision_log_message' => [],
'revision_translation_affected' => [
[
'value' => TRUE,
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'DELETE':
return "The 'administer menu' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class MenuLinkContentXmlAnonTest extends MenuLinkContentResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class MenuLinkContentXmlBasicAuthTest extends MenuLinkContentResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class MenuLinkContentXmlCookieTest extends MenuLinkContentResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\KernelTests\KernelTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\Entity\User;
use Drupal\Core\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\Route;
/**
* Ensures that rendered menu links bubble the necessary bubbleable metadata.
*
* This for outbound path/route processing.
*
* @group menu_link_content
*/
class MenuLinkContentCacheabilityBubblingTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'menu_link_content',
'system',
'link',
'outbound_processing_test',
'url_alter_test',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser(['uid' => 0]);
$this->installEntitySchema('menu_link_content');
// Ensure that the weight of module_link_content is higher than system.
// @see menu_link_content_install()
module_set_weight('menu_link_content', 1);
}
/**
* Tests bubbleable metadata of menu links' outbound route/path processing.
*/
public function testOutboundPathAndRouteProcessing(): void {
$request_stack = \Drupal::requestStack();
/** @var \Symfony\Component\Routing\RequestContext $request_context */
$request_context = \Drupal::service('router.request_context');
$request = Request::create('/');
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, '<front>');
$request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('/'));
$request->setSession(new Session(new MockArraySessionStorage()));
$request_stack->push($request);
$request_context->fromRequest($request);
$menu_tree = \Drupal::menuTree();
$renderer = \Drupal::service('renderer');
$default_menu_cacheability = (new BubbleableMetadata())
->setCacheMaxAge(Cache::PERMANENT)
->setCacheTags(['config:system.menu.tools'])
->setCacheContexts(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']);
User::create(['uid' => 1, 'name' => $this->randomString()])->save();
User::create(['uid' => 2, 'name' => $this->randomString()])->save();
// Five test cases, four asserting one outbound path/route processor, and
// together covering one of each:
// - no cacheability metadata,
// - a cache context,
// - a cache tag,
// - a cache max-age.
// Plus an additional test case to verify that multiple links adding
// cacheability metadata of the same type is working (two links with cache
// tags).
$test_cases = [
// \Drupal\Core\RouteProcessor\RouteProcessorCurrent: 'route' cache context.
[
'uri' => 'route:<current>',
'cacheability' => (new BubbleableMetadata())->setCacheContexts(['route']),
],
// \Drupal\Core\Access\RouteProcessorCsrf: placeholder.
[
'uri' => 'route:outbound_processing_test.route.csrf',
'cacheability' => (new BubbleableMetadata())->setCacheContexts(['session'])->setAttachments(['placeholders' => []]),
],
// \Drupal\Core\PathProcessor\PathProcessorFront: permanently cacheable.
[
'uri' => 'internal:/',
'cacheability' => (new BubbleableMetadata()),
],
// \Drupal\url_alter_test\PathProcessorTest: user entity's cache tags.
[
'uri' => 'internal:/user/1',
'cacheability' => (new BubbleableMetadata())->setCacheTags(User::load(1)->getCacheTags()),
],
[
'uri' => 'internal:/user/2',
'cacheability' => (new BubbleableMetadata())->setCacheTags(User::load(2)->getCacheTags()),
],
];
// Test each expectation individually.
foreach ($test_cases as $expectation) {
$menu_link_content = MenuLinkContent::create([
'link' => ['uri' => $expectation['uri']],
'menu_name' => 'tools',
'title' => 'Link test',
]);
$menu_link_content->save();
$tree = $menu_tree->load('tools', new MenuTreeParameters());
$build = $menu_tree->build($tree);
$renderer->renderRoot($build);
$expected_cacheability = $default_menu_cacheability->merge($expectation['cacheability']);
$this->assertEqualsCanonicalizing($expected_cacheability, BubbleableMetadata::createFromRenderArray($build));
$menu_link_content->delete();
}
// Now test them all together in one menu: the rendered menu's cacheability
// metadata should be the combination of the cacheability of all links, and
// thus of all tested outbound path & route processors.
$expected_cacheability = new BubbleableMetadata();
foreach ($test_cases as $expectation) {
$menu_link_content = MenuLinkContent::create([
'link' => ['uri' => $expectation['uri']],
'menu_name' => 'tools',
'title' => 'Link test',
]);
$menu_link_content->save();
$expected_cacheability = $expected_cacheability->merge($expectation['cacheability']);
}
$tree = $menu_tree->load('tools', new MenuTreeParameters());
$build = $menu_tree->build($tree);
$renderer->renderRoot($build);
$expected_cacheability = $expected_cacheability->merge($default_menu_cacheability);
$this->assertEqualsCanonicalizing($expected_cacheability, BubbleableMetadata::createFromRenderArray($build));
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\MenuParentFormSelectorInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\menu_link_content\Form\MenuLinkContentForm;
/**
* Tests the deprecation notices of the menu_link_content module.
*
* @group menu_link_content
* @group legacy
*/
class MenuLinkContentDeprecationsTest extends KernelTestBase {
/**
* Tests the deprecation in the \Drupal\menu_link_content\Form\MenuLinkContentForm constructor.
*/
public function testMenuLinkContentFormConstructorDeprecation(): void {
$entity_repository = $this->prophesize(EntityRepositoryInterface::class);
$menu_parent_form_selector = $this->prophesize(MenuParentFormSelectorInterface::class);
$language_manager = $this->prophesize(LanguageManagerInterface::class);
$path_validator = $this->prophesize(PathValidatorInterface::class);
$entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class);
$time = $this->prophesize(TimeInterface::class);
$this->expectDeprecation('Calling Drupal\menu_link_content\Form\MenuLinkContentForm::__construct() with the $language_manager argument is deprecated in drupal:10.2.0 and is removed in drupal:11.0.0. See https://www.drupal.org/node/3325178');
new MenuLinkContentForm(
$entity_repository->reveal(),
$menu_parent_form_selector->reveal(),
$language_manager->reveal(),
$path_validator->reveal(),
$entity_type_bundle_info->reveal(),
$time->reveal()
);
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Routing\Route;
/**
* Tests the menu link content deriver.
*
* @group menu_link_content
*/
class MenuLinkContentDeriverTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'menu_link_content',
'link',
'system',
'menu_link_content_dynamic_route',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content');
}
/**
* Tests the rediscovering.
*/
public function testRediscover(): void {
\Drupal::state()->set('menu_link_content_dynamic_route.routes', [
'route_name_1' => new Route('/example-path'),
]);
\Drupal::service('router.builder')->rebuild();
// Set up a custom menu link pointing to a specific path.
$parent = MenuLinkContent::create([
'title' => '<script>alert("Welcome to the discovered jungle!")</script>',
'link' => [['uri' => 'internal:/example-path']],
'menu_name' => 'tools',
]);
$parent->save();
$menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertCount(1, $menu_tree);
/** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */
$tree_element = reset($menu_tree);
$this->assertEquals('route_name_1', $tree_element->link->getRouteName());
// Change the underlying route and trigger the rediscovering.
\Drupal::state()->set('menu_link_content_dynamic_route.routes', [
'route_name_2' => new Route('/example-path'),
]);
\Drupal::service('router.builder')->rebuild();
// Ensure that the new route name / parameters are captured by the tree.
$menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertCount(1, $menu_tree);
/** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */
$tree_element = reset($menu_tree);
$this->assertEquals('route_name_2', $tree_element->link->getRouteName());
$title = $tree_element->link->getTitle();
$this->assertNotInstanceOf(TranslatableMarkup::class, $title);
$this->assertSame('<script>alert("Welcome to the discovered jungle!")</script>', $title);
// Create a hierarchy.
\Drupal::state()->set('menu_link_content_dynamic_route.routes', [
'route_name_1' => new Route('/example-path'),
'route_name_2' => new Route('/example-path/child'),
]);
$child = MenuLinkContent::create([
'title' => 'Child',
'link' => [['uri' => 'entity:/example-path/child']],
'menu_name' => 'tools',
'parent' => 'menu_link_content:' . $parent->uuid(),
]);
$child->save();
$parent->set('link', [['uri' => 'entity:/example-path']]);
$parent->save();
$menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertCount(1, $menu_tree);
/** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */
$tree_element = reset($menu_tree);
$this->assertTrue($tree_element->hasChildren);
$this->assertCount(1, $tree_element->subtree);
// Edit child element link to use 'internal' instead of 'entity'.
$child->set('link', [['uri' => 'internal:/example-path/child']]);
$child->save();
\Drupal::service('plugin.manager.menu.link')->rebuild();
$menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertCount(1, $menu_tree);
/** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */
$tree_element = reset($menu_tree);
$this->assertTrue($tree_element->hasChildren);
$this->assertCount(1, $tree_element->subtree);
}
}

View File

@@ -0,0 +1,480 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\entity_test\Entity\EntityTestExternal;
use Drupal\KernelTests\KernelTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent as MenuLinkContentPlugin;
use Drupal\system\Entity\Menu;
use Drupal\user\Entity\User;
/**
* Tests handling of menu links hierarchies.
*
* @group Menu
*/
class MenuLinksTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'link',
'menu_link_content',
'router_test',
'system',
'user',
];
/**
* The menu link plugin manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->menuLinkManager = \Drupal::service('plugin.manager.menu.link');
$this->installSchema('user', ['users_data']);
$this->installEntitySchema('entity_test_external');
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('user');
Menu::create([
'id' => 'menu-test',
'label' => 'Test menu',
'description' => 'Description text',
])->save();
}
/**
* Create a simple hierarchy of links.
*/
public function createLinkHierarchy($module = 'menu_test') {
// First remove all the menu links in the menu.
$this->menuLinkManager->deleteLinksInMenu('menu-test');
// Then create a simple link hierarchy:
// - parent
// - child-1
// - child-1-1
// - child-1-2
// - child-2
$base_options = [
'title' => 'Menu link test',
'provider' => $module,
'menu_name' => 'menu-test',
];
$parent = $base_options + [
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent'],
];
$link = MenuLinkContent::create($parent);
$link->save();
$links['parent'] = $link->getPluginId();
$child_1 = $base_options + [
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child'],
'parent' => $links['parent'],
];
$link = MenuLinkContent::create($child_1);
$link->save();
$links['child-1'] = $link->getPluginId();
$child_1_1 = $base_options + [
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child2/child'],
'parent' => $links['child-1'],
];
$link = MenuLinkContent::create($child_1_1);
$link->save();
$links['child-1-1'] = $link->getPluginId();
$child_1_2 = $base_options + [
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child2/child'],
'parent' => $links['child-1'],
];
$link = MenuLinkContent::create($child_1_2);
$link->save();
$links['child-1-2'] = $link->getPluginId();
$child_2 = $base_options + [
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child'],
'parent' => $links['parent'],
];
$link = MenuLinkContent::create($child_2);
$link->save();
$links['child-2'] = $link->getPluginId();
return $links;
}
/**
* Assert that at set of links is properly parented.
*
* @internal
*/
public function assertMenuLinkParents(array $links, array $expected_hierarchy): void {
foreach ($expected_hierarchy as $id => $parent) {
/** @var \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin */
$menu_link_plugin = $this->menuLinkManager->createInstance($links[$id]);
$expected_parent = $links[$parent] ?? '';
$link_parent = $menu_link_plugin->getParent();
$this->assertEquals($expected_parent, $link_parent, "Menu link $id has parent of $link_parent, expected $expected_parent.");
}
}
/**
* Assert that a link entity's created timestamp is set.
*/
public function testCreateLink(): void {
$options = [
'menu_name' => 'menu-test',
'bundle' => 'menu_link_content',
'link' => [['uri' => 'internal:/']],
'title' => 'Link test',
];
$link = MenuLinkContent::create($options);
$link->save();
// Make sure the changed timestamp is set.
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $link->getChangedTime(), 'Creating a menu link sets the "changed" timestamp.');
$options = [
'title' => 'Test Link',
];
$link->link->options = $options;
$link->changed->value = 0;
$link->save();
// Make sure the changed timestamp is updated.
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $link->getChangedTime(), 'Changing a menu link sets "changed" timestamp.');
}
/**
* Tests that menu link pointing to entities get removed on entity remove.
*/
public function testMenuLinkOnEntityDelete(): void {
// Create user.
$user = User::create(['name' => 'username']);
$user->save();
// Create External test entity.
$external_entity = EntityTestExternal::create();
$external_entity->save();
// Ensure an external entity can be deleted.
$external_entity->delete();
// Create "canonical" menu link pointing to the user.
$menu_link_content = MenuLinkContent::create([
'title' => 'username profile',
'menu_name' => 'menu-test',
'link' => [['uri' => 'entity:user/' . $user->id()]],
'bundle' => 'menu_test',
]);
$menu_link_content->save();
// Create "collection" menu link pointing to the user listing page.
$menu_link_content_collection = MenuLinkContent::create([
'title' => 'users listing',
'menu_name' => 'menu-test',
'link' => [['uri' => 'internal:/' . $user->toUrl('collection')->getInternalPath()]],
'bundle' => 'menu_test',
]);
$menu_link_content_collection->save();
// Check is menu links present in the menu.
$menu_tree_condition = (new MenuTreeParameters())->addCondition('route_name', 'entity.user.canonical');
$this->assertCount(1, \Drupal::menuTree()->load('menu-test', $menu_tree_condition));
$menu_tree_condition_collection = (new MenuTreeParameters())->addCondition('route_name', 'entity.user.collection');
$this->assertCount(1, \Drupal::menuTree()->load('menu-test', $menu_tree_condition_collection));
// Delete the user.
$user->delete();
// The "canonical" menu item has to be deleted.
$this->assertCount(0, \Drupal::menuTree()->load('menu-test', $menu_tree_condition));
// The "collection" menu item should still present in the menu.
$this->assertCount(1, \Drupal::menuTree()->load('menu-test', $menu_tree_condition_collection));
}
/**
* Tests automatic reparenting of menu links.
*/
public function testMenuLinkReparenting($module = 'menu_test'): void {
// Check the initial hierarchy.
$links = $this->createLinkHierarchy($module);
$expected_hierarchy = [
'parent' => '',
'child-1' => 'parent',
'child-1-1' => 'child-1',
'child-1-2' => 'child-1',
'child-2' => 'parent',
];
$this->assertMenuLinkParents($links, $expected_hierarchy);
// Start over, and move child-1 under child-2, and check that all the
// children of child-1 have been moved too.
$links = $this->createLinkHierarchy($module);
$this->menuLinkManager->updateDefinition($links['child-1'], ['parent' => $links['child-2']]);
// Verify that the entity was updated too.
/** @var \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin */
$menu_link_plugin = $this->menuLinkManager->createInstance($links['child-1']);
$entity = \Drupal::service('entity.repository')->loadEntityByUuid('menu_link_content', $menu_link_plugin->getDerivativeId());
$this->assertEquals($links['child-2'], $entity->getParentId());
$expected_hierarchy = [
'parent' => '',
'child-1' => 'child-2',
'child-1-1' => 'child-1',
'child-1-2' => 'child-1',
'child-2' => 'parent',
];
$this->assertMenuLinkParents($links, $expected_hierarchy);
// Start over, and delete child-1, and check that the children of child-1
// have been reassigned to the parent.
$links = $this->createLinkHierarchy($module);
$this->menuLinkManager->removeDefinition($links['child-1']);
$expected_hierarchy = [
'parent' => FALSE,
'child-1-1' => 'parent',
'child-1-2' => 'parent',
'child-2' => 'parent',
];
$this->assertMenuLinkParents($links, $expected_hierarchy);
// Try changing the parent at the entity level.
$definition = $this->menuLinkManager->getDefinition($links['child-1-2']);
$entity = MenuLinkContent::load($definition['metadata']['entity_id']);
$entity->parent->value = '';
$entity->save();
$expected_hierarchy = [
'parent' => '',
'child-1-1' => 'parent',
'child-1-2' => '',
'child-2' => 'parent',
];
$this->assertMenuLinkParents($links, $expected_hierarchy);
// @todo Figure out what makes sense to test in terms of automatic
// re-parenting. https://www.drupal.org/node/2309531
}
/**
* Tests the MenuLinkContent::preDelete function.
*/
public function testMenuLinkContentReparenting(): void {
// Add new menu items in a hierarchy.
$parent = MenuLinkContent::create([
'title' => $this->randomMachineName(8),
'link' => [['uri' => 'internal:/']],
'menu_name' => 'main',
]);
$parent->save();
$child1 = MenuLinkContent::create([
'title' => $this->randomMachineName(8),
'link' => [['uri' => 'internal:/']],
'menu_name' => 'main',
'parent' => 'menu_link_content:' . $parent->uuid(),
]);
$child1->save();
$child2 = MenuLinkContent::create([
'title' => $this->randomMachineName(8),
'link' => [['uri' => 'internal:/']],
'menu_name' => 'main',
'parent' => 'menu_link_content:' . $child1->uuid(),
]);
$child2->save();
// Delete the middle child.
$child1->delete();
// Refresh $child2.
$child2 = MenuLinkContent::load($child2->id());
// Test the reference in the child.
$this->assertSame('menu_link_content:' . $parent->uuid(), $child2->getParentId());
}
/**
* Tests uninstalling a module providing default links.
*/
public function testModuleUninstalledMenuLinks(): void {
\Drupal::service('module_installer')->install(['menu_test']);
\Drupal::service('plugin.manager.menu.link')->rebuild();
$menu_links = $this->menuLinkManager->loadLinksByRoute('menu_test.menu_test');
$this->assertCount(1, $menu_links);
$menu_link = reset($menu_links);
$this->assertEquals('menu_test', $menu_link->getPluginId());
// Uninstall the module and ensure the menu link got removed.
\Drupal::service('module_installer')->uninstall(['menu_test']);
\Drupal::service('plugin.manager.menu.link')->rebuild();
$menu_links = $this->menuLinkManager->loadLinksByRoute('menu_test.menu_test');
$this->assertCount(0, $menu_links);
}
/**
* Tests handling of pending revisions.
*
* @covers \Drupal\menu_link_content\Plugin\Validation\Constraint\MenuTreeHierarchyConstraintValidator::validate
*/
public function testPendingRevisions(): void {
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
// Add new menu items in a hierarchy.
$default_root_1_title = $this->randomMachineName(8);
$root_1 = $storage->create([
'title' => $default_root_1_title,
'link' => [['uri' => 'internal:/#root_1']],
'menu_name' => 'menu-test',
]);
$root_1->save();
$default_child1_title = $this->randomMachineName(8);
$child1 = $storage->create([
'title' => $default_child1_title,
'link' => [['uri' => 'internal:/#child1']],
'menu_name' => 'menu-test',
'parent' => 'menu_link_content:' . $root_1->uuid(),
]);
$child1->save();
$default_child2_title = $this->randomMachineName(8);
$child2 = $storage->create([
'title' => $default_child2_title,
'link' => [['uri' => 'internal:/#child2']],
'menu_name' => 'menu-test',
'parent' => 'menu_link_content:' . $child1->uuid(),
]);
$child2->save();
$default_root_2_title = $this->randomMachineName(8);
$root_2 = $storage->create([
'title' => $default_root_2_title,
'link' => [['uri' => 'internal:/#root_2']],
'menu_name' => 'menu-test',
]);
$root_2->save();
// Check that changing the title and the link in a pending revision is
// allowed.
$pending_child1_title = $this->randomMachineName(8);
$child1_pending_revision = $storage->createRevision($child1, FALSE);
$child1_pending_revision->set('title', $pending_child1_title);
$child1_pending_revision->set('link', [['uri' => 'internal:/#test']]);
$violations = $child1_pending_revision->validate();
$this->assertEmpty($violations);
$child1_pending_revision->save();
$storage->resetCache();
$child1_pending_revision = $storage->loadRevision($child1_pending_revision->getRevisionId());
$this->assertFalse($child1_pending_revision->isDefaultRevision());
$this->assertEquals($pending_child1_title, $child1_pending_revision->getTitle());
$this->assertEquals('/#test', $child1_pending_revision->getUrlObject()->toString());
// Check that saving a pending revision does not affect the menu tree.
$menu_tree = \Drupal::menuTree()->load('menu-test', new MenuTreeParameters());
$parent_link = reset($menu_tree);
$this->assertEquals($default_root_1_title, $parent_link->link->getTitle());
$this->assertEquals('/#root_1', $parent_link->link->getUrlObject()->toString());
$child1_link = reset($parent_link->subtree);
$this->assertEquals($default_child1_title, $child1_link->link->getTitle());
$this->assertEquals('/#child1', $child1_link->link->getUrlObject()->toString());
$child2_link = reset($child1_link->subtree);
$this->assertEquals($default_child2_title, $child2_link->link->getTitle());
$this->assertEquals('/#child2', $child2_link->link->getUrlObject()->toString());
// Check that changing the parent in a pending revision is not allowed.
$child2_pending_revision = $storage->createRevision($child2, FALSE);
$child2_pending_revision->set('parent', $child1->id());
$violations = $child2_pending_revision->validate();
$this->assertCount(1, $violations);
$this->assertEquals('You can only change the hierarchy for the published version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('menu_parent', $violations[0]->getPropertyPath());
// Check that changing the weight in a pending revision is not allowed.
$child2_pending_revision = $storage->createRevision($child2, FALSE);
$child2_pending_revision->set('weight', 500);
$violations = $child2_pending_revision->validate();
$this->assertCount(1, $violations);
$this->assertEquals('You can only change the hierarchy for the published version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('weight', $violations[0]->getPropertyPath());
// Check that changing both the parent and the weight in a pending revision
// is not allowed.
$child2_pending_revision = $storage->createRevision($child2, FALSE);
$child2_pending_revision->set('parent', $child1->id());
$child2_pending_revision->set('weight', 500);
$violations = $child2_pending_revision->validate();
$this->assertCount(2, $violations);
$this->assertEquals('You can only change the hierarchy for the published version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('You can only change the hierarchy for the published version of this menu link.', $violations[1]->getMessage());
$this->assertEquals('menu_parent', $violations[0]->getPropertyPath());
$this->assertEquals('weight', $violations[1]->getPropertyPath());
// Check that changing the parent of a term which didn't have a parent
// initially is not allowed in a pending revision.
$root_2_pending_revision = $storage->createRevision($root_2, FALSE);
$root_2_pending_revision->set('parent', $root_1->id());
$violations = $root_2_pending_revision->validate();
$this->assertCount(1, $violations);
$this->assertEquals('You can only change the hierarchy for the published version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('menu_parent', $violations[0]->getPropertyPath());
}
/**
* Tests that getEntity() method returns correct value.
*/
public function testMenuLinkContentGetEntity(): void {
// Set up a custom menu link pointing to a specific path.
$user = User::create(['name' => 'username']);
$user->save();
$title = $this->randomMachineName();
$menu_link = MenuLinkContent::create([
'title' => $title,
'link' => [['uri' => 'internal:/' . $user->toUrl('collection')->getInternalPath()]],
'menu_name' => 'menu-test',
]);
$menu_link->save();
$menu_tree = \Drupal::menuTree()->load('menu-test', new MenuTreeParameters());
$this->assertCount(1, $menu_tree);
/** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */
$tree_element = reset($menu_tree);
$this->assertInstanceOf(MenuLinkContentPlugin::class, $tree_element->link);
$this->assertInstanceOf(MenuLinkContent::class, $tree_element->link->getEntity());
$this->assertEquals($title, $tree_element->link->getEntity()->getTitle());
$this->assertEquals($menu_link->id(), $tree_element->link->getEntity()->id());
}
/**
* Tests that the form doesn't break for links with arbitrary menu names.
*/
public function testMenuLinkContentFormInvalidParentMenu(): void {
$menu_link = MenuLinkContent::create([
'title' => 'Menu link test',
'provider' => 'menu_link_content',
'menu_name' => 'non-existent',
'link' => ['uri' => 'internal:/user/login'],
]);
// Get the form for a new link, assert that building it doesn't break if
// the links menu name doesn't exist.
$build = \Drupal::service('entity.form_builder')->getForm($menu_link);
static::assertIsArray($build);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
/**
* Test stub creation for menu link content entities.
*
* @group menu_link_content
*/
class MigrateMenuLinkContentStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_link_content', 'link'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('menu_link_content');
}
/**
* Tests creation of menu link content stubs.
*/
public function testStub(): void {
$this->performStubTest('menu_link_content');
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\menu_link_content\MenuLinkContentInterface;
/**
* Provides assertions for testing MenuLinkContent.
*/
trait MigrateMenuLinkTestTrait {
/**
* Asserts various aspects of a menu link entity.
*
* @param string $id
* The link ID.
* @param string $langcode
* The language of the menu link.
* @param string $title
* The expected title of the link.
* @param string $menu
* The expected ID of the menu to which the link will belong.
* @param string $description
* The link's expected description.
* @param bool $enabled
* Whether the link is enabled.
* @param bool $expanded
* Whether the link is expanded.
* @param array $attributes
* Additional attributes the link is expected to have.
* @param string $uri
* The expected URI of the link.
* @param int $weight
* The expected weight of the link.
*
* @return \Drupal\menu_link_content\MenuLinkContentInterface
* The menu link content.
*/
protected function assertEntity($id, $langcode, $title, $menu, $description, $enabled, $expanded, array $attributes, $uri, $weight) {
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */
$menu_link = MenuLinkContent::load($id);
$menu_link = $menu_link->getTranslation($langcode);
$this->assertInstanceOf(MenuLinkContentInterface::class, $menu_link);
$this->assertSame($title, $menu_link->getTitle());
$this->assertSame($langcode, $menu_link->language()->getId());
$this->assertSame($menu, $menu_link->getMenuName());
$this->assertSame($description, $menu_link->getDescription());
$this->assertSame($enabled, $menu_link->isEnabled());
$this->assertSame($expanded, $menu_link->isExpanded());
$this->assertSame($attributes, $menu_link->link->options);
$this->assertSame($uri, $menu_link->link->uri);
$this->assertSame($weight, $menu_link->getWeight());
return $menu_link;
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate\d6;
use Drupal\Tests\menu_link_content\Kernel\Migrate\MigrateMenuLinkTestTrait;
use Drupal\Tests\node\Kernel\Migrate\d6\MigrateNodeTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests Menu link localized translation migration.
*
* @group migrate_drupal_6
*/
class MigrateMenuLinkLocalizedTest extends MigrateNodeTestBase {
use MigrateMenuLinkTestTrait;
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'language',
'link',
'menu_link_content',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser();
$this->installEntitySchema('menu_link_content');
$this->executeMigrations([
'language',
'd6_language_content_menu_settings',
'd6_menu',
'd6_menu_links',
'd6_menu_links_localized',
]);
}
/**
* Tests migration of menu link localized translations.
*/
public function testMenuLinkLocalized(): void {
// A localized menu link.
$this->assertEntity('463', 'fr', 'fr - Test 1', 'secondary-links', 'fr - Test menu link 1', TRUE, FALSE, [
'attributes' => ['title' => 'fr - Test menu link 1'],
'langcode' => 'fr',
'alter' => TRUE,
], 'internal:/user/login', -49);
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate\d6;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\Tests\node\Kernel\Migrate\d6\MigrateNodeTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Menu link migration.
*
* @group migrate_drupal_6
*/
class MigrateMenuLinkTest extends MigrateNodeTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'language',
'menu_link_content',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser();
$this->installEntitySchema('menu_link_content');
$this->executeMigrations([
'language',
'd6_language_content_settings',
'd6_node',
'd6_node_translation',
'd6_menu',
'd6_menu_links',
'node_translation_menu_links',
]);
}
/**
* Asserts various aspects of a menu link entity.
*
* @param string $id
* The link ID.
* @param string $title
* The expected title of the link.
* @param string $menu
* The expected ID of the menu to which the link will belong.
* @param string|null $description
* The link's expected description.
* @param bool $enabled
* Whether the link is enabled.
* @param bool $expanded
* Whether the link is expanded.
* @param array $attributes
* Additional attributes the link is expected to have.
* @param string $uri
* The expected URI of the link.
* @param int $weight
* The expected weight of the link.
*
* @internal
*/
protected function assertEntity(string $id, string $title, string $menu, ?string $description, bool $enabled, bool $expanded, array $attributes, string $uri, int $weight): void {
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */
$menu_link = MenuLinkContent::load($id);
$this->assertInstanceOf(MenuLinkContentInterface::class, $menu_link);
$this->assertSame($title, $menu_link->getTitle());
$this->assertSame($menu, $menu_link->getMenuName());
$this->assertSame($description, $menu_link->getDescription());
$this->assertSame($enabled, $menu_link->isEnabled());
$this->assertSame($expanded, $menu_link->isExpanded());
$this->assertSame($attributes, $menu_link->link->options);
$this->assertSame($uri, $menu_link->link->uri);
$this->assertSame($weight, $menu_link->getWeight());
}
/**
* Tests migration of menu links.
*/
public function testMenuLinks(): void {
$this->assertEntity('138', 'Test 1', 'secondary-links', 'Test menu link 1', TRUE, FALSE, ['attributes' => ['title' => 'Test menu link 1'], 'langcode' => 'en'], 'internal:/user/login', -50);
$this->assertEntity('139', 'Test 2', 'secondary-links', 'Test menu link 2', TRUE, TRUE, ['query' => ['foo' => 'bar'], 'attributes' => ['title' => 'Test menu link 2']], 'internal:/admin', -49);
$this->assertEntity('140', 'Drupal.org', 'secondary-links', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'https://www.drupal.org', -50);
// Assert that missing title attributes don't stop or break migration.
$this->assertEntity('393', 'Test 3', 'secondary-links', NULL, TRUE, FALSE, [], 'internal:/user/login', -47);
// Test the migration of menu links for translated nodes.
$this->assertEntity('459', 'The Real McCoy', 'primary-links', NULL, TRUE, FALSE, ['attributes' => ['title' => ''], 'alter' => TRUE], 'entity:node/10', 0);
$this->assertEntity('460', 'Le Vrai McCoy', 'primary-links', NULL, TRUE, FALSE, ['attributes' => ['title' => ''], 'alter' => TRUE], 'entity:node/10', 0);
$this->assertEntity('461', 'Abantu zulu', 'primary-links', NULL, TRUE, FALSE, ['attributes' => ['title' => ''], 'alter' => TRUE], 'entity:node/12', 0);
$this->assertEntity('462', 'The Zulu People', 'primary-links', NULL, TRUE, FALSE, ['attributes' => ['title' => ''], 'alter' => TRUE], 'entity:node/12', 0);
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate\d6;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Menu link migration.
*
* @group migrate_drupal_6
*/
class MigrateMenuLinkTranslationTest extends MigrateDrupal6TestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'menu_ui',
'menu_link_content',
'language',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrateContent();
$this->setUpCurrentUser();
$this->installEntitySchema('menu_link_content');
$this->executeMigrations([
'language',
'd6_menu',
'd6_menu_links',
'd6_menu_links_translation',
]);
}
/**
* Tests migration of menu links.
*/
public function testMenuLinks(): void {
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */
$menu_link = MenuLinkContent::load(139)->getTranslation('fr');
$this->assertInstanceOf(MenuLinkContent::class, $menu_link);
$this->assertSame('fr - Test 2', $menu_link->getTitle());
$this->assertSame('fr - Test menu link 2', $menu_link->getDescription());
$this->assertSame('secondary-links', $menu_link->getMenuName());
$this->assertTrue($menu_link->isEnabled());
$this->assertTrue($menu_link->isExpanded());
$this->assertSame(['query' => ['foo' => 'bar'], 'attributes' => ['title' => 'Test menu link 2']], $menu_link->link->options);
$this->assertSame('internal:/admin', $menu_link->link->uri);
$this->assertSame(-49, $menu_link->getWeight());
$menu_link = MenuLinkContent::load(139)->getTranslation('zu');
$this->assertInstanceOf(MenuLinkContent::class, $menu_link);
$this->assertSame('Test 2', $menu_link->getTitle());
$this->assertSame('zu - Test menu link 2', $menu_link->getDescription());
$this->assertSame('secondary-links', $menu_link->getMenuName());
$this->assertTrue($menu_link->isEnabled());
$this->assertTrue($menu_link->isExpanded());
$this->assertSame(['query' => ['foo' => 'bar'], 'attributes' => ['title' => 'Test menu link 2']], $menu_link->link->options);
$this->assertSame('internal:/admin', $menu_link->link->uri);
$this->assertSame(-49, $menu_link->getWeight());
$menu_link = MenuLinkContent::load(140)->getTranslation('fr');
$this->assertInstanceOf(MenuLinkContent::class, $menu_link);
$this->assertSame('fr - Drupal.org', $menu_link->getTitle());
$this->assertSame('', $menu_link->getDescription());
$this->assertSame('secondary-links', $menu_link->getMenuName());
$this->assertTrue($menu_link->isEnabled());
$this->assertFalse($menu_link->isExpanded());
$this->assertSame(['attributes' => ['title' => '']], $menu_link->link->options);
$this->assertSame('https://www.drupal.org', $menu_link->link->uri);
$this->assertSame(-50, $menu_link->getWeight());
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate\d7;
use Drupal\Tests\menu_link_content\Kernel\Migrate\MigrateMenuLinkTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests Menu link localized translation migration.
*
* @group migrate_drupal_7
*/
class MigrateMenuLinkLocalizedTest extends MigrateDrupal7TestBase {
use MigrateMenuLinkTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'language',
'link',
'menu_link_content',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations(['language']);
$this->installEntitySchema('menu_link_content');
$this->executeMigrations([
'd7_menu',
'd7_language_content_menu_settings',
'd7_menu_links',
'd7_menu_links_localized',
]);
}
/**
* Tests migration of menu link localized translations.
*/
public function testMenuLinkLocalized(): void {
// A translate and localize menu, menu-test-menu.
$this->assertEntity(468, 'en', 'Yahoo', 'menu-test-menu', 'english description', TRUE, FALSE, ['attributes' => ['title' => 'english description'], 'alter' => TRUE], 'http://yahoo.com', 0);
$this->assertEntity(468, 'fr', 'fr - Yahoo', 'menu-test-menu', 'fr - description', TRUE, FALSE, ['attributes' => ['title' => 'english description'], 'alter' => TRUE], 'http://yahoo.com', 0);
$this->assertEntity(468, 'is', 'is - Yahoo', 'menu-test-menu', 'is - description', TRUE, FALSE, ['attributes' => ['title' => 'english description'], 'alter' => TRUE], 'http://yahoo.com', 0);
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate\d7;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Tests\menu_link_content\Kernel\Migrate\MigrateMenuLinkTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Menu link migration.
*
* @group menu_link_content
*/
class MigrateMenuLinkTest extends MigrateDrupal7TestBase {
const MENU_NAME = 'menu-test-menu';
use UserCreationTrait;
use MigrateMenuLinkTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'language',
'link',
'menu_ui',
'menu_link_content',
'node',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser();
$this->installEntitySchema('menu_link_content');
$this->installSchema('node', ['node_access']);
$this->installConfig(static::$modules);
$this->migrateUsers(FALSE);
$this->migrateContentTypes();
$this->executeMigrations([
'language',
'd7_language_content_settings',
'd7_node',
'd7_node_translation',
'd7_menu',
'd7_menu_links',
'node_translation_menu_links',
]);
}
/**
* Tests migration of menu links.
*/
public function testMenuLinks(): void {
$this->assertEntity(469, 'und', 'Bing', static::MENU_NAME, 'Bing', TRUE, FALSE, ['attributes' => ['title' => 'Bing']], 'http://bing.com', 0);
// This link has an i18n translation so the language is changed to the
// default language of the source site.
$this->assertEntity(467, 'en', 'Google', static::MENU_NAME, 'Google', TRUE, FALSE, ['attributes' => ['title' => 'Google']], 'http://google.com', 0);
$this->assertEntity(468, 'en', 'Yahoo', static::MENU_NAME, 'english description', TRUE, FALSE, ['attributes' => ['title' => 'english description'], 'alter' => TRUE], 'http://yahoo.com', 0);
// Tests migrating an external link with an undefined title attribute.
$this->assertEntity(470, 'und', 'Ask', static::MENU_NAME, NULL, TRUE, FALSE, [], 'http://ask.com', 0);
$this->assertEntity(245, 'und', 'Home', 'main', NULL, TRUE, FALSE, [], 'internal:/', 0);
$this->assertEntity(478, 'und', 'custom link test', 'admin', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'internal:/admin/content', 0);
$this->assertEntity(479, 'und', 'node link test', 'tools', 'node 2', TRUE, FALSE, [
'attributes' => ['title' => 'node 2'],
'query' => [
'name' => 'ferret',
'color' => 'purple',
],
],
'entity:node/2', 3);
$menu_link_tree_service = \Drupal::service('menu.link_tree');
$parameters = new MenuTreeParameters();
$tree = $menu_link_tree_service->load(static::MENU_NAME, $parameters);
$this->assertCount(2, $tree);
$children = 0;
$google_found = FALSE;
foreach ($tree as $menu_link_tree_element) {
$children += $menu_link_tree_element->hasChildren;
if ($menu_link_tree_element->link->getUrlObject()->toString() == 'http://bing.com') {
$this->assertEquals('http://google.com', reset($menu_link_tree_element->subtree)->link->getUrlObject()->toString());
$google_found = TRUE;
}
}
$this->assertEquals(1, $children);
$this->assertTrue($google_found);
// Now find the custom link under a system link.
$parameters->root = 'system.admin_structure';
$tree = $menu_link_tree_service->load(static::MENU_NAME, $parameters);
$found = FALSE;
foreach ($tree as $menu_link_tree_element) {
$this->assertNotEmpty($menu_link_tree_element->link->getUrlObject()->toString());
if ($menu_link_tree_element->link->getTitle() == 'custom link test') {
$found = TRUE;
break;
}
}
$this->assertTrue($found);
// Test the migration of menu links for translated nodes.
$this->assertEntity(484, 'und', 'The thing about Deep Space 9', 'tools', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'entity:node/2', 9);
$this->assertEntity(485, 'en', 'is - The thing about Deep Space 9', 'tools', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'entity:node/2', 10);
$this->assertEntity(486, 'und', 'is - The thing about Firefly', 'tools', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'entity:node/4', 11);
$this->assertEntity(487, 'en', 'en - The thing about Firefly', 'tools', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'entity:node/4', 12);
// Test there have been no attempts to stub a shortcut in a MigrationLookup
// process.
$messages = $this->getMigration('d7_menu')->getIdMap()->getMessages()->fetchAll();
$this->assertCount(0, $messages);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Migrate\d7;
use Drupal\Tests\menu_link_content\Kernel\Migrate\MigrateMenuLinkTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests Menu link translation migration.
*
* @group migrate_drupal_7
*/
class MigrateMenuLinkTranslationTest extends MigrateDrupal7TestBase {
use MigrateMenuLinkTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'language',
'link',
'menu_link_content',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations(['language']);
$this->installEntitySchema('menu_link_content');
$this->executeMigrations([
'd7_menu',
'd7_language_content_menu_settings',
'd7_menu_links',
'd7_menu_links_translation',
]);
}
/**
* Tests migration of menu link translations.
*/
public function testMenuLinkTranslation(): void {
$this->assertEntity(467, 'fr', 'fr - Google', 'menu-test-menu', 'fr - Google description', TRUE, FALSE, ['attributes' => ['title' => 'Google']], 'http://google.com', 0);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Ensures that the menu tree adapts to path alias changes.
*
* @group menu_link_content
* @group path
*/
class PathAliasMenuLinkContentTest extends KernelTestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'menu_link_content',
'system',
'link',
'path_alias',
'test_page_test',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('path_alias');
// Ensure that the weight of module_link_content is higher than system.
// @see menu_link_content_install()
module_set_weight('menu_link_content', 1);
}
/**
* Tests the path aliasing changing.
*/
public function testPathAliasChange(): void {
$path_alias = $this->createPathAlias('/test-page', '/my-blog');
$menu_link_content = MenuLinkContent::create([
'title' => 'Menu title',
'link' => ['uri' => 'internal:/my-blog'],
'menu_name' => 'tools',
]);
$menu_link_content->save();
$tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertEquals('test_page_test.test_page', $tree[$menu_link_content->getPluginId()]->link->getPluginDefinition()['route_name']);
// Saving an alias should clear the alias manager cache.
$path_alias->setPath('/test-render-title');
$path_alias->setAlias('/my-blog');
$path_alias->save();
$tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertEquals('test_page_test.render_title', $tree[$menu_link_content->getPluginId()]->link->getPluginDefinition()['route_name']);
// Delete the alias.
$path_alias->delete();
$tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertTrue(isset($tree[$menu_link_content->getPluginId()]));
$this->assertEquals('', $tree[$menu_link_content->getPluginId()]->link->getRouteName());
// Verify the plugin now references a path that does not match any route.
$this->assertEquals('base:my-blog', $tree[$menu_link_content->getPluginId()]->link->getUrlObject()->getUri());
}
}

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Plugin\migrate\process;
use Drupal\KernelTests\KernelTestBase;
use Drupal\menu_link_content\Plugin\migrate\process\LinkUri;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\node\Entity\Node;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests \Drupal\menu_link_content\Plugin\migrate\process\LinkUri.
*
* @group menu_link_content
*
* @coversDefaultClass \Drupal\menu_link_content\Plugin\migrate\process\LinkUri
*/
class LinkUriTest extends KernelTestBase {
use UserCreationTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser();
$this->installEntitySchema('node');
}
/**
* Tests LinkUri::transform().
*
* @param string $value
* The value to pass to LinkUri::transform().
* @param string $expected
* The expected return value of LinkUri::transform().
*
* @dataProvider providerTestRouted
*
* @covers ::transform
*/
public function testRouted($value, $expected): void {
$actual = $this->doTransform($value);
$this->assertSame($expected, $actual);
}
/**
* Provides test cases for LinkUriTest::testTransform().
*
* @return array
* An array of test cases, each which the following values:
* - The value array to pass to LinkUri::transform().
* - The expected path returned by LinkUri::transform().
*/
public static function providerTestRouted() {
$tests = [];
$value = 'http://example.com';
$expected = 'http://example.com';
$tests['with_scheme'] = [$value, $expected];
$value = '<front>';
$expected = 'internal:/';
$tests['front'] = [$value, $expected];
$value = '<nolink>';
$expected = 'route:<nolink>';
$tests['nolink'] = [$value, $expected];
return $tests;
}
/**
* Tests that Non routed URLs throws an exception.
*
* @param string $value
* The value to pass to LinkUri::transform().
* @param string $exception_message
* The expected exception message.
*
* @dataProvider providerTestNotRouted
*/
public function testNotRouted($value, $exception_message): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage($exception_message);
$this->doTransform($value);
}
/**
* Provides test cases for LinkUriTest::testNotRouted().
*
* @return array
* An array of test cases, each which the following values:
* - The value array to pass to LinkUri::transform().
* - The expected path returned by LinkUri::transform().
* - (optional) A URL object that the path validator prophecy will return.
*/
public static function providerTestNotRouted() {
$tests = [];
$message = 'The path "%s" failed validation.';
$value = '/test';
$expected = 'internal:/test';
$exception_message = sprintf($message, $expected);
$tests['leading_slash'] = [$value, $exception_message];
$value = 'test';
$expected = 'internal:/test';
$exception_message = sprintf($message, $expected);
$tests['without_scheme'] = [$value, $exception_message];
return $tests;
}
/**
* Tests disabling route validation in LinkUri::transform().
*
* @param string $value
* The value to pass to LinkUri::transform().
* @param string $expected
* The expected return value of LinkUri::transform().
*
* @dataProvider providerTestDisablingRouteValidation
*
* @covers ::transform
*/
public function testDisablingRouteValidation($value, $expected): void {
// Create a node so we have a valid route.
Node::create([
'nid' => 1,
'title' => 'test',
'type' => 'page',
])->save();
$actual = $this->doTransform($value, ['validate_route' => FALSE]);
$this->assertSame($expected, $actual);
}
/**
* Provides test cases for LinkUriTest::testDisablingRouteValidation().
*
* @return array
* An array of test cases, each which the following values:
* - The value array to pass to LinkUri::transform().
* - The expected path returned by LinkUri::transform().
*/
public static function providerTestDisablingRouteValidation() {
$tests = [];
$value = 'node/1';
$expected = 'entity:node/1';
$tests['routed'] = [$value, $expected];
$value = 'node/2';
$expected = 'base:node/2';
$tests['unrouted'] = [$value, $expected];
return $tests;
}
/**
* Transforms a link path into an 'internal:' or 'entity:' URI.
*
* @param string $value
* The value to pass to LinkUri::transform().
* @param array $configuration
* The plugin configuration.
*
* @return string
* The transformed link.
*/
public function doTransform($value, $configuration = []) {
$entityTypeManager = $this->container->get('entity_type.manager');
$row = new Row();
$executable = $this->prophesize(MigrateExecutableInterface::class)->reveal();
$plugin = new LinkUri($configuration, 'link_uri', [], $entityTypeManager);
$actual = $plugin->transform($value, $executable, $row, 'destination_property');
return $actual;
}
}

View File

@@ -0,0 +1,332 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Plugin\migrate\source;
use Drupal\Component\Utility\Unicode;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
use Drupal\TestTools\Random;
// cspell:ignore mlid plid tsid
/**
* Tests the menu link source plugin.
*
* @covers \Drupal\menu_link_content\Plugin\migrate\source\MenuLink
*
* @group menu_link_content
*/
class MenuLinkTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_link_content', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['menu_links'] = [
[
// Customized menu link, provided by system module.
'menu_name' => 'menu-test-menu',
'mlid' => 140,
'plid' => 0,
'link_path' => 'admin/config/system/cron',
'router_path' => 'admin/config/system/cron',
'link_title' => 'Cron',
'options' => [],
'module' => 'system',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 0,
'customized' => 1,
'p1' => '0',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_translation' => TRUE,
],
[
// D6 customized menu link, provided by menu module.
'menu_name' => 'menu-test-menu',
'mlid' => 141,
'plid' => 0,
'link_path' => 'node/141',
'router_path' => 'node/%',
'link_title' => 'Node 141',
'options' => [],
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 0,
'customized' => 1,
'p1' => '0',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_translation' => TRUE,
],
[
// D6 non-customized menu link, provided by menu module.
'menu_name' => 'menu-test-menu',
'mlid' => 142,
'plid' => 0,
'link_path' => 'node/142',
'router_path' => 'node/%',
'link_title' => 'Node 142',
'options' => [],
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 0,
'customized' => 0,
'p1' => '0',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'en',
'i18n_tsid' => '1',
'skip_translation' => TRUE,
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 138,
'plid' => 0,
'link_path' => 'admin',
'router_path' => 'admin',
'link_title' => 'Test 1',
'options' => ['attributes' => ['title' => 'Test menu link 1']],
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 1,
'expanded' => 0,
'weight' => 15,
'depth' => 1,
'customized' => 1,
'p1' => '138',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_translation' => TRUE,
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 139,
'plid' => 138,
'link_path' => 'admin/modules',
'router_path' => 'admin/modules',
'link_title' => 'Test 2',
'options' => ['attributes' => ['title' => 'Test menu link 2']],
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 12,
'depth' => 2,
'customized' => 1,
'p1' => '138',
'p2' => '139',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_translation' => TRUE,
],
[
'menu_name' => 'menu-user',
'mlid' => 143,
'plid' => 0,
'link_path' => 'admin/build/menu-customize/navigation',
'router_path' => 'admin/build/menu-customize/%',
'link_title' => 'Navigation',
'options' => [],
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 0,
'customized' => 0,
'p1' => '0',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_translation' => TRUE,
],
[
// D7 non-customized menu link, provided by menu module.
'menu_name' => 'menu-test2-menu',
'mlid' => 300,
'plid' => 0,
'link_path' => 'node/142',
'router_path' => 'node/%',
'link_title' => 'Node 142',
'options' => [],
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 0,
'customized' => 0,
'p1' => '0',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'fr',
'i18n_tsid' => '1',
'skip_translation' => FALSE,
],
[
// D7 shortcut set link.
'menu_name' => 'shortcut-set-1',
'mlid' => 301,
'plid' => 0,
'link_path' => 'node/add',
'router_path' => 'node/add',
'link_title' => 'Add Content',
'options' => [],
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 1,
'customized' => 0,
'p1' => '301',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_translation' => TRUE,
],
];
// Add long link title attributes to source data.
$title = Random::getGenerator()->string('500');
$tests[0]['source_data']['menu_links'][0]['options']['attributes']['title'] = $title;
// Build the expected results.
$expected = $tests[0]['source_data']['menu_links'];
// Add long link title attributes to expected results.
$expected[0]['description'] = Unicode::truncate($title, 255);
// Don't expect D6 menu link to a custom menu, provided by menu module.
unset($expected[5]);
array_walk($tests[0]['source_data']['menu_links'], function (&$row) {
$row['options'] = serialize($row['options']);
});
// Adjust the order to match the order used in the query. The expected[5] is
// not returned by the source query because it is an admin menu link.
$tests[0]['expected_data'] = [];
$tests[0]['expected_data'][] = $expected[0];
$tests[0]['expected_data'][] = $expected[1];
$tests[0]['expected_data'][] = $expected[2];
$tests[0]['expected_data'][] = $expected[6];
$tests[0]['expected_data'][] = $expected[3];
$tests[0]['expected_data'][] = $expected[4];
// Tests retrieval of links from multiple menus.
$tests[1] = $tests[0];
$tests[1]['expected_count'] = NULL;
$tests[1]['configuration'] = [
'menu_name' => ['menu-test-menu', 'menu-test2-menu'],
];
// Tests retrieval of links from a single menu.
$tests[2] = $tests[1];
$tests[2]['configuration'] = [
'menu_name' => 'menu-test2-menu',
];
$tests[2]['expected_data'] = [$expected[6]];
// Tests retrieval of links from a not existing menu.
$tests[3] = $tests[1];
$tests[3]['configuration'] = [
'menu_name' => 'menu-not-exists',
];
$tests[3]['expected_data'] = [];
return $tests;
}
}

View File

@@ -0,0 +1,254 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
// cspell:ignore mlid objectid objectindex plid
/**
* Tests menu link translation source plugin.
*
* @covers \Drupal\menu_link_content\Plugin\migrate\source\d6\MenuLinkTranslation
* @group menu_link_content
*/
class MenuLinkTranslationTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_link_content', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$test = [];
$test[0]['source_data']['menu_links'] = [
[
'menu_name' => 'menu-test-menu',
'mlid' => 138,
'plid' => 0,
'link_path' => 'admin',
'router_path' => 'admin',
'link_title' => 'Test 1',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:16:"Test menu link 1";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 1,
'expanded' => 0,
'weight' => 15,
'depth' => 1,
'customized' => 1,
'p1' => '138',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 139,
'plid' => 138,
'link_path' => 'admin/modules',
'router_path' => 'admin/modules',
'link_title' => 'Test 2',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:16:"Test menu link 2";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 12,
'depth' => 2,
'customized' => 1,
'p1' => '138',
'p2' => '139',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 140,
'plid' => 0,
'link_path' => 'https://www.drupal.org',
'router_path' => 'admin/modules',
'link_title' => 'Test 2',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:16:"Test menu link 2";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 12,
'depth' => 2,
'customized' => 1,
'p1' => '0',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 141,
'plid' => 0,
'link_path' => 'https://api.drupal.org/api/drupal/8.3.x',
'router_path' => 'admin/modules',
'link_title' => 'Test 3',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:16:"Test menu link 3";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 12,
'depth' => 2,
'customized' => 1,
'p1' => '0',
'p2' => '0',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
],
];
$test[0]['source_data']['i18n_strings'] = [
[
'lid' => 1,
'objectid' => 139,
'type' => 'item',
'property' => 'title',
'objectindex' => 0,
'format' => 0,
],
[
'lid' => 2,
'objectid' => 139,
'type' => 'item',
'property' => 'description',
'objectindex' => 0,
'format' => 0,
],
[
'lid' => 3,
'objectid' => 140,
'type' => 'item',
'property' => 'description',
'objectindex' => 0,
'format' => 0,
],
[
'lid' => 4,
'objectid' => 141,
'type' => 'item',
'property' => 'title',
'objectindex' => 0,
'format' => 0,
],
];
$test[0]['source_data']['locales_target'] = [
[
'lid' => 1,
'language' => 'fr',
'translation' => 'fr - title translation',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
[
'lid' => 2,
'language' => 'fr',
'translation' => 'fr - description translation',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
[
'lid' => 3,
'language' => 'zu',
'translation' => 'zu - description translation',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
[
'lid' => 4,
'language' => 'zu',
'translation' => 'zu - title translation',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
];
$test[0]['expected_data'] = [
[
'menu_name' => 'menu-test-menu',
'mlid' => 139,
'property' => 'title',
'language' => 'fr',
'link_title' => 'Test 2',
'description' => 'Test menu link 2',
'title_translated' => 'fr - title translation',
'description_translated' => 'fr - description translation',
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 139,
'property' => 'description',
'language' => 'fr',
'link_title' => 'Test 2',
'description' => 'Test menu link 2',
'title_translated' => 'fr - title translation',
'description_translated' => 'fr - description translation',
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 140,
'property' => 'description',
'language' => 'zu',
'link_title' => 'Test 2',
'description' => 'Test menu link 2',
'title_translated' => NULL,
'description_translated' => 'zu - description translation',
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 141,
'property' => 'title',
'language' => 'zu',
'link_title' => 'Test 3',
'description' => 'Test menu link 3',
'title_translated' => 'zu - title translation',
'description_translated' => NULL,
],
];
return $test;
}
}

View File

@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
// cspell:ignore mlid plid tsid
/**
* Tests menu link localized translation source plugin.
*
* @covers \Drupal\menu_link_content\Plugin\migrate\source\d7\MenuLinkLocalized
* @group menu_link_content
*/
class MenuLinkLocalizedTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_link_content', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
$tests[0]['source_data']['menu_links'] = [
[
'menu_name' => 'menu-test-menu',
'mlid' => 130,
'plid' => 469,
'link_path' => 'http://google.com',
'router_path' => '',
'link_title' => 'Google',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:11:"en - Google";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'en',
'i18n_tsid' => '2',
'skip_source_translation' => TRUE,
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 131,
'plid' => 469,
'link_path' => 'http://google.com',
'router_path' => '',
'link_title' => 'fr - Google',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:23:"fr - Google description";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'fr',
'i18n_tsid' => '2',
'skip_source_translation' => TRUE,
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 132,
'plid' => 469,
'link_path' => 'https://duckduckgo.com/',
'router_path' => '',
'link_title' => 'DuckDuckGo',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:21:"DuckDuckGo";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_source_translation' => TRUE,
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 139,
'plid' => 138,
'link_path' => 'admin/modules',
'router_path' => 'admin/modules',
'link_title' => 'Test 2',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:9:"Test link";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 0,
'has_children' => 0,
'expanded' => 0,
'weight' => 12,
'depth' => 2,
'customized' => 1,
'p1' => '138',
'p2' => '139',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'skip_source_translation' => TRUE,
],
];
$tests[0]['expected_data'] = [
[
'menu_name' => 'menu-test-menu',
'mlid' => 130,
'plid' => 469,
'link_path' => 'http://google.com',
'router_path' => '',
'link_title' => 'Google',
'description' => 'en - Google',
'module' => 'menu',
'hidden' => 0,
'enabled' => TRUE,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'en',
'i18n_tsid' => '2',
'skip_source_translation' => FALSE,
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 130,
'plid' => 469,
'link_path' => 'http://google.com',
'router_path' => '',
'link_title' => 'fr - Google',
'description' => 'fr - Google description',
'module' => 'menu',
'hidden' => 0,
'enabled' => TRUE,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'fr',
'i18n_tsid' => '2',
'skip_source_translation' => TRUE,
],
];
return $tests;
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
// cspell:ignore mlid objectid objectindex plid tsid textgroup
/**
* Tests menu link localized translation source plugin.
*
* @covers \Drupal\menu_link_content\Plugin\migrate\source\d7\MenuLinkTranslation
* @group menu_link_content
*/
class MenuLinkTranslationTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_link_content', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$test = [];
$test[0]['source_data']['menu_links'] = [
[
'menu_name' => 'menu-test-menu',
'mlid' => 130,
'plid' => 469,
'link_path' => 'http://google.com',
'router_path' => '',
'link_title' => 'Google',
'options' => 'a:1:{s:10:"attributes";a:1:{s:5:"title";s:16:"Test menu link 1";}}',
'module' => 'menu',
'hidden' => 0,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'und',
'i18n_tsid' => '0',
'ml_language' => 'und',
'lid' => '1',
'property' => 'title',
'lt_language' => 'fr',
'translation' => 'fr - title translation',
'title_translated' => 'fr - title translation',
'description_translated' => 'fr - description translation',
],
];
$test[0]['source_data']['i18n_string'] = [
[
'lid' => 1,
'textgroup' => 'menu',
'context' => 'item:130:title',
'objectid' => 130,
'type' => 'item',
'property' => 'title',
'objectindex' => 130,
'format' => 0,
],
[
'lid' => 2,
'textgroup' => 'menu',
'context' => 'item:130:description',
'objectid' => 130,
'type' => 'item',
'property' => 'description',
'objectindex' => 130,
'format' => 0,
],
];
$test[0]['source_data']['locales_target'] = [
[
'lid' => 1,
'language' => 'fr',
'translation' => 'fr - title translation',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
[
'lid' => 2,
'language' => 'fr',
'translation' => 'fr - description translation',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
];
$test[0]['expected_data'] = [
[
'menu_name' => 'menu-test-menu',
'mlid' => 130,
'plid' => 469,
'link_path' => 'http://google.com',
'router_path' => '',
'link_title' => 'Google',
'module' => 'menu',
'hidden' => 0,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'fr',
'i18n_tsid' => '0',
'parent_link_path' => NULL,
'property' => 'title',
'lid' => '1',
'lt_language' => 'fr',
'translation' => 'fr - title translation',
'description_translated' => 'fr - description translation',
'title_translated' => 'fr - title translation',
],
[
'menu_name' => 'menu-test-menu',
'mlid' => 130,
'plid' => 469,
'link_path' => 'http://google.com',
'router_path' => '',
'link_title' => 'Google',
'module' => 'menu',
'hidden' => 0,
'external' => 1,
'has_children' => 0,
'expanded' => 0,
'weight' => 0,
'depth' => 2,
'customized' => 1,
'p1' => '469',
'p2' => '467',
'p3' => '0',
'p4' => '0',
'p5' => '0',
'p6' => '0',
'p7' => '0',
'p8' => '0',
'p9' => '0',
'updated' => '0',
'language' => 'fr',
'i18n_tsid' => '0',
'parent_link_path' => NULL,
'property' => 'description',
'lid' => '2',
'lt_language' => 'fr',
'translation' => 'fr - description translation',
'description_translated' => 'fr - description translation',
'title_translated' => 'fr - title translation',
],
];
return $test;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Unit;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\menu_link_content\MenuLinkContentAccessControlHandler;
use Drupal\Tests\UnitTestCase;
/**
* Tests menu link content entity access.
*
* @coversDefaultClass \Drupal\menu_link_content\MenuLinkContentAccessControlHandler
* @group menu_link_content
*/
class MenuLinkContentEntityAccessTest extends UnitTestCase {
/**
* Tests an operation not implemented by the access control handler.
*
* @covers ::checkAccess
*/
public function testUnrecognizedOperation(): void {
$entityType = $this->createMock(EntityTypeInterface::class);
$accessManager = $this->createMock(AccessManagerInterface::class);
$moduleHandler = $this->createMock(ModuleHandlerInterface::class);
$moduleHandler->expects($this->any())
->method('invokeAll')
->willReturn([]);
$language = $this->createMock(LanguageInterface::class);
$language->expects($this->any())
->method('getId')
->willReturn('de');
$entity = $this->createMock(ContentEntityInterface::class);
$entity->expects($this->any())
->method('language')
->willReturn($language);
$account = $this->createMock(AccountInterface::class);
$accessControl = new MenuLinkContentAccessControlHandler($entityType, $accessManager);
$accessControl->setModuleHandler($moduleHandler);
$access = $accessControl->access($entity, 'not-an-op', $account, TRUE);
$this->assertInstanceOf(AccessResultInterface::class, $access);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_link_content\Unit;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent
*
* @group Menu
*/
class MenuLinkPluginTest extends UnitTestCase {
/**
* @covers ::getUuid
*/
public function testGetInstanceReflection(): void {
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $menu_link_content_plugin */
$menu_link_content_plugin = $this->prophesize(MenuLinkContent::class);
$menu_link_content_plugin->getDerivativeId()->willReturn('test_id');
$menu_link_content_plugin = $menu_link_content_plugin->reveal();
$class = new \ReflectionClass(MenuLinkContent::class);
$instance_method = $class->getMethod('getUuid');
$this->assertEquals('test_id', $instance_method->invoke($menu_link_content_plugin));
}
}