Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,8 @@
name: 'Custom Menu Links'
type: module
description: 'Allows administrators to create custom menu links.'
package: Core
version: VERSION
core: 8.x
dependencies:
- link

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Install, update and uninstall functions for the menu_link_content module.
*/
/**
* Implements hook_install().
*/
function menu_link_content_install() {
// Add a higher weight so that menu_link_content_path_update() is called after
// system_path_update() clears the path alias cache.
// @todo remove this when the cache clearing is moved to path module or if
// caching is removed for path aliases due to
// https://www.drupal.org/node/1965074
module_set_weight('menu_link_content', 1);
}

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,84 @@
<?php
/**
* @file
* Allows administrators to create custom menu links.
*/
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 .= '<h3>' . t('About') . '</h3>';
$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>.', array('!menu-help' => \Drupal::url('help.page', array('name' => 'menu_ui')), '!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 enable the Menu UI module, it provides an interface for managing menus and menu links.', array('!drupal-org-help' => 'https://www.drupal.org/documentation/modules/menu_link'));
}
$output .= '</p>';
return $output;
}
}
/**
* Implements hook_menu_delete().
*/
function menu_link_content_menu_delete(MenuInterface $menu) {
$storage = \Drupal::entityManager()->getStorage('menu_link_content');
$menu_links = $storage->loadByProperties(array('menu_name' => $menu->id()));
$storage->delete($menu_links);
}
/**
* Implements hook_path_insert().
*/
function menu_link_content_path_insert($path) {
_menu_link_content_update_path_alias($path['alias']);
}
/**
* 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::entityManager()
->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_path_update().
*/
function menu_link_content_path_update($path) {
if ($path['alias'] != $path['original']['alias']) {
_menu_link_content_update_path_alias($path['alias']);
_menu_link_content_update_path_alias($path['original']['alias']);
}
elseif ($path['source'] != $path['original']['source']) {
_menu_link_content_update_path_alias($path['alias']);
}
}
/**
* Implements hook_path_delete().
*/
function menu_link_content_path_delete($path) {
_menu_link_content_update_path_alias($path['alias']);
}

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,37 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Controller\MenuController.
*/
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->entityManager()->getStorage('menu_link_content')->create(array(
'id' => '',
'parent' => '',
'menu_name' => $menu->id(),
'bundle' => 'menu_link_content',
));
return $this->entityFormBuilder()->getForm($menu_link);
}
}

View file

@ -0,0 +1,404 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Entity\MenuLinkContent.
*/
namespace Drupal\menu_link_content\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
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\link\LinkItemInterface link
* @property \Drupal\Core\Field\FieldItemList rediscover
*
* @ContentEntityType(
* id = "menu_link_content",
* label = @Translation("Custom menu link"),
* handlers = {
* "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
* "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"
* }
* },
* admin_permission = "administer menu",
* base_table = "menu_link_content",
* data_table = "menu_link_content_data",
* translatable = TRUE,
* entity_keys = {
* "id" = "id",
* "label" = "title",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "bundle" = "bundle"
* },
* 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",
* }
* )
*/
class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterface {
use EntityChangedTrait;
/**
* 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() {
return $this->get('parent')->value;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return (int) $this->get('weight')->value;
}
/**
* {@inheritdoc}
*/
public function getChangedTime() {
return $this->get('changed')->value;
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
$definition = array();
$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'] = array('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);
/** @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 an menu link entry
// on both entity and menu link plugin level.
if ($update && $menu_link_manager->getDefinition($this->getPluginId())) {
// 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(), $this->getPluginDefinition(), FALSE);
}
}
else {
$menu_link_manager->addDefinition($this->getPluginId(), $this->getPluginDefinition());
}
}
/**
* {@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);
}
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['id'] = BaseFieldDefinition::create('integer')
->setLabel(t('Entity ID'))
->setDescription(t('The entity ID for this menu link content entity.'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$fields['uuid'] = BaseFieldDefinition::create('uuid')
->setLabel(t('UUID'))
->setDescription(t('The content menu link UUID.'))
->setReadOnly(TRUE);
$fields['bundle'] = BaseFieldDefinition::create('string')
->setLabel(t('Bundle'))
->setDescription(t('The content menu link bundle.'))
->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH)
->setSetting('is_ascii', TRUE)
->setReadOnly(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)
->setSettings(array(
'max_length' => 255,
))
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
))
->setDisplayOptions('form', array(
'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)
->setSettings(array(
'max_length' => 255,
))
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'string',
'weight' => 0,
))
->setDisplayOptions('form', array(
'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.'))
->setRequired(TRUE)
->setSettings(array(
'link_type' => LinkItemInterface::LINK_GENERIC,
'title' => DRUPAL_DISABLED,
))
->setDisplayOptions('form', array(
'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);
$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', array(
'label' => 'hidden',
'type' => 'integer',
'weight' => 0,
))
->setDisplayOptions('form', array(
'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.'))
->setDefaultValue(FALSE)
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'boolean',
'weight' => 0,
))
->setDisplayOptions('form', array(
'settings' => array('display_label' => TRUE),
'weight' => 0,
));
$fields['enabled'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Enabled'))
->setDescription(t('A flag for whether the link should be enabled in menus or hidden.'))
->setDefaultValue(TRUE)
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'boolean',
'weight' => 0,
))
->setDisplayOptions('form', array(
'settings' => array('display_label' => TRUE),
'weight' => -1,
));
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The menu link language code.'))
->setTranslatable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',
))
->setDisplayOptions('form', array(
'type' => 'language_select',
'weight' => 2,
));
$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);
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
/**
* @file
* Contains \Drupal\menu_link_content\Form\MenuLinkContentDeleteForm.
*/
namespace Drupal\menu_link_content\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Url;
/**
* Provides a delete form for content menu links.
*/
class MenuLinkContentDeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.menu.edit_form', array('menu' => $this->entity->getMenuName()));
}
/**
* {@inheritdoc}
*/
protected function getRedirectUrl() {
return $this->getCancelUrl();
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
return $this->t('The menu link %title has been deleted.', array('%title' => $this->entity->label()));
}
}

View file

@ -0,0 +1,140 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Form\MenuLinkContentForm.
*/
namespace Drupal\menu_link_content\Form;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\MenuParentFormSelectorInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form to add/update content menu links.
*/
class MenuLinkContentForm extends ContentEntityForm {
/**
* The content menu link.
*
* @var \Drupal\menu_link_content\MenuLinkContentInterface
*/
protected $entity;
/**
* The parent form selector service.
*
* @var \Drupal\Core\Menu\MenuParentFormSelectorInterface
*/
protected $menuParentSelector;
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* Constructs a MenuLinkContentForm object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector
* The menu parent form selector service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The path validator.
*/
public function __construct(EntityManagerInterface $entity_manager, MenuParentFormSelectorInterface $menu_parent_selector, LanguageManagerInterface $language_manager, PathValidatorInterface $path_validator) {
parent::__construct($entity_manager, $language_manager);
$this->menuParentSelector = $menu_parent_selector;
$this->pathValidator = $path_validator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('menu.parent_form_selector'),
$container->get('language_manager'),
$container->get('path.validator')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$default = $this->entity->getMenuName() . ':' . $this->entity->getParentId();
$id = $this->entity->isNew() ? '' : $this->entity->getPluginId();
$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);
list($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(array('enabled', 'value')));
$entity->expanded->value = (!$form_state->isValueEmpty(array('expanded', 'value')));
return $entity;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
// The entity is rebuilt in parent::submit().
$menu_link = $this->entity;
$saved = $menu_link->save();
if ($saved) {
drupal_set_message($this->t('The menu link has been saved.'));
$form_state->setRedirect(
'entity.menu_link_content.canonical',
array('menu_link_content' => $menu_link->id())
);
}
else {
drupal_set_message($this->t('There was an error saving the menu link.'), 'error');
$form_state->setRebuild();
}
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\MenuLinkContentAccessControlHandler.
*/
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 user 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, $langcode, AccountInterface $account) {
switch ($operation) {
case 'view':
// There is no direct view.
return AccessResult::neutral();
case 'update':
if (!$account->hasPermission('administer menu')) {
return AccessResult::neutral()->cachePerPermissions();
}
else {
// If there is a URL, this is an external link so always accessible.
$access = AccessResult::allowed()->cachePerPermissions()->cacheUntilEntityChanges($entity);
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
// We allow access, but only if the link is accessible as well.
if (($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::allowedIf(!$entity->isNew() && $account->hasPermission('administer menu'))->cachePerPermissions()->cacheUntilEntityChanges($entity);
}
}
}

View file

@ -0,0 +1,136 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\MenuLinkContentInterface.
*/
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Defines an interface for custom menu links.
*/
interface MenuLinkContentInterface extends ContentEntityInterface, EntityChangedInterface {
/**
* 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,36 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\MenuLinkContentStorageSchema.
*/
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 == 'menu_link_content') {
switch ($field_name) {
case 'rediscover':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
}
}
return $schema;
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Plugin\Deriver\MenuLinkContentDeriver.
*/
namespace Drupal\menu_link_content\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\Query\QueryFactory;
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 query factory.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $queryFactory;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* Constructs a MenuLinkContentDeriver instance.
*
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
* The query factory.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
*/
public function __construct(QueryFactory $query_factory, EntityManagerInterface $entity_manager, MenuLinkManagerInterface $menu_link_manager) {
$this->queryFactory = $query_factory;
$this->entityManager = $entity_manager;
$this->menuLinkManager = $menu_link_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity.query'),
$container->get('entity.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->queryFactory->get('menu_link_content')
->condition('rediscover', TRUE)
->execute();
$plugin_definitions = [];
$menu_link_content_entities = $this->entityManager->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,250 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent.
*/
namespace Drupal\menu_link_content\Plugin\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\MenuLinkBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
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 = array();
/**
* {@inheritdoc}
*/
protected $overrideAllowed = array(
'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;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* 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\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
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->entityManager = $entity_manager;
$this->languageManager = $language_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.manager'),
$container->get('language_manager')
);
}
/**
* 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.
*/
protected function getEntity() {
if (empty($this->entity)) {
$entity = NULL;
$storage = $this->entityManager->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 = isset($entities[$entity_id]) ? $entities[$entity_id] : NULL;
static::$entityIdsToLoad = array();
}
if (!$entity) {
// Fallback to the loading by the UUID.
$uuid = $this->getUuid();
$loaded_entities = $storage->loadByProperties(array('uuid' => $uuid));
$entity = reset($loaded_entities);
}
if (!$entity) {
throw new PluginException(SafeMarkup::format('Entity not found through the menu link plugin definition and could not fallback on UUID @uuid', array('@uuid' => $uuid)));
}
// Clone the entity object to avoid tampering with the static cache.
$this->entity = clone $entity;
$the_entity = $this->entityManager->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() {
return $this->getEntity()->urlInfo('delete-form');
}
/**
* {@inheritdoc}
*/
public function getEditRoute() {
return $this->getEntity()->urlInfo();
}
/**
* {@inheritdoc}
*/
public function getTranslateRoute() {
return $this->getEntity()->urlInfo('drupal:content-translation-overview');
}
/**
* Returns the unique ID representing the menu link.
*
* @return string
* The menu link ID.
*/
protected function getUuid() {
$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;
}
$this->entityManager->getStorage('menu_link_content')->save($entity);
}
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,218 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Tests\LinksTest.
*/
namespace Drupal\menu_link_content\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\locale\TranslationString;
use Drupal\simpletest\WebTestBase;
/**
* Tests handling of menu links hierarchies.
*
* @group Menu
*/
class LinksTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('router_test', 'menu_link_content');
/**
* The menu link plugin manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
*/
protected $menuLinkManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->menuLinkManager = \Drupal::service('plugin.manager.menu.link');
entity_create('menu', array(
'id' => 'menu_test',
'label' => 'Test menu',
'description' => 'Description text',
))->save();
}
/**
* Create a simple hierarchy of links.
*/
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 = array(
'title' => 'Menu link test',
'provider' => $module,
'menu_name' => 'menu_test',
);
$parent = $base_options + array(
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent'],
);
$link = entity_create('menu_link_content', $parent);
$link->save();
$links['parent'] = $link->getPluginId();
$child_1 = $base_options + array(
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child'],
'parent' => $links['parent'],
);
$link = entity_create('menu_link_content', $child_1);
$link->save();
$links['child-1'] = $link->getPluginId();
$child_1_1 = $base_options + array(
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child2/child'],
'parent' => $links['child-1'],
);
$link = entity_create('menu_link_content', $child_1_1);
$link->save();
$links['child-1-1'] = $link->getPluginId();
$child_1_2 = $base_options + array(
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child2/child'],
'parent' => $links['child-1'],
);
$link = entity_create('menu_link_content', $child_1_2);
$link->save();
$links['child-1-2'] = $link->getPluginId();
$child_2 = $base_options + array(
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child'],
'parent' => $links['parent'],
);
$link = entity_create('menu_link_content', $child_2);
$link->save();
$links['child-2'] = $link->getPluginId();
return $links;
}
/**
* Assert that at set of links is properly parented.
*/
function assertMenuLinkParents($links, $expected_hierarchy) {
foreach ($expected_hierarchy as $id => $parent) {
/* @var \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin */
$menu_link_plugin = $this->menuLinkManager->createInstance($links[$id]);
$expected_parent = isset($links[$parent]) ? $links[$parent] : '';
$this->assertEqual($menu_link_plugin->getParent(), $expected_parent, SafeMarkup::format('Menu link %id has parent of %parent, expected %expected_parent.', array('%id' => $id, '%parent' => $menu_link_plugin->getParent(), '%expected_parent' => $expected_parent)));
}
}
/**
* Assert that a link entity's created timestamp is set.
*/
public function testCreateLink() {
$options = array(
'menu_name' => 'menu_test',
'bundle' => 'menu_link_content',
'link' => [['uri' => 'internal:/']],
);
$link = entity_create('menu_link_content', $options);
$link->save();
// Make sure the changed timestamp is set.
$this->assertEqual($link->getChangedTime(), REQUEST_TIME, 'Creating a menu link sets the "changed" timestamp.');
$options = array(
'title' => 'Test Link',
);
$link->link->options = $options;
$link->changed->value = 0;
$link->save();
// Make sure the changed timestamp is updated.
$this->assertEqual($link->getChangedTime(), REQUEST_TIME, 'Changing a menu link sets "changed" timestamp.');
}
/**
* Test automatic reparenting of menu links.
*/
function testMenuLinkReparenting($module = 'menu_test') {
// Check the initial hierarchy.
$links = $this->createLinkHierarchy($module);
$expected_hierarchy = array(
'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);
/* @var \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin */
$this->menuLinkManager->updateDefinition($links['child-1'], array('parent' => $links['child-2']));
// Verify that the entity was updated too.
$menu_link_plugin = $this->menuLinkManager->createInstance($links['child-1']);
$entity = \Drupal::entityManager()->loadEntityByUuid('menu_link_content', $menu_link_plugin->getDerivativeId());
$this->assertEqual($entity->getParentId(), $links['child-2']);
$expected_hierarchy = array(
'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 = array(
'parent' => FALSE,
'child-1-1' => 'parent',
'child-1-2' => 'parent',
'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 uninstalling a module providing default links.
*/
public function testModuleUninstalledMenuLinks() {
\Drupal::service('module_installer')->install(array('menu_test'));
\Drupal::service('router.builder')->rebuild();
\Drupal::service('plugin.manager.menu.link')->rebuild();
$menu_links = $this->menuLinkManager->loadLinksByRoute('menu_test.menu_test');
$this->assertEqual(count($menu_links), 1);
$menu_link = reset($menu_links);
$this->assertEqual($menu_link->getPluginId(), 'menu_test');
// Uninstall the module and ensure the menu link got removed.
\Drupal::service('module_installer')->uninstall(array('menu_test'));
\Drupal::service('plugin.manager.menu.link')->rebuild();
$menu_links = $this->menuLinkManager->loadLinksByRoute('menu_test.menu_test');
$this->assertEqual(count($menu_links), 0);
}
}

View file

@ -0,0 +1,149 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Tests\MenuLinkContentCacheabilityBubblingTest.
*/
namespace Drupal\menu_link_content\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\simpletest\KernelTestBase;
use Drupal\user\Entity\User;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Ensures that rendered menu links bubble the necessary cacheability metadata
* for outbound path/route processing.
*
* @group menu_link_content
*/
class MenuLinkContentCacheabilityBubblingTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['menu_link_content', 'system', 'link', 'outbound_processing_test', 'url_alter_test', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('user');
$this->installSchema('system', ['url_alias', 'router']);
// 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 bubbling of menu links' outbound route/path processing cacheability.
*/
public function testOutboundPathAndRouteProcessing() {
\Drupal::service('router.builder')->rebuild();
$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_stack->push($request);
$request_context->fromRequest($request);
$menu_tree = \Drupal::menuTree();
$renderer = \Drupal::service('renderer');
$default_menu_cacheability = (new CacheableMetadata())
->setCacheMaxAge(Cache::PERMANENT)
->setCacheTags(['config:system.menu.tools'])
->setCacheContexts(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme']);
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 CacheableMetadata())->setCacheContexts(['route']),
],
// \Drupal\Core\Access\RouteProcessorCsrf: max-age = 0.
[
'uri' => 'route:outbound_processing_test.route.csrf',
'cacheability' => (new CacheableMetadata())->setCacheMaxAge(0),
],
// \Drupal\Core\PathProcessor\PathProcessorFront: permanently cacheable.
[
'uri' => 'internal:/',
'cacheability' => (new CacheableMetadata()),
],
// \Drupal\url_alter_test\PathProcessorTest: user entity's cache tags.
[
'uri' => 'internal:/user/1',
'cacheability' => (new CacheableMetadata())->setCacheTags(User::load(1)->getCacheTags()),
],
[
'uri' => 'internal:/user/2',
'cacheability' => (new CacheableMetadata())->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',
]);
$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->assertEqual($expected_cacheability, CacheableMetadata::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 CacheableMetadata();
foreach ($test_cases as $expectation) {
$menu_link_content = MenuLinkContent::create([
'link' => ['uri' => $expectation['uri']],
'menu_name' => 'tools',
]);
$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->assertEqual($expected_cacheability, CacheableMetadata::createFromRenderArray($build));
}
}

View file

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Tests\MenuLinkContentDeriverTest.
*/
namespace Drupal\menu_link_content\Tests;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\simpletest\KernelTestBase;
use Symfony\Component\Routing\Route;
/**
* Tests the menu link content deriver.
*
* @group menu_link_content
*/
class MenuLinkContentDeriverTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['menu_link_content', 'link', 'system', 'menu_link_content_dynamic_route'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('menu_link_content');
$this->installSchema('system', 'router');
}
/**
* Tests the rediscovering.
*/
public function testRediscover() {
\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.
MenuLinkContent::create([
'title' => 'Example',
'link' => [['uri' => 'internal:/example-path']],
'menu_name' => 'tools',
])->save();
$menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertEqual(1, count($menu_tree));
/** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */
$tree_element = reset($menu_tree);
$this->assertEqual('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->assertEqual(1, count($menu_tree));
/** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */
$tree_element = reset($menu_tree);
$this->assertEqual('route_name_2', $tree_element->link->getRouteName());
}
}

View file

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Tests\MenuLinkContentFormTest.
*/
namespace Drupal\menu_link_content\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the menu link content UI.
*
* @group Menu
*/
class MenuLinkContentFormTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array(
'menu_link_content',
'menu_ui',
);
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(array('administer menu'));
$this->drupalLogin($web_user);
}
/**
* Tests the MenuLinkContentForm class.
*/
public function testMenuLinkContentForm() {
$this->drupalGet('admin/structure/menu/manage/admin/add');
$element = $this->xpath('//select[@id = :id]/option[@selected]', array(':id' => 'edit-menu-parent'));
$this->assertTrue($element, 'A default menu parent was found.');
$this->assertEqual('admin:', $element[0]['value'], '<Administration> menu is the parent.');
$this->drupalPostForm(
NULL,
array(
'title[0][value]' => t('Front page'),
'link[0][uri]' => '<front>',
),
t('Save')
);
$this->assertText(t('The menu link has been saved.'));
}
/**
* Tests validation for the MenuLinkContentForm class.
*/
public function testMenuLinkContentFormValidation() {
$this->drupalGet('admin/structure/menu/manage/admin/add');
$this->drupalPostForm(
NULL,
array(
'title[0][value]' => t('Test page'),
'link[0][uri]' => '<test>',
),
t('Save')
);
$this->assertText(t('Manually entered paths should start with /, ? or #.'));
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Tests\MenuLinkContentTranslationUITest.
*/
namespace Drupal\menu_link_content\Tests;
use Drupal\content_translation\Tests\ContentTranslationUITestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
* Tests the menu link content translation UI.
*
* @group Menu
*/
class MenuLinkContentTranslationUITest extends ContentTranslationUITestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array(
'language',
'content_translation',
'menu_link_content',
'menu_ui',
);
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->entityTypeId = 'menu_link_content';
$this->bundle = 'menu_link_content';
parent::setUp();
}
/**
* {@inheritdoc}
*/
protected function getTranslatorPermissions() {
return array_merge(parent::getTranslatorPermissions(), array('administer menu'));
}
/**
* {@inheritdoc}
*/
protected function getAdministratorPermissions() {
return array_merge(parent::getAdministratorPermissions(), array('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() {
$this->drupalGet('admin/structure/menu/manage/tools');
$this->assertNoLink(t('Translate'));
$menu_link_content = MenuLinkContent::create(['menu_name' => 'tools', 'link' => ['uri' => 'internal:/admin/structure/menu']]);
$menu_link_content->save();
$this->drupalGet('admin/structure/menu/manage/tools');
$this->assertLink(t('Translate'));
}
/**
* Tests that translation page inherits admin status of edit page.
*/
function testTranslationLinkTheme() {
$this->drupalLogin($this->administrator);
$entityId = $this->createEntity(array(), 'en');
// Set up Seven as the admin theme to test.
$this->container->get('theme_handler')->install(array('seven'));
$edit = array();
$edit['admin_theme'] = 'seven';
$this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
$this->drupalGet('admin/structure/menu/item/' . $entityId . '/edit');
$this->assertRaw('core/themes/seven/css/base/elements.css', 'Edit uses admin theme.');
$this->drupalGet('admin/structure/menu/item/' . $entityId . '/edit/translations');
$this->assertRaw('core/themes/seven/css/base/elements.css', 'Translation uses admin theme as well.');
}
/**
* {@inheritdoc}
*/
protected function doTestTranslationEdit() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$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 = array('language' => $languages[$langcode]);
$url = $entity->urlInfo('edit-form', $options);
$this->drupalGet($url);
$title = t('@title [%language translation]', array(
'@title' => $entity->getTranslation($langcode)->label(),
'%language' => $languages[$langcode]->getName(),
));
$this->assertRaw($title);
}
}
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Tests\PathAliasMenuLinkContentTest.
*/
namespace Drupal\menu_link_content\Tests;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\simpletest\KernelTestBase;
/**
* Ensures that the menu tree adapts to path alias changes.
*
* @group menu_link_content
*/
class PathAliasMenuLinkContentTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['menu_link_content', 'system', 'link', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('menu_link_content');
$this->installSchema('system', ['url_alias', 'router']);
// Ensure that the weight of module_link_content is higher than system.
// @see menu_link_content_install()
module_set_weight('menu_link_content', 1);
}
/**
* {@inheritdoc}
*/
public function containerBuild(ContainerBuilder $container) {
parent::containerBuild($container);
$definition = $container->getDefinition('path_processor_alias');
$definition
->addTag('path_processor_inbound', ['priority' => 100]);
}
/**
* Tests the path aliasing changing.
*/
public function testPathAliasChange() {
\Drupal::service('router.builder')->rebuild();
/** @var \Drupal\Core\Path\AliasStorageInterface $path_alias_storage */
$path_alias_storage = \Drupal::service('path.alias_storage');
$alias = $path_alias_storage->save('/test-page', '/my-blog');
$pid = $alias['pid'];
$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->assertEqual('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_storage->save('/test-render-title', '/my-blog', LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid);
$tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertEqual('test_page_test.render_title', $tree[$menu_link_content->getPluginId()]->link->getPluginDefinition()['route_name']);
// Delete the alias.
$path_alias_storage->delete(['pid' => $pid]);
$tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
$this->assertTrue(isset($tree[$menu_link_content->getPluginId()]));
$this->assertEqual('', $tree[$menu_link_content->getPluginId()]->link->getRouteName());
// Verify the plugin now references a path that does not match any route.
$this->assertEqual('base:my-blog', $tree[$menu_link_content->getPluginId()]->link->getUrlObject()->getUri());
}
}

View file

@ -0,0 +1,6 @@
name: 'Menu link content dynamic route'
type: module
core: 8.x
hidden: true
dependencies:
- menu_link_content

View file

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

View file

@ -0,0 +1,19 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content_dynamic_route\Routes.
*/
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,4 @@
name: 'Outbound route/path processing'
type: module
core: 8.x
hidden: true

View file

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