diff --git a/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php b/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php
new file mode 100644
index 0000000..16f0b43
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php
@@ -0,0 +1,162 @@
+menuTree = $menu_tree;
+ $this->stringTranslation = $string_translation;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('menu.link_tree'),
+ $container->get('string_translation')
+ );
+ }
+
+ /**
+ * Injects the menu link plugin.
+ *
+ * @param MenuLinkInterface $menu_link
+ * A menu link plugin instance.
+ */
+ public function setMenuLinkInstance(MenuLinkInterface $menu_link) {
+ $this->menuLink = $menu_link;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildEditForm(array &$form, array &$form_state) {
+ $form['#title'] = $this->t('Edit menu link %title', array('%title' => $this->menuLink->getTitle()));
+
+ $form['info'] = array(
+ '#type' => 'item',
+ '#title' => $this->t('This is a module-provided link. The label and path cannot be changed.'),
+ );
+ $form['path'] = array(
+ 'link' => $this->menuLink->build(),
+ '#type' => 'item',
+ '#title' => $this->t('Link'),
+ );
+
+ $form['enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enable'),
+ '#description' => $this->t('Menu links that are not enabled will not be listed in any menu.'),
+ '#default_value' => !$this->menuLink->isHidden(),
+ );
+ $form['expanded'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show as expanded'),
+ '#description' => $this->t('If selected and this menu link has children, the menu will always appear expanded.'),
+ '#default_value' => $this->menuLink->isExpanded(),
+ );
+ $delta = max(abs($this->menuLink->getWeight()), 50);
+ $form['weight'] = array(
+ '#type' => 'weight',
+ '#delta' => $delta,
+ '#default_value' => $this->menuLink->getWeight(),
+ '#title' => $this->t('Weight'),
+ '#description' => $this->t('Link weight among links in the same menu at the same depth.'),
+ );
+
+ $options = $this->menuTree->getParentSelectOptions($this->menuLink->getPluginId());
+ $menu_parent = $this->menuLink->getMenuName() . ':' . $this->menuLink->getParent();
+
+ if (!isset($options[$menu_parent])) {
+ // Put it at the top level in the current menu.
+ $menu_parent = $this->menuLink->getMenuName() . ':';
+ }
+ $form['menu_parent'] = array(
+ '#type' => 'select',
+ '#title' => $this->t('Parent link'),
+ '#options' => $options,
+ '#default_value' => $menu_parent,
+ '#description' => $this->t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => $this->menuTree->maxDepth())),
+ '#attributes' => array('class' => array('menu-title-select')),
+ );
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractFormValues(array &$form, array &$form_state) {
+ $new_definition = array();
+ $new_definition['hidden'] = $form_state['values']['enabled'] ? 0 : 1;
+ $new_definition['weight'] = (int) $form_state['values']['weight'];
+ $new_definition['expanded'] = $form_state['values']['expanded'] ? 1 : 0;
+ list($menu_name, $parent) = explode(':', $form_state['values']['menu_parent'], 2);
+ if (!empty($menu_name)) {
+ $new_definition['menu_name'] = $menu_name;
+ }
+ if (isset($parent)) {
+ $new_definition['parent'] = $parent;
+ }
+ return $new_definition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateEditForm(array &$form, array &$form_state) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitEditForm(array &$form, array &$form_state) {
+ $new_definition = $this->extractFormValues($form, $form_state);
+
+ return $this->menuTree->updateLink($this->menuLink->getPluginId(), $new_definition);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/Form/MenuLinkFormInterface.php b/core/lib/Drupal/Core/Menu/Form/MenuLinkFormInterface.php
new file mode 100644
index 0000000..27d3cb0
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/Form/MenuLinkFormInterface.php
@@ -0,0 +1,77 @@
+getOptions();
+ $description = $this->getDescription();
+ if ($title_attribute && $description) {
+ $options['attributes']['title'] = $description;
+ }
+ $build = array(
+ '#type' => 'link',
+ '#route_name' => $this->pluginDefinition['route_name'],
+ '#route_parameters' => $this->pluginDefinition['route_parameters'],
+ '#title' => $this->getTitle(),
+ '#options' => $options,
+ );
+ return $build;
+ }
+
+ /**
+ * Returns the weight of the menu link.
+ *
+ * @return int
+ * The weight of the menu link, 0 by default.
+ */
+ public function getWeight() {
+ // By default the weight is 0.
+ if (!isset($this->pluginDefinition['weight'])) {
+ $this->pluginDefinition['weight'] = 0;
+ }
+ return $this->pluginDefinition['weight'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTitle() {
+ // Subclasses may pull in the request or specific attributes as parameters.
+ $options = array();
+ if (!empty($this->pluginDefinition['title_context'])) {
+ $options['context'] = $this->pluginDefinition['title_context'];
+ }
+ $args = array();
+ if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) {
+ $args = (array) $title_arguments;
+ }
+ return $this->t($this->pluginDefinition['title'], $args, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMenuName() {
+ return $this->pluginDefinition['menu_name'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProvider() {
+ return $this->pluginDefinition['provider'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent() {
+ return $this->pluginDefinition['parent'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return (bool) $this->pluginDefinition['hidden'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isExpanded() {
+ return (bool) $this->pluginDefinition['expanded'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDiscovered() {
+ return (bool) $this->pluginDefinition['discovered'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isResetable() {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isTranslatable() {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDeletable() {
+ return (bool) $this->getDeleteRoute();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ if ($this->pluginDefinition['description']) {
+ return $this->t($this->pluginDefinition['description']);
+ }
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOptions() {
+ return $this->pluginDefinition['options'] ?: array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMetaData() {
+ return $this->pluginDefinition['metadata'] ?: array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUrlObject($title_attribute = TRUE) {
+ $options = $this->getOptions();
+ $description = $this->getDescription();
+ if ($title_attribute && $description) {
+ $options['attributes']['title'] = $description;
+ }
+ if (empty($this->pluginDefinition['url'])) {
+ return new Url($this->pluginDefinition['route_name'], $this->pluginDefinition['route_parameters'], $options);
+ }
+ else {
+ $url = Url::createFromPath($this->pluginDefinition['url']);
+ $url->setOptions($options);
+ return $url;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormClass() {
+ return $this->pluginDefinition['form_class'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeleteRoute() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEditRoute() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTranslateRoute() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteLink() {
+ throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $this->getPluginId()));
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkDefault.php b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php
new file mode 100644
index 0000000..170ea19
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php
@@ -0,0 +1,96 @@
+ 1,
+ 'parent' => 1,
+ 'weight' => 1,
+ 'expanded' => 1,
+ 'hidden' => 1,
+ );
+
+ /**
+ * The static menu link service used to store updates to weight/parent etc.
+ *
+ * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
+ */
+ protected $staticOverride;
+
+ /**
+ * Constructs a new MenuLinkDefault.
+ *
+ * @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\Menu\StaticMenuLinkOverridesInterface $static_override
+ * The static override storage.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+ $this->staticOverride = $static_override;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('menu_link.static.overrides')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isResetable() {
+ // The link can be reset if it was discovered and has an override.
+ return $this->pluginDefinition['discovered'] && $this->staticOverride->loadOverride($this->getPluginId());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function updateLink(array $new_definition_values, $persist) {
+ $overrides = array_intersect_key($new_definition_values, $this->overrideAllowed);
+ if ($persist) {
+ $this->staticOverride->saveOverride($this->getPluginId(), $overrides);
+ }
+ // Update the definition.
+ $this->pluginDefinition = $overrides + $this->getPluginDefinition();
+ return $this->pluginDefinition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function persistLinkDeletion() {
+ // @todo - what should this do by default?
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
new file mode 100644
index 0000000..2e42d52
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
@@ -0,0 +1,205 @@
+ 'tools',
+ // (required) The name of the route this links to, unless it's external.
+ 'route_name' => '',
+ // Parameters for route variables when generating a link.
+ 'route_parameters' => array(),
+ // The external URL if this link has one (required if route_name is empty).
+ 'url' => '',
+ // The static title for the menu link.
+ 'title' => '',
+ 'title_arguments' => array(),
+ 'title_context' => '',
+ // The description.
+ 'description' => '',
+ // The plugin ID of the parent link (or NULL for a top-level link).
+ 'parent' => '',
+ // The weight of the link.
+ 'weight' => 0,
+ // The default link options.
+ 'options' => array(),
+ 'expanded' => 0,
+ 'hidden' => 0,
+ // Flag for whether this plugin was discovered. Should be set to 0 or NULL
+ // for definitions that are added via a direct save.
+ 'discovered' => 0,
+ 'provider' => '',
+ 'metadata' => array(),
+ // Default class for local task implementations.
+ 'class' => 'Drupal\Core\Menu\MenuLinkDefault',
+ 'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm',
+ // The plugin id. Set by the plugin system based on the top-level YAML key.
+ 'id' => '',
+ );
+
+ /**
+ * The object that discovers plugins managed by this manager.
+ *
+ * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
+ */
+ protected $discovery;
+
+ /**
+ * The object that instantiates plugins managed by this manager.
+ *
+ * @var \Drupal\Component\Plugin\Factory\FactoryInterface
+ */
+ protected $factory;
+
+ /**
+ * The language manager.
+ *
+ * @var \Drupal\Core\Language\LanguageManagerInterface
+ */
+ protected $languageManager;
+
+ /**
+ * Cache backend instance for the extracted tree data.
+ *
+ * @var \Drupal\Core\Cache\CacheBackendInterface
+ */
+ protected $treeCacheBackend;
+
+ /**
+ * The menu link tree storage.
+ *
+ * @var \Drupal\Core\Menu\MenuTreeStorageInterface
+ */
+ protected $treeStorage;
+
+ /**
+ * Service providing overrides for static links
+ *
+ * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
+ */
+ protected $overrides;
+
+ /**
+ * The request stack.
+ *
+ * @var \Symfony\Component\HttpFoundation\RequestStack
+ */
+ protected $requestStack;
+
+ /**
+ * The plugin instances.
+ *
+ * @var array
+ */
+ protected $instances = array();
+
+ /**
+ * The statically cached definitions.
+ *
+ * @var array
+ */
+ protected $definitions = array();
+
+ /**
+ * The route provider to load routes by name.
+ *
+ * @var \Drupal\Core\Routing\RouteProviderInterface
+ */
+ protected $routeProvider;
+
+ /**
+ * The access manager.
+ *
+ * @var \Drupal\Core\Access\AccessManager
+ */
+ protected $accessManager;
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $account;
+
+ /**
+ * The entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManagerInterface
+ */
+ protected $entityManager;
+
+ /**
+ * The module handler.
+ *
+ * @var \Drupal\Core\Extension\ModuleHandlerInterface
+ */
+ protected $moduleHandler;
+
+ /**
+ * The configuration factory.
+ *
+ * @var \Drupal\Core\Config\ConfigFactoryInterface
+ */
+ protected $configFactory;
+
+ /**
+ * Stores the menu tree used by the doBuildTree method, keyed by a cache ID.
+ *
+ * This cache ID is built using the $menu_name, the current language and
+ * some parameters passed into an entity query.
+ */
+ protected $menuTree;
+
+ /**
+ * Stores the menu tree data on the current page keyed by a cache ID.
+ *
+ * This contains less information than a tree built with buildAllData.
+ *
+ * @var array
+ */
+ protected $menuPageTrees;
+
+ /**
+ * Stores the preferred menu link keyed by route_name + parameters.
+ *
+ * @var array
+ */
+ protected $preferredLinks = array();
+
+ /**
+ * Stores the active menu names.
+ *
+ * @var array
+ */
+ protected $activeMenus = array();
+
+ /**
+ * Stores the parameters for buildAllData keyed by cached ID.
+ *
+ * @var array
+ */
+ protected $buildAllDataParameters = array();
+
+ /**
+ * Constructs a \Drupal\Core\Menu\MenuLinkTree object.
+ *
+ * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
+ * The menu link tree storage.
+ * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides
+ * Service providing overrides for static links
+ * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+ * A request object for the controller resolver and finding the preferred
+ * menu and link for the current page.
+ * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+ * The route provider to load routes by name.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The module handler.
+ * @param \Drupal\Core\Cache\CacheBackendInterface $tree_cache_backend
+ * Cache backend instance for the extracted tree data.
+ * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * The language manager.
+ * @param \Drupal\Core\Access\AccessManager $access_manager
+ * The access manager.
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * The current user.
+ * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+ * The entity manager.
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * Configuration factory.
+ */
+ public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, RequestStack $request_stack, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $tree_cache_backend, LanguageManagerInterface $language_manager, AccessManager $access_manager, AccountInterface $account, EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory) {
+ $this->treeStorage = $tree_storage;
+ $this->overrides = $overrides;
+ $this->factory = new ContainerFactory($this);
+ $this->requestStack = $request_stack;
+ $this->routeProvider = $route_provider;
+ $this->accessManager = $access_manager;
+ $this->account = $account;
+ $this->moduleHandler = $module_handler;
+ $this->treeCacheBackend = $tree_cache_backend;
+ $this->languageManager = $language_manager;
+ $this->entityManager = $entity_manager;
+ $this->configFactory = $config_factory;
+ }
+
+ /**
+ * Performs extra processing on plugin definitions.
+ *
+ * By default we add defaults for the type to the definition. If a type has
+ * additional processing logic they can do that by replacing or extending the
+ * method.
+ */
+ protected function processDefinition(&$definition, $plugin_id) {
+ $definition = NestedArray::mergeDeep($this->defaults, $definition);
+ $definition['parent'] = (string) $definition['parent'];
+ $definition['id'] = $plugin_id;
+ }
+
+ /**
+ * Instanciates the discovery.
+ */
+ protected function getDiscovery() {
+ if (empty($this->discovery)) {
+ $yaml = new YamlDiscovery('menu_links', $this->moduleHandler->getModuleDirectories());
+ $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml);
+ }
+ return $this->discovery;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefinitions() {
+ // Since this function is called rarely, instantiate the discovery here.
+ $definitions = $this->getDiscovery()->getDefinitions();
+
+ $this->moduleHandler->alter('menu_links', $definitions);
+
+ foreach ($definitions as $plugin_id => &$definition) {
+ $definition['id'] = $plugin_id;
+ $this->processDefinition($definition, $plugin_id);
+ }
+
+ // If this plugin was provided by a module that does not exist, remove the
+ // plugin definition.
+ foreach ($definitions as $plugin_id => $plugin_definition) {
+ if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) {
+ unset($definitions[$plugin_id]);
+ }
+ else {
+ // Any link found here is flagged as discovered, so it can be purged
+ // if it does exit in the future.
+ $definitions[$plugin_id]['discovered'] = 1;
+ }
+ }
+ return $definitions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rebuild() {
+ // Fetch the list of existing menus, in case some are not longer populated
+ // after the rebuild.
+ $before_menus = $this->treeStorage->getMenuNames();
+ $definitions = $this->getDefinitions();
+ // Apply overrides from config.
+ $overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions));
+ foreach ($overrides as $id => $changes) {
+ if (!empty($definitions[$id])) {
+ $definitions[$id] = $changes + $definitions[$id];
+ }
+ }
+ $this->treeStorage->rebuild($definitions);
+ $this->treeCacheBackend->deleteAll();
+ $affected_menus = $this->treeStorage->getMenuNames() + $before_menus;
+ Cache::invalidateTags(array('menu' => $affected_menus));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
+ // When building tress, we will usually have the definitions already loaded.
+ // This makes the call to $this->factory->createInstance() faster.
+ // @todo Normal plugin managers throw an exception in case it doesn't exist.
+ if (!isset($this->definitions[$plugin_id])) {
+ $this->definitions[$plugin_id] = $this->treeStorage->load($plugin_id);
+ }
+ if (empty($this->definitions[$plugin_id]) && $exception_on_invalid) {
+ throw new PluginNotFoundException("$plugin_id could not be found.");
+ }
+ return $this->definitions[$plugin_id];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasDefinition($plugin_id) {
+ return (bool) $this->getDefinition($plugin_id, FALSE);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\Core\Menu\MenuLinkInterface
+ * A menu link instance.
+ */
+ public function createInstance($plugin_id, array $configuration = array()) {
+ return $this->factory->createInstance($plugin_id, $configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInstance(array $options) {
+ if (isset($options['id'])) {
+ return $this->createInstance($options['id']);
+ }
+ }
+
+ /**
+ * Returns an array containing all links for a menu.
+ *
+ * @param string $menu_name
+ * The name of the menu whose links should be returned.
+ *
+ * @return \Drupal\Core\Menu\MenuLinkInterface[]
+ * An array of menu link plugin instances keyed by ID.
+ */
+ public function loadLinks($menu_name) {
+ $instances = array();
+ $loaded = $this->treeStorage->loadByProperties(array('menu_name' => $menu_name));
+ foreach ($loaded as $plugin_id => $definition) {
+ // Setting the definition here means it will be used by getDefinition()
+ // which is called by createInstance() from the factory.
+ $this->definitions[$plugin_id] = $definition;
+ $instances[$plugin_id] = $this->createInstance($plugin_id);
+ }
+ return $instances;
+ }
+
+ /**
+ * Deletes all links for a menu.
+ *
+ * @todo - this should really only be called as part of the flow of
+ * deleting a menu entity, so maybe we should load it and make sure it's
+ * not locked?
+ *
+ * @param string $menu_name
+ * The name of the menu whose links will be deleted.
+ */
+ public function deleteLinksInMenu($menu_name) {
+ $affected_menus = array($menu_name => $menu_name);
+ foreach ($this->treeStorage->loadByProperties(array('menu_name' => $menu_name)) as $plugin_id => $definition) {
+ // Setting the definition here means it will be used by getDefinition()
+ // which is called by createInstance() from the factory.
+ $this->definitions[$plugin_id] = $definition;
+ $instance = $this->createInstance($plugin_id);
+ if ($instance->isResetable()) {
+ $new_instance = $this->resetInstance($instance);
+ $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
+ }
+ elseif ($instance->isDeletable()) {
+ $this->deleteInstance($instance, TRUE);
+ }
+ }
+ Cache::invalidateTags(array('menu' => $affected_menus));
+ }
+
+ /**
+ * Helper function to delete a specific instance.
+ */
+ protected function deleteInstance(MenuLinkInterface $instance, $persist) {
+ $id = $instance->getPluginId();
+ if ($instance->isDeletable()) {
+ if ($persist) {
+ $instance->deleteLink();
+ }
+ }
+ else {
+ throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $id));
+ }
+ $this->treeStorage->delete($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteLink($id, $persist = TRUE) {
+ $definition = $this->treeStorage->load($id);
+ // It's possible the definition has already been deleted, or doesn't exist.
+ if ($definition) {
+ // Setting the definition here means it will be used by getDefinition()
+ // which is called by createInstance() from the factory.
+ $this->definitions[$id] = $definition;
+ $instance = $this->createInstance($id);
+ $this->deleteInstance($instance, $persist);
+ // Many children may have moved.
+ $this->resetDefinitions();
+ Cache::invalidateTags(array('menu' => array($definition['menu_name'])));
+ }
+ $this->resetDefinition($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function countMenuLinks($menu_name = NULL) {
+ return $this->treeStorage->countMenuLinks($menu_name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadLinksByRoute($route_name, array $route_parameters = array(), $include_hidden = FALSE) {
+ $instances = array();
+ $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $include_hidden);
+ foreach ($loaded as $plugin_id => $definition) {
+ // Setting the definition here means it will be used by getDefinition()
+ // which is called by createInstance() from the factory.
+ $this->definitions[$plugin_id] = $definition;
+ $instances[$plugin_id] = $this->createInstance($plugin_id);
+ }
+ return $instances;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function maxDepth() {
+ return $this->treeStorage->maxDepth();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRenderTree($tree) {
+ $build = array();
+
+ foreach ($tree as $data) {
+ $class = array();
+ /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
+ $link = $data['link'];
+ // Generally we only deal with visible links, but just in case.
+ if ($link->isHidden()) {
+ continue;
+ }
+ // Set a class for the
-tag. Only set 'expanded' class if the link
+ // also has visible children within the current tree.
+ if ($data['has_children'] && $data['below']) {
+ $class[] = 'expanded';
+ }
+ elseif ($data['has_children']) {
+ $class[] = 'collapsed';
+ }
+ else {
+ $class[] = 'leaf';
+ }
+ // Set a class if the link is in the active trail.
+ if ($data['in_active_trail']) {
+ $class[] = 'active-trail';
+ }
+
+ // Allow menu-specific theme overrides.
+ $element['#theme'] = 'menu_link__' . strtr($link->getMenuName(), '-', '_');
+ $element['#attributes']['class'] = $class;
+ $element['#title'] = $link->getTitle();
+ $element['#url'] = $link->getUrlObject();
+ $element['#below'] = $data['below'] ? $this->buildRenderTree($data['below']) : array();
+ $element['#original_link'] = $link;
+ // Index using the link's unique ID.
+ $build[$link->getPluginId()] = $element;
+ }
+ if ($build) {
+ // Make sure drupal_render() does not re-order the links.
+ $build['#sorted'] = TRUE;
+ // Get the menu name from the last link.
+ $menu_name = $link->getMenuName();
+ // Add the theme wrapper for outer markup.
+ // Allow menu-specific theme overrides.
+ $build['#theme_wrappers'][] = 'menu_tree__' . strtr($menu_name, '-', '_');
+ // Set cache tag.
+ $build['#cache']['tags']['menu'][$menu_name] = $menu_name;
+ }
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getActiveTrailIds($menu_name) {
+ // Parent ids; used both as key and value to ensure uniqueness.
+ // We always want all the top-level links with parent == ''.
+ $active_trail = array('' => '');
+
+ $request = $this->requestStack->getCurrentRequest();
+
+ if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) {
+ $route_parameters = $request->attributes->get('_raw_variables')->all();
+ $page_is_403 = $request->attributes->get('_exception_statuscode') == 403;
+ // Find a menu link corresponding to the current path. If
+ // $active_path is NULL, let $this->menuLinkGetPreferred() determine the
+ // path.
+ if (!$page_is_403) {
+ $active_link = $this->menuLinkGetPreferred($route_name, $route_parameters, $menu_name);
+ if ($active_link && $active_link->getMenuName() == $menu_name) {
+ $active_trail += $this->treeStorage->getRootPathIds($active_link->getPluginId());
+ }
+ }
+ }
+ return $active_trail;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function menuLinkGetPreferred($route_name = NULL, array $route_parameters = array(), $selected_menu = NULL) {
+ if (!isset($route_name)) {
+ $request = $this->requestStack->getCurrentRequest();
+
+ $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME);
+ $route_parameters = $request->attributes->get('_raw_variables')->all();
+ }
+
+ $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account);
+ if (!$access) {
+ return NULL;
+ }
+ asort($route_parameters);
+ $route_key = $route_name . serialize($route_parameters);
+
+ if (empty($selected_menu)) {
+ // Use an illegal menu name as the key for the preferred menu link.
+ $selected_menu = '%';
+ }
+
+ if (!isset($this->preferredLinks[$route_key])) {
+ // Retrieve a list of menu names, ordered by preference.
+ $menu_names = $this->getActiveMenuNames();
+ // Put the selected menu at the front of the list.
+ array_unshift($menu_names, $selected_menu);
+ // If this menu name is not fond later, we want to just get NULL.
+ $this->preferredLinks[$route_key][$selected_menu] = NULL;
+
+ // Only load non-hidden links.
+ $definitions = $this->treeStorage->loadByRoute($route_name, $route_parameters);
+ // Sort candidates by menu name.
+ $candidates = array();
+ foreach ($definitions as $candidate) {
+ $candidates[$candidate['menu_name']] = $candidate;
+ $menu_names[] = $candidate['menu_name'];
+ }
+ foreach ($menu_names as $menu_name) {
+ if (isset($candidates[$menu_name]) && !isset($this->preferredLinks[$route_key][$menu_name])) {
+ $candidate = $candidates[$menu_name];
+ $this->definitions[$candidate['id']] = $candidate;
+ $instance = $this->createInstance($candidate['id']);
+ $this->preferredLinks[$route_key][$menu_name] = $instance;
+ if (!isset($this->preferredLinks[$route_key]['%'])) {
+ $this->preferredLinks[$route_key]['%'] = $instance;
+ }
+ }
+ }
+
+ }
+ return isset($this->preferredLinks[$route_key][$selected_menu]) ? $this->preferredLinks[$route_key][$selected_menu] : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getActiveMenuNames() {
+ return $this->activeMenus;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setActiveMenuNames(array $menu_names) {
+
+ if (isset($menu_names) && is_array($menu_names)) {
+ $this->activeMenus = $menu_names;
+ }
+ elseif (!isset($this->activeMenus)) {
+ $config = $this->configFactory->get('system.menu');
+ $this->activeMenus = $config->get('active_menus_default') ?: array_keys($this->listSystemMenus());
+ }
+ }
+
+ /**
+ * Returns an array containing the names of system-defined (default) menus.
+ */
+ protected function listSystemMenus() {
+ // For simplicity and performance, this is simply a hard-coded list copied
+ // from menu_list_system_menus() which is simply the list of all Menu config
+ // entities that are shipped with system module.
+ return array(
+ 'tools' => 'Tools',
+ 'admin' => 'Administration',
+ 'account' => 'User account menu',
+ 'main' => 'Main navigation',
+ 'footer' => 'Footer menu',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
+ $language_interface = $this->languageManager->getCurrentLanguage();
+
+ // Load the request corresponding to the current page.
+ $request = $this->requestStack->getCurrentRequest();
+ $page_is_403 = FALSE;
+ $system_path = NULL;
+ if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) {
+ $system_path = $request->attributes->get('_system_path');
+ $page_is_403 = $request->attributes->get('_exception_statuscode') == 403;
+ }
+
+ if (isset($max_depth)) {
+ $max_depth = min($max_depth, $this->treeStorage->maxDepth());
+ }
+ // Generate a cache ID (cid) specific for this page.
+ $cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_is_403 . ':' . (int) $max_depth;
+ // If we are asked for the active trail only, and $menu_name has not been
+ // built and cached for this page yet, then this likely means that it
+ // won't be built anymore, as this function is invoked from
+ // template_preprocess_page(). So in order to not build a giant menu tree
+ // that needs to be checked for access on all levels, we simply check
+ // whether we have the menu already in cache, or otherwise, build a
+ // minimum tree containing the active trail only.
+ // @see menu_set_active_trail()
+ if (!isset($this->menuPageTrees[$cid]) && $only_active_trail) {
+ $cid .= ':trail';
+ }
+
+ // @todo Decide whether it makes sense to static cache page menu trees.
+ if (!isset($this->menuPageTrees[$cid])) {
+ // If the static variable doesn't have the data, check {cache_menu}.
+ $cache = $this->treeCacheBackend->get($cid);
+ if ($cache && isset($cache->data)) {
+ // If the cache entry exists, it contains the parameters for
+ // menu_build_tree().
+ $tree_parameters = $cache->data;
+ }
+ else {
+ $tree_parameters = $this->doBuildPageDataTreeParameters($menu_name, $max_depth, $only_active_trail, $page_is_403);
+
+ // Cache the tree building parameters using the page-specific cid.
+ $this->treeCacheBackend->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
+ }
+
+ // Build the tree using the parameters; the resulting tree will be cached
+ // by $this->buildTree()).
+ $this->menuPageTrees[$cid] = $this->buildTree($menu_name, $tree_parameters);
+ }
+ return $this->menuPageTrees[$cid];
+ }
+
+ /**
+ * Determines the required tree parameters used for the page menu tree.
+ *
+ * This method takes into account the active trail of the current page.
+ *
+ * @param string $menu_name
+ * The menu name.
+ * @param int $max_depth
+ * The maximum allowed depth of menus.
+ * @param bool $only_active_trail
+ * If TRUE, just load level 0 plus the active trail, otherwise load the full
+ * menu tree.
+ * @param bool $page_is_403
+ * Is the current request happening on a 403 subrequest.
+ *
+ * @return array
+ * An array of tree parameters.
+ */
+ protected function doBuildPageDataTreeParameters($menu_name, $max_depth, $only_active_trail, $page_is_403) {
+ $tree_parameters = array(
+ 'min_depth' => 1,
+ 'max_depth' => $max_depth,
+ );
+
+ // If this page is accessible to the current user, build the tree
+ // parameters accordingly.
+ if (!$page_is_403) {
+ $active_trail = $this->getActiveTrailIds($menu_name);
+ // The active trail contains more than only array(0 => 0).
+ if (count($active_trail) > 1) {
+ // If we are asked to build links for the active trail only,skip
+ // the entire 'expanded' handling.
+ if ($only_active_trail) {
+ $tree_parameters['only_active_trail'] = TRUE;
+ }
+ }
+ $parents = $active_trail;
+
+ if (!$only_active_trail) {
+ // Collect all the links set to be expanded, and then add all of
+ // their children to the list as well.
+ $parents = $this->treeStorage->getExpanded($menu_name, $parents);
+ }
+ }
+ else {
+ // If access is denied, we only show top-level links in menus.
+ $active_trail = array('' => '');
+ $parents = $active_trail;
+ }
+ $tree_parameters['expanded'] = $parents;
+ $tree_parameters['active_trail'] = $active_trail;
+ return $tree_parameters;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @todo should this accept a menu link instance or just the ID?
+ */
+ public function buildAllData($menu_name, $id = NULL, $max_depth = NULL) {
+ $language_interface = $this->languageManager->getCurrentLanguage();
+
+ // Use ID as a flag for whether the data being loaded is for the whole
+ // tree.
+ $id = isset($id) ? $id : '%';
+ // Generate a cache ID (cid) specific for this $menu_name, $link, $language,
+ // and depth.
+ $cid = 'links:' . $menu_name . ':all:' . $id . ':' . $language_interface->id . ':' . (int) $max_depth;
+ if (!isset($this->buildAllDataParameters[$cid])) {
+ $tree_parameters = array(
+ 'min_depth' => 1,
+ 'max_depth' => $max_depth,
+ );
+ if ($id != '%') {
+ // The tree is for a single item, so we need to match the values in
+ // of all the IDs on the path to root.
+ $tree_parameters['active_trail'] = $this->treeStorage->getRootPathIds($id);
+ $tree_parameters['expanded'] = $tree_parameters['active_trail'];
+ // Include top-level links.
+ $tree_parameters['expanded'][''] = '';
+ }
+ $this->buildAllDataParameters[$cid] = $tree_parameters;
+ }
+ // Build the tree using the parameters; the resulting tree will be cached
+ // by buildTree().
+ return $this->buildTree($menu_name, $this->buildAllDataParameters[$cid]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChildLinks($id, $max_relative_depth = NULL) {
+ $links = array();
+ $definitions = $this->treeStorage->loadAllChildLinks($id, $max_relative_depth);
+ foreach ($definitions as $id => $definition) {
+ $instance = $this->menuLinkCheckAccess($definition);
+ if ($instance) {
+ $links[$id] = $instance;
+ }
+ }
+ return $links;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParentIds($id) {
+ if ($this->getDefinition($id, FALSE)) {
+ return $this->treeStorage->getRootPathIds($id);
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChildIds($id) {
+ if ($this->getDefinition($id, FALSE)) {
+ return $this->treeStorage->getAllChildIds($id);
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildSubtree($id, $max_relative_depth = NULL) {
+ $subtree = $this->treeStorage->loadSubtree($id, $max_relative_depth);
+ if ($subtree) {
+ // Check access and instantiate. @todo rename these methods.
+ $instance = $this->menuLinkCheckAccess($subtree['definition']);
+ if ($instance) {
+ $subtree['link'] = $instance;
+ $route_names = $this->collectRoutes($subtree['below']);
+ // Pre-load all the route objects in the tree for access checks.
+ if ($route_names) {
+ $this->routeProvider->getRoutesByNames($route_names);
+ }
+ $this->treeCheckAccess($subtree['below']);
+ return $subtree;
+ }
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildTree($menu_name, array $parameters = array()) {
+ $language_interface = $this->languageManager->getCurrentLanguage();
+
+ // Build the cache id; sort parents to prevent duplicate storage and remove
+ // default parameter values.
+ asort($parameters);
+ if (isset($parameters['expanded'])) {
+ sort($parameters['expanded']);
+ }
+ $tree_cid = 'links:' . $menu_name . ':tree-data:' . $language_interface->id . ':' . hash('sha256', serialize($parameters));
+
+ // If we do not have this tree in the static cache, check cache.menu.
+ if (!isset($this->menuTree[$tree_cid])) {
+ $cache = $this->treeCacheBackend->get($tree_cid);
+ if ($cache && isset($cache->data)) {
+ $this->menuTree[$tree_cid] = $cache->data;
+ }
+ }
+
+ if (!isset($this->menuTree[$tree_cid])) {
+ // Rebuild the links which are stored.
+ $data['tree'] = $this->treeStorage->loadTree($menu_name, $parameters);
+ $data['route_names'] = $this->collectRoutes($data['tree']);
+ // Cache the data, if it is not already in the cache.
+ $this->treeCacheBackend->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name));
+ $this->menuTree[$tree_cid] = $data;
+ }
+ else {
+ $data = $this->menuTree[$tree_cid];
+ }
+
+ // Pre-load all the route objects in the tree for access checks.
+ if ($data['route_names']) {
+ $this->routeProvider->getRoutesByNames($data['route_names']);
+ }
+ $tree = $data['tree'];
+ $this->treeCheckAccess($tree);
+ return $tree;
+ }
+
+ /**
+ * Traverses the menu tree and collects all the route names.
+ *
+ * @param array $tree
+ * The menu tree you wish to operate on.
+ *
+ * @return array
+ * Array of route names, with all values being unique.
+ */
+ protected function collectRoutes($tree) {
+ return array_values($this->doCollectRoutes($tree));
+ }
+
+ /**
+ * Recursive helper function to collect all the route names.
+ */
+ protected function doCollectRoutes($tree) {
+ $route_names = array();
+ foreach ($tree as $key => $v) {
+ $definition = $tree[$key]['definition'];
+ if (!empty($definition['route_name'])) {
+ $route_names[$definition['route_name']] = $definition['route_name'];
+ }
+ if ($tree[$key]['below']) {
+ $route_names += $this->doCollectRoutes($tree[$key]['below']);
+ }
+ }
+ return $route_names;
+ }
+
+ /**
+ * Sorts the menu tree and recursively checks access for each item.
+ *
+ * @param array $tree
+ * The menu tree you wish to operate on.
+ */
+ protected function treeCheckAccess(&$tree) {
+ $this->doTreeCheckAccess($tree);
+ $this->sortTree($tree);
+ }
+
+ /**
+ * Helper function that recursively checks access for each item.
+ */
+ protected function doTreeCheckAccess(&$tree) {
+ foreach ($tree as $key => $v) {
+ $definition = $tree[$key]['definition'];
+ // Setting the definition here means it will be used by getDefinition()
+ // which is called by createInstance() from the factory.
+ $this->definitions[$definition['id']] = $definition;
+ $instance = $this->menuLinkCheckAccess($definition);
+ if ($instance) {
+ $tree[$key]['link'] = $instance;
+ if ($tree[$key]['below']) {
+ $this->doTreeCheckAccess($tree[$key]['below']);
+ }
+ unset($tree[$key]['definition']);
+ }
+ else {
+ unset($tree[$key]);
+ }
+ }
+ }
+
+ /**
+ * Sorts the menu tree and recursively using the weight and title.
+ *
+ * @param array $tree
+ * The menu tree you wish to operate on.
+ */
+ protected function sortTree(&$tree) {
+ $new_tree = array();
+ foreach ($tree as $key => $v) {
+ if ($tree[$key]['below']) {
+ $this->sortTree($tree[$key]['below']);
+ }
+ $instance = $tree[$key]['link'];
+ // The weights are made a uniform 5 digits by adding 50000 as an offset.
+ // After $this->menuLinkCheckAccess(), $instance->getTitle() has the
+ // localized or translated title. Adding the plugin id to the end of the
+ // index insures that it is unique.
+ $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key];
+ }
+ // Sort siblings in the tree based on the weights and localized titles.
+ ksort($new_tree);
+ $tree = $new_tree;
+ }
+
+ /**
+ * Check access for the item and create an instance if it is accessible.
+ *
+ * @param array $definition
+ * The menu link definition.
+ *
+ * @return \Drupal\Core\Menu\MenuLinkInterface|NULL
+ * A plugin instance or NULL if the current user can not access its route.
+ */
+ protected function menuLinkCheckAccess(array $definition) {
+ // 'url' should only be populated for external links.
+ if (!empty($definition['url']) && empty($definition['route_name'])) {
+ $access = TRUE;
+ }
+ else {
+ $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account);
+ }
+ // For performance, don't instantiate a link the user can't access.
+ if ($access) {
+ return $this->createInstance($definition['id']);
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createLink($id, array $definition) {
+ // Add defaults and other stuff, so there is no requirement to specify
+ // everything.
+ $this->processDefinition($definition, $id);
+
+ // Store the new link in the tree and invalidate some caches.
+ $affected_menus = $this->treeStorage->save($definition);
+ Cache::invalidateTags(array('menu' => $affected_menus));
+ $this->resetDefinition($id);
+ return $this->createInstance($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function updateLink($id, array $new_definition_values, $persist = TRUE) {
+ $instance = $this->createInstance($id);
+ if ($instance) {
+ $new_definition_values['id'] = $id;
+ $changed_definition = $instance->updateLink($new_definition_values, $persist);
+ $affected_menus = $this->treeStorage->save($changed_definition);
+ $this->moduleHandler->invokeAll('menu_link_update', array($changed_definition));
+ $this->resetDefinitions();
+ Cache::invalidateTags(array('menu' => $affected_menus));
+ }
+ return $instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPluginForm(MenuLinkInterface $menu_link) {
+ $class_name = $menu_link->getFormClass();
+ /** @var \Drupal\Core\Menu\Form\MenuLinkFormInterface $form */
+ if (in_array('Drupal\Core\DependencyInjection\ContainerInjectionInterface', class_implements($class_name))) {
+ $form = $class_name::create(\Drupal::getContainer());
+ }
+ else {
+ $form = new $class_name();
+ }
+ $form->setMenuLinkInstance($menu_link);
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParentSelectOptions($id = '', array $menus = array()) {
+ // @todo: Core allows you to replace the select element ... this is a sign
+ // that we might want to write a form element as well, which can be swapped.
+ if (empty($menus)) {
+ $menus = $this->getMenuOptions();
+ }
+
+ $options = array();
+ $depth_limit = $this->getParentDepthLimit($id);
+ foreach ($menus as $menu_name => $menu_title) {
+ $options[$menu_name . ':'] = '<' . $menu_title . '>';
+
+ $tree = $this->buildAllData($menu_name, NULL, $depth_limit);
+ $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit);
+ }
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParentDepthLimit($id) {
+ if ($id) {
+ $limit = $this->treeStorage->maxDepth() - $this->treeStorage->getSubtreeHeight($id);
+ }
+ else {
+ $limit = $this->treeStorage->maxDepth() - 1;
+ }
+ return $limit;
+ }
+
+ /**
+ * Iterates over all items in the tree to prepare the parents select options.
+ *
+ * @param array $tree
+ * The menu tree.
+ * @param string $menu_name
+ * The menu name.
+ * @param string $indent
+ * The indentation string used for the label.
+ * @param array $options
+ * The select options.
+ * @param string $exclude
+ * An excluded menu link.
+ * @param int $depth_limit
+ * The maximum depth of menu links considered for the select options.
+ */
+ protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit) {
+ foreach ($tree as $data) {
+ if ($data['depth'] > $depth_limit) {
+ // Don't iterate through any links on this level.
+ break;
+ }
+ /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
+ $link = $data['link'];
+ if ($link->getPluginId() != $exclude) {
+ $title = $indent . ' ' . Unicode::truncate($link->getTitle(), 30, TRUE, FALSE);
+ if ($link->isHidden()) {
+ $title .= ' (' . t('disabled') . ')';
+ }
+ $options[$menu_name . ':' . $link->getPluginId()] = $title;
+ if ($data['below']) {
+ $this->parentSelectOptionsTreeWalk($data['below'], $menu_name, $indent . '--', $options, $exclude, $depth_limit);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMenuOptions(array $menu_names = NULL) {
+ $menus = $this->entityManager->getStorage('menu')->loadMultiple($menu_names);
+ $options = array();
+ foreach ($menus as $menu) {
+ $options[$menu->id()] = $menu->label();
+ }
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function menuNameInUse($menu_name) {
+ $this->treeStorage->menuNameInUse($menu_name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resetLink($id) {
+ $instance = $this->createInstance($id);
+ $affected_menus[$instance->getMenuName()] = $instance->getMenuName();
+ $new_instance = $this->resetInstance($instance);
+ $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
+ Cache::invalidateTags(array('menu' => $affected_menus));
+ return $new_instance;
+ }
+
+ /**
+ * Resets the menu link to its default settings.
+ *
+ * @param \Drupal\Core\Menu\MenuLinkInterface $instance
+ * The menu link which should be reset.
+ *
+ * @return \Drupal\Core\Menu\MenuLinkInterface
+ * The reset menu link.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ * Thrown when the menu link is not resetable.
+ */
+ protected function resetInstance(MenuLinkInterface $instance) {
+ $id = $instance->getPluginId();
+
+ if (!$instance->isResetable()) {
+ throw new PluginException(String::format('Menu link %id is not resetable', array('%id' => $id)));
+ }
+ // Get the original data from disk, reset the override and re-save the menu
+ // tree for this link.
+ $definition = $this->getDefinitions()[$id];
+ $this->overrides->deleteOverride($id);
+ $this->resetDefinition($id, $definition);
+ $this->treeStorage->save($definition);
+ return $this->createInstance($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resetDefinitions() {
+ $this->definitions = array();
+ $this->menuTree = array();
+ $this->buildAllDataParameters = array();
+ $this->menuPageTrees = array();
+ }
+
+ /**
+ * Resets the local definition cache for one plugin.
+ *
+ * @param string $id
+ * The menu link plugin ID.
+ * @param array $definition
+ * Optional new definition for the given plugin ID.
+ */
+ protected function resetDefinition($id, $definition = NULL) {
+ $this->definitions[$id] = $definition;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php
new file mode 100644
index 0000000..a75e284
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php
@@ -0,0 +1,397 @@
+connection = $connection;
+ $this->urlGenerator = $url_generator;
+ $this->table = $table;
+ $this->options = $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function maxDepth() {
+ return static::MAX_DEPTH;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rebuild(array $definitions) {
+ $links = array();
+ $children = array();
+ $top_links = array();
+ if ($definitions) {
+ foreach ($definitions as $id => $link) {
+ if (!empty($link['parent'])) {
+ $children[$link['parent']][$id] = $id;
+ }
+ else {
+ // A top level link - we need them to root our tree.
+ $top_links[$id] = $id;
+ $link['parent'] = '';
+ }
+ $links[$id] = $link;
+ }
+ }
+ foreach ($top_links as $id) {
+ $this->saveRecursive($id, $children, $links);
+ }
+ // Handle any children we didn't find starting from top-level links.
+ foreach ($children as $orphan_links) {
+ foreach ($orphan_links as $id) {
+ // Force it to the top level.
+ $links[$id]['parent'] = '';
+ $this->saveRecursive($id, $children, $links);
+ }
+ }
+ // Find any previously discovered menu links that no longer exist.
+ if ($definitions) {
+ $query = $this->connection->select($this->table, NULL, $this->options);
+ $query->addField($this->table, 'id');
+ $query->condition('discovered', 1);
+ $query->condition('id', array_keys($definitions), 'NOT IN');
+ $query->orderBy('depth', 'DESC');
+ $result = $query->execute()->fetchCol();
+ }
+ else {
+ $result = array();
+ }
+
+ // Remove all such items. Starting from those with the greatest depth will
+ // minimize the amount of re-parenting done by the menu link controller.
+ if ($result) {
+ $this->purgeMultiple($result);
+ }
+ }
+
+ /**
+ * Purges multiple menu links that no longer exist.
+ *
+ * @param array $ids
+ * An array of menu link IDs.
+ * @param bool $prevent_reparenting
+ * (optional) Disables the re-parenting logic from the deletion process.
+ * Defaults to FALSE.
+ */
+ protected function purgeMultiple(array $ids, $prevent_reparenting = FALSE) {
+ if (!$prevent_reparenting) {
+ $loaded = $this->loadFullMultiple($ids);
+ foreach ($loaded as $id => $link) {
+ if ($link['has_children']) {
+ $children = $this->loadByProperties(array('parent' => $id));
+ foreach ($children as $child) {
+ $child['parent'] = $link['parent'];
+ $this->save($child);
+ }
+ }
+ }
+ }
+ $query = $this->connection->delete($this->table, $this->options);
+ $query->condition('id', $ids, 'IN');
+ $query->execute();
+ }
+
+ /**
+ * Executes a select query while making sure the database table exists.
+ *
+ * @param \Drupal\Core\Database\Query\SelectInterface $query
+ * The select object to be executed.
+ *
+ * @return \Drupal\Core\Database\StatementInterface|null
+ * A prepared statement, or NULL if the query is not valid.
+ *
+ * @throws \Exception
+ * If the table could not be created or the database connection failed.
+ */
+ protected function safeExecuteSelect(SelectInterface $query) {
+ try {
+ return $query->execute();
+ }
+ catch (\Exception $e) {
+ // If there was an exception, try to create the table.
+ if ($this->ensureTableExists()) {
+ return $query->execute();
+ }
+ // Some other failure that we can not recover from.
+ throw $e;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $link) {
+ $original = $this->loadFull($link['id']);
+ // @todo - should we just return here if the links values match the original
+ // values completely?.
+
+ $affected_menus = array();
+
+ $transaction = $this->connection->startTransaction();
+ try {
+ if ($original) {
+ $link['mlid'] = $original['mlid'];
+ $link['has_children'] = $original['has_children'];
+ $affected_menus[$original['menu_name']] = $original['menu_name'];
+ }
+ else {
+ // Generate a new mlid.
+ $options = array('return' => Database::RETURN_INSERT_ID) + $this->options;
+ $link['mlid'] = $this->connection->insert($this->table, $options)
+ ->fields(array('id' => $link['id'], 'menu_name' => $link['menu_name']))
+ ->execute();
+ }
+ $fields = $this->preSave($link, $original);
+ // We may be moving the link to a new menu.
+ $affected_menus[$fields['menu_name']] = $fields['menu_name'];
+ $query = $this->connection->update($this->table, $this->options);
+ $query->condition('mlid', $link['mlid']);
+ $query->fields($fields)
+ ->execute();
+ if ($original) {
+ $this->updateParentalStatus($original);
+ }
+ $this->updateParentalStatus($link);
+ // Ignore slave server temporarily.
+ db_ignore_slave();
+ }
+ catch (\Exception $e) {
+ $transaction->rollback();
+ throw $e;
+ }
+ return $affected_menus;
+ }
+
+ /**
+ * Using the link definition, but up all the fields needed for database save.
+ *
+ * @param array $link
+ * The link definition to be updated.
+ * @param array $original
+ * The link definition before the changes. May be empty if not found.
+ *
+ * @return array
+ * The values which will be stored.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ * Thrown when the specific depth exceeds the maximum.
+ */
+ protected function preSave(array &$link, array $original) {
+ static $schema_fields, $schema_defaults;
+ if (empty($schema_fields)) {
+ $schema = static::schemaDefinition();
+ $schema_fields = $schema['fields'];
+ foreach ($schema_fields as $name => $spec) {
+ if (isset($spec['default'])) {
+ $schema_defaults[$name] = $spec['default'];
+ }
+ }
+ }
+
+ // Try to find a parent link. If found, assign it and derive its menu.
+ $parent = $this->findParent($link, $original);
+ if ($parent) {
+ $link['parent'] = $parent['id'];
+ $link['menu_name'] = $parent['menu_name'];
+ }
+ else {
+ $link['parent'] = '';
+ }
+
+ // If no corresponding parent link was found, move the link to the
+ // top-level.
+ foreach ($schema_defaults as $name => $default) {
+ if (!isset($link[$name])) {
+ $link[$name] = $default;
+ }
+ }
+ $fields = array_intersect_key($link, $schema_fields);
+ asort($fields['route_parameters']);
+ // Since this will be urlencoded, it's safe to store and match against a
+ // text field.
+ $fields['route_param_key'] = $fields['route_parameters'] ? UrlHelper::buildQuery($fields['route_parameters']) : '';
+
+ foreach ($this->serializedFields() as $name) {
+ $fields[$name] = serialize($fields[$name]);
+ }
+
+ // Directly fill parents for top-level links.
+ if (empty($link['parent'])) {
+ $fields['p1'] = $link['mlid'];
+ for ($i = 2; $i <= $this->maxDepth(); $i++) {
+ $fields["p$i"] = 0;
+ }
+ $fields['depth'] = 1;
+ }
+ // Otherwise, ensure that this link's depth is not beyond the maximum depth
+ // and fill parents based on the parent link.
+ else {
+ // @todo - we want to also check $original['has_children'] here, but that
+ // will be 0 even if there are children if those are hidden.
+ // has_children is really just the rendering hint. So, we either need
+ // to define another column (has_any_children), or do the extra query.
+ if ($original) {
+ $limit = $this->maxDepth() - $this->doFindChildrenRelativeDepth($original) - 1;
+ }
+ else {
+ $limit = $this->maxDepth() - 1;
+ }
+ if ($parent['depth'] > $limit) {
+ throw new PluginException(sprintf('The link with ID %s or its children exceeded the maximum depth of %d', $link['id'], $this->maxDepth()));
+ }
+ $this->setParents($fields, $parent);
+ }
+
+ // Need to check both parent and menu_name, since parent can be empty in any
+ // menu.
+ if ($original && ($link['parent'] != $original['parent'] || $link['menu_name'] != $original['menu_name'])) {
+ $this->moveChildren($fields, $original);
+ }
+ // We needed the mlid above, but not in the update query.
+ unset($fields['mlid']);
+
+ // Cast booleans to int, if needed.
+ $fields['hidden'] = (int) $fields['hidden'];
+ $fields['expanded'] = (int) $fields['expanded'];
+ return $fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($id) {
+ // Children get re-attached to the menu link's parent.
+ $item = $this->loadFull($id);
+ // It's possible the link is already deleted.
+ if ($item) {
+ $parent = $item['parent'];
+ $children = $this->loadByProperties(array('parent' => $id));
+ foreach ($children as $child) {
+ $child['parent'] = $parent;
+ $this->save($child);
+ }
+
+ $this->connection->delete($this->table, $this->options)
+ ->condition('id', $id)
+ ->execute();
+
+ $this->updateParentalStatus($item);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSubtreeHeight($id) {
+ $original = $this->loadFull($id);
+ return $original ? $this->doFindChildrenRelativeDepth($original) + 1 : 0;
+ }
+
+ /**
+ * Finds the relative depth of this link's deepest child.
+ *
+ * @param array $original
+ * The parent definition used to find the depth.
+ *
+ * @return int
+ * Returns the relative depth.
+ */
+ protected function doFindChildrenRelativeDepth(array $original) {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->addField($this->table, 'depth');
+ $query->condition('menu_name', $original['menu_name']);
+ $query->orderBy('depth', 'DESC');
+ $query->range(0, 1);
+
+ for ($i = 1; $i <= static::MAX_DEPTH && $original["p$i"]; $i++) {
+ $query->condition("p$i", $original["p$i"]);
+ }
+
+ $max_depth = $this->safeExecuteSelect($query)->fetchField();
+
+ return ($max_depth > $original['depth']) ? $max_depth - $original['depth'] : 0;
+ }
+
+ /**
+ * Sets the materialized path field values based on the parent.
+ *
+ * @param array $fields
+ * The menu link.
+ * @param array $parent
+ * The parent menu link.
+ */
+ protected function setParents(array &$fields, array $parent) {
+ $fields['depth'] = $parent['depth'] + 1;
+ $i = 1;
+ while ($i < $fields['depth']) {
+ $p = 'p' . $i++;
+ $fields[$p] = $parent[$p];
+ }
+ $p = 'p' . $i++;
+ // The parent (p1 - p9) corresponding to the depth always equals the mlid.
+ $fields[$p] = $fields['mlid'];
+ while ($i <= static::MAX_DEPTH) {
+ $p = 'p' . $i++;
+ $fields[$p] = 0;
+ }
+ }
+
+ /**
+ * Moves the link's children using the query fields value and original values.
+ *
+ * @param array $fields
+ * The changed menu link.
+ * @param array $original
+ * The original menu link.
+ */
+ protected function moveChildren($fields, $original) {
+ $query = $this->connection->update($this->table, $this->options);
+
+ $query->fields(array('menu_name' => $fields['menu_name']));
+
+ $expressions = array();
+ for ($i = 1; $i <= $fields['depth']; $i++) {
+ $expressions[] = array("p$i", ":p_$i", array(":p_$i" => $fields["p$i"]));
+ }
+ $j = $original['depth'] + 1;
+ while ($i <= $this->maxDepth() && $j <= $this->maxDepth()) {
+ $expressions[] = array('p' . $i++, 'p' . $j++, array());
+ }
+ while ($i <= $this->maxDepth()) {
+ $expressions[] = array('p' . $i++, 0, array());
+ }
+
+ $shift = $fields['depth'] - $original['depth'];
+ if ($shift > 0) {
+ // The order of expressions must be reversed so the new values don't
+ // overwrite the old ones before they can be used because "Single-table
+ // UPDATE assignments are generally evaluated from left to right"
+ // @see http://dev.mysql.com/doc/refman/5.0/en/update.html
+ $expressions = array_reverse($expressions);
+ }
+ foreach ($expressions as $expression) {
+ $query->expression($expression[0], $expression[1], $expression[2]);
+ }
+
+ $query->expression('depth', 'depth + :depth', array(':depth' => $shift));
+ $query->condition('menu_name', $original['menu_name']);
+
+ for ($i = 1; $i <= $this->maxDepth() && $original["p$i"]; $i++) {
+ $query->condition("p$i", $original["p$i"]);
+ }
+
+ $query->execute();
+ }
+
+ /**
+ * Loads the parent definition if it exists.
+ *
+ * @param array $link
+ * The link definition to check.
+ * @param array|FALSE $original
+ * The original link, or FALSE.
+ *
+ * @return array|FALSE
+ * Returns a definition array, or FALSE if no parent was found.
+ */
+ protected function findParent($link, $original) {
+ $parent = FALSE;
+
+ // This item is explicitly top-level, skip the rest of the parenting.
+ if (isset($link['parent']) && empty($link['parent'])) {
+ return $parent;
+ }
+
+ // If we have a parent link ID, try to use that.
+ $candidates = array();
+ if (isset($link['parent'])) {
+ $candidates[] = $link['parent'];
+ }
+ elseif (!empty($original['parent']) && $link['menu_name'] == $original['menu_name']) {
+ $candidates[] = $original['parent'];
+ }
+
+ // Else, if we have a link hierarchy try to find a valid parent in there.
+ // @todo - why does this make sense to do at all?
+
+ foreach ($candidates as $id) {
+ $parent = $this->loadFull($id);
+ if ($parent) {
+ break;
+ }
+ }
+ return $parent;
+ }
+
+ /**
+ * Set the has_children flag for the link's parent if it has visible children.
+ *
+ * @param array $link
+ * The link to get a parent ID from.
+ */
+ protected function updateParentalStatus(array $link) {
+ // If parent is empty, there is nothing to update.
+ if (!empty($link['parent'])) {
+ // Check if at least one visible child exists in the table.
+ $query = $this->connection->select($this->table, $this->options);
+ $query->addExpression('1');
+ $query->range(0, 1);
+ $query
+ ->condition('menu_name', $link['menu_name'])
+ ->condition('parent', $link['parent'])
+ ->condition('hidden', 0);
+
+ $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
+ $this->connection->update($this->table, $this->options)
+ ->fields(array('has_children' => $parent_has_children))
+ ->condition('id', $link['parent'])
+ ->execute();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadByProperties(array $properties) {
+ // @todo - only allow loading by plugin definition properties.
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table, $this->definitionFields());
+ foreach ($properties as $name => $value) {
+ $query->condition($name, $value);
+ }
+ $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+ foreach ($loaded as &$link) {
+ foreach ($this->serializedFields() as $name) {
+ $link[$name] = unserialize($link[$name]);
+ }
+ }
+ return $loaded;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadByRoute($route_name, array $route_parameters = array(), $include_hidden = FALSE) {
+ asort($route_parameters);
+ // Since this will be urlencoded, it's safe to store and match against a
+ // text field.
+ // @todo - does this make more sense than using the system path?
+ $param_key = $route_parameters ? UrlHelper::buildQuery($route_parameters) : '';
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table, $this->definitionFields());
+ $query->condition('route_name', $route_name);
+ $query->condition('route_param_key', $param_key);
+ if (!$include_hidden) {
+ $query->condition('hidden', 0);
+ }
+ $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+ foreach ($loaded as &$link) {
+ foreach ($this->serializedFields() as $name) {
+ $link[$name] = unserialize($link[$name]);
+ }
+ }
+ return $loaded;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadMultiple(array $ids) {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table, $this->definitionFields());
+ $query->condition('id', $ids, 'IN');
+ $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+ foreach ($loaded as &$link) {
+ foreach ($this->serializedFields() as $name) {
+ $link[$name] = unserialize($link[$name]);
+ }
+ }
+ return $loaded;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load($id) {
+ $loaded = $this->loadMultiple(array($id));
+ return isset($loaded[$id]) ? $loaded[$id] : FALSE;
+ }
+
+ /**
+ * Loads all table fields, not just those that are in the plugin definition.
+ *
+ * @param string $id
+ * The menu link ID.
+ *
+ * @return array
+ * The loaded menu link definition or an empty array if not be found.
+ */
+ protected function loadFull($id) {
+ $loaded = $this->loadFullMultiple(array($id));
+ return isset($loaded[$id]) ? $loaded[$id] : array();
+ }
+
+ /**
+ * Loads multiple menu link definitions by ID.
+ *
+ * @param array $ids
+ * The IDs to load.
+ *
+ * @return array
+ * The loaded menu link definitions.
+ */
+ protected function loadFullMultiple(array $ids) {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table);
+ $query->condition('id', $ids, 'IN');
+ $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+ foreach ($loaded as &$link) {
+ foreach ($this->serializedFields() as $name) {
+ $link[$name] = unserialize($link[$name]);
+ }
+ }
+ return $loaded;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRootPathIds($id) {
+ $subquery = $this->connection->select($this->table, $this->options);
+ // @todo - consider making this dynamic based on static::MAX_DEPTH
+ // or from the schema if that is generated using static::MAX_DEPTH.
+ $subquery->fields($this->table, array('p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'));
+ $subquery->condition('id', $id);
+ $result = current($subquery->execute()->fetchAll(\PDO::FETCH_ASSOC));
+ $ids = array_filter($result);
+ if ($ids) {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table, array('id'));
+ $query->orderBy('depth', 'DESC');
+ $query->condition('mlid', $ids, 'IN');
+ // @todo - cache this result in memory if we find it's being used more
+ // than once per page load.
+ return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+ }
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExpanded($menu_name, array $parents) {
+ // @todo - go back to tracking in state or some other way
+ // which menus have expanded links?
+ do {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table, array('id'));
+ $query->condition('menu_name', $menu_name);
+ $query->condition('expanded', 1);
+ $query->condition('has_children', 1);
+ $query->condition('hidden', 0);
+ $query->condition('parent', $parents, 'IN');
+ $query->condition('id', $parents, 'NOT IN');
+ $result = $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+ $parents += $result;
+ } while (!empty($result));
+ return $parents;
+ }
+
+ /**
+ * Saves menu links recursively.
+ */
+ protected function saveRecursive($id, &$children, &$links) {
+
+ if (!empty($links[$id]['parent']) && empty($links[$links[$id]['parent']])) {
+ // Invalid parent ID, so remove it.
+ $links[$id]['parent'] = '';
+ }
+ $this->save($links[$id]);
+
+ if (!empty($children[$id])) {
+ foreach ($children[$id] as $next_id) {
+ $this->saveRecursive($next_id, $children, $links);
+ }
+ }
+ // Remove processed link names so we can find stragglers.
+ unset($children[$id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadTree($menu_name, array $parameters = array()) {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table);
+ for ($i = 1; $i <= $this->maxDepth(); $i++) {
+ $query->orderBy('p' . $i, 'ASC');
+ }
+
+ $query->condition('menu_name', $menu_name);
+
+ if (!empty($parameters['expanded'])) {
+ $query->condition('parent', $parameters['expanded'], 'IN');
+ }
+ elseif (!empty($parameters['only_active_trail'])) {
+ $query->condition('id', $parameters['active_trail'], 'IN');
+ }
+ $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : NULL);
+ if ($min_depth) {
+ $query->condition('depth', $min_depth, '>=');
+ }
+ if (isset($parameters['max_depth'])) {
+ $query->condition('depth', $parameters['max_depth'], '<=');
+ }
+ // Add custom query conditions, if any were passed.
+ if (!empty($parameters['conditions'])) {
+ // Only allow conditions that are testing definition fields.
+ $parameters['conditions'] = array_intersect_key($parameters['conditions'], array_flip($this->definitionFields()));
+ foreach ($parameters['conditions'] as $column => $value) {
+ $query->condition($column, $value);
+ }
+ }
+ $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
+ $links = $this->safeExecuteSelect($query)->fetchAll(\PDO::FETCH_ASSOC);
+ if (!isset($min_depth)) {
+ $first_link = reset($links);
+ if ($first_link) {
+ $min_depth = $first_link['depth'];
+ }
+ }
+ $tree = $this->doBuildTreeData($links, $active_trail, $min_depth);
+ return $tree;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadSubtree($id, $max_relative_depth = NULL) {
+ $tree = array();
+ $root = $this->loadFull($id);
+ if (!$root) {
+ return $tree;
+ }
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table);
+ for ($i = 1; $i <= $this->maxDepth(); $i++) {
+ $query->orderBy('p' . $i, 'ASC');
+ }
+ $query->condition('hidden', 0);
+ $query->condition('menu_name', $root['menu_name']);
+ for ($i = 1; $i <= $root['depth']; $i++) {
+ $query->condition("p$i", $root["p$i"]);
+ }
+ if (!empty($max_relative_depth)) {
+ $query->condition('depth', (int) $root['depth'] + $max_relative_depth, '<=');
+ }
+ $links = $this->safeExecuteSelect($query)->fetchAll(\PDO::FETCH_ASSOC);
+ $tree = $this->doBuildTreeData($links, array(), $root['depth']);
+ $subtree = current($tree);
+ return $subtree;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function menuNameInUse($menu_name) {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->addField($this->table, 'mlid');
+ $query->condition('menu_name', $menu_name);
+ $query->range(0, 1);
+ return (bool) $this->safeExecuteSelect($query);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMenuNames() {
+ $query = $this->connection->select($this->table, $this->options);
+ $query->addField($this->table, 'menu_name');
+ $query->distinct();
+ return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function countMenuLinks($menu_name = NULL) {
+ $query = $this->connection->select($this->table, $this->options);
+ if ($menu_name) {
+ $query->condition('menu_name', $menu_name);
+ }
+ return $this->safeExecuteSelect($query->countQuery())->fetchField();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAllChildIds($id) {
+ $root = $this->loadFull($id);
+ if (!$root) {
+ return array();
+ }
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table, array('id'));
+ $query->condition('menu_name', $root['menu_name']);
+ for ($i = 1; $i <= $root['depth']; $i++) {
+ $query->condition("p$i", $root["p$i"]);
+ }
+ // The next p column should not be empty. This excludes the root link.
+ $query->condition("p$i", 0, '>');
+ return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadAllChildLinks($id, $max_relative_depth = NULL) {
+ $tree = array();
+ $root = $this->loadFull($id);
+ if (!$root || $root['depth'] == $this->maxDepth()) {
+ return $tree;
+ }
+ $query = $this->connection->select($this->table, $this->options);
+ $query->fields($this->table, $this->definitionFields());
+ $query->condition('hidden', 0);
+ $query->condition('menu_name', $root['menu_name']);
+ for ($i = 1; $i <= $root['depth']; $i++) {
+ $query->condition("p$i", $root["p$i"]);
+ }
+ // The next p column should not be empty. This excludes the root link.
+ $query->condition("p$i", 0, '>');
+ if (!empty($max_relative_depth)) {
+ $query->condition('depth', (int) $root['depth'] + $max_relative_depth, '<=');
+ }
+ $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+ foreach ($loaded as &$link) {
+ foreach ($this->serializedFields() as $name) {
+ $link[$name] = unserialize($link[$name]);
+ }
+ }
+ return $loaded;
+ }
+
+ /**
+ * Prepares the data for calling $this->treeDataRecursive().
+ */
+ protected function doBuildTreeData(array $links, array $parents = array(), $depth = 1) {
+ // Reverse the array so we can use the more efficient array_pop() function.
+ $links = array_reverse($links);
+ return $this->treeDataRecursive($links, $parents, $depth);
+ }
+
+ /**
+ * Builds the data representing a menu tree.
+ *
+ * The function is a bit complex because the rendering of a link depends on
+ * the next menu link.
+ *
+ * @param array $links
+ * A flat array of menu links that are part of the menu. Each array element
+ * is an associative array of information about the menu link, containing
+ * the fields from the {menu_links} table, and optionally additional
+ * information from the {menu_router} table, if the menu item appears in
+ * both tables. This array must be ordered depth-first.
+ * See _menu_build_tree() for a sample query.
+ * @param array $parents
+ * An array of the menu link ID values that are in the path from the current
+ * page to the root of the menu tree.
+ * @param int $depth
+ * The minimum depth to include in the returned menu tree.
+ *
+ * @return array
+ * The fully build tree.
+ */
+ protected function treeDataRecursive(&$links, $parents, $depth) {
+ $tree = array();
+ while ($item = array_pop($links)) {
+ // We need to determine if we're on the path to root so we can later build
+ // the correct active trail.
+ foreach ($this->serializedFields() as $name) {
+ $item[$name] = unserialize($item[$name]);
+ }
+ // Add the current link to the tree.
+ $tree[$item['id']] = array(
+ 'definition' => array_intersect_key($item, array_flip($this->definitionFields())),
+ 'has_children' => $item['has_children'],
+ 'in_active_trail' => in_array($item['id'], $parents),
+ 'below' => array(),
+ 'depth' => $item['depth'],
+ );
+ for ($i = 1; $i <= $this->maxDepth(); $i++) {
+ $tree[$item['id']]['p' . $i] = $item['p' . $i];
+ }
+ // Look ahead to the next link, but leave it on the array so it's
+ // available to other recursive function calls if we return or build a
+ // sub-tree.
+ $next = end($links);
+ // Check whether the next link is the first in a new sub-tree.
+ if ($next && $next['depth'] > $depth) {
+ // Recursively call doBuildTreeData to build the sub-tree.
+ $tree[$item['id']]['below'] = $this->treeDataRecursive($links, $parents, $next['depth']);
+ // Fetch next link after filling the sub-tree.
+ $next = end($links);
+ }
+ // Determine if we should exit the loop and return.
+ if (!$next || $next['depth'] < $depth) {
+ break;
+ }
+ }
+ return $tree;
+ }
+
+ /**
+ * Checks if the tree table exists and create it if not.
+ *
+ * @return bool
+ * TRUE if the table was created, FALSE otherwise.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ * If a database error occurs.
+ */
+ protected function ensureTableExists() {
+ try {
+ if (!$this->connection->schema()->tableExists($this->table)) {
+ $this->connection->schema()->createTable($this->table, static::schemaDefinition());
+ return TRUE;
+ }
+ }
+ catch (SchemaObjectExistsException $e) {
+ // If another process has already created the config table, attempting to
+ // recreate it will throw an exception. In this case just catch the
+ // exception and do nothing.
+ return TRUE;
+ }
+ catch (\Exception $e) {
+ throw new PluginException($e->getMessage(), NULL, $e);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Helper function to determine serialized fields.
+ */
+ protected function serializedFields() {
+ // For now, build the list from the schema since it's in active development.
+ if (empty($this->serializedFields)) {
+ $schema = static::schemaDefinition();
+ foreach ($schema['fields'] as $name => $field) {
+ if (!empty($field['serialize'])) {
+ $this->serializedFields[] = $name;
+ }
+ }
+ }
+ return $this->serializedFields;
+ }
+
+ /**
+ * Helper function to determine fields that are part of the plugin definition.
+ */
+ protected function definitionFields() {
+ return $this->definitionFields;
+ }
+
+ /**
+ * Defines the schema for the tree table.
+ */
+ protected static function schemaDefinition() {
+ $schema = array(
+ 'description' => 'Contains the menu tree hierarchy.',
+ 'fields' => array(
+ 'menu_name' => array(
+ 'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.",
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'mlid' => array(
+ 'description' => 'The menu link ID (mlid) is the integer primary key.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'id' => array(
+ 'description' => 'Unique machine name: the plugin ID.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'parent' => array(
+ 'description' => 'The plugin ID for the parent of this link.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'route_name' => array(
+ 'description' => 'The machine name of a defined Symfony Route this menu item represents.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ ),
+ 'route_param_key' => array(
+ 'description' => 'An encoded string of route parameters for loading by route.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ ),
+ 'route_parameters' => array(
+ 'description' => 'Serialized array of route parameters of this menu link.',
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'not null' => FALSE,
+ 'serialize' => TRUE,
+ ),
+ 'url' => array(
+ 'description' => 'The external path this link points to (when not using a route).',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'title' => array(
+ 'description' => 'The text displayed for the link.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'title_arguments' => array(
+ 'description' => 'A serialized array of arguments to be passed to t() (if this plugin uses it).',
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'not null' => FALSE,
+ 'serialize' => TRUE,
+ ),
+ 'title_context' => array(
+ 'description' => 'The translation context for the link title.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'description' => array(
+ 'description' => 'The description of this link - used for admin pages and title attribute.',
+ 'type' => 'text',
+ 'not null' => FALSE,
+ ),
+ 'class' => array(
+ 'description' => 'The class for this link plugin.',
+ 'type' => 'text',
+ 'not null' => FALSE,
+ ),
+ 'options' => array(
+ 'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.',
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'not null' => FALSE,
+ 'serialize' => TRUE,
+ ),
+ 'provider' => array(
+ 'description' => 'The name of the module that generated this link.',
+ 'type' => 'varchar',
+ 'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
+ 'not null' => TRUE,
+ 'default' => 'system',
+ ),
+ 'hidden' => array(
+ 'description' => 'A flag for whether the link should be rendered in menus. (1 = a disabled menu item that may be shown on admin screens, 0 = a normal, visible link)',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ 'discovered' => array(
+ 'description' => 'A flag for whether the link was discovered, so can be purged on rebuild',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ 'expanded' => array(
+ 'description' => '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)',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ 'weight' => array(
+ 'description' => 'Link weight among links in the same menu at the same depth.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'metadata' => array(
+ 'description' => 'A serialized array of data that may be used by the plugin instance.',
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'not null' => FALSE,
+ 'serialize' => TRUE,
+ ),
+ 'has_children' => array(
+ 'description' => 'Flag indicating whether any non-hidden links have this link as a parent (1 = children exist, 0 = no children).',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ 'depth' => array(
+ 'description' => 'The depth relative to the top level. A link with empty parent will have depth == 1.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ 'p1' => array(
+ 'description' => '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 parent link mlid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p2' => array(
+ 'description' => 'The second mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p3' => array(
+ 'description' => 'The third mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p4' => array(
+ 'description' => 'The fourth mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p5' => array(
+ 'description' => 'The fifth mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p6' => array(
+ 'description' => 'The sixth mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p7' => array(
+ 'description' => 'The seventh mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p8' => array(
+ 'description' => 'The eighth mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'p9' => array(
+ 'description' => 'The ninth mlid in the materialized path. See p1.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'form_class' => array(
+ 'description' => 'meh',
+ 'type' => 'varchar',
+ 'length' => 255,
+ ),
+ ),
+ 'indexes' => array(
+ 'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'),
+ // @todo - test this index for effectiveness.
+ 'menu_parent_expand_child' => array('menu_name', 'expanded', 'has_children', array('parent', 16)),
+ 'route_values' => array(array('route_name', 32), array('route_param_key', 16)),
+ ),
+ 'primary key' => array('mlid'),
+ 'unique keys' => array(
+ 'id' => array('id'),
+ ),
+ );
+
+ return $schema;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
new file mode 100644
index 0000000..d90a767
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
@@ -0,0 +1,253 @@
+configFactory = $config_factory;
+ }
+
+ /**
+ * Helper function to get the config object.
+ *
+ * Since this service is injected into all static menu link objects, but
+ * only used when updating one, avoid actually loading the config when it's
+ * not needed.
+ */
+ protected function getConfig() {
+ if (empty($this->config)) {
+ $this->config = $this->configFactory->get($this->configName);
+ }
+ return $this->config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reload() {
+ $this->config = NULL;
+ $this->configFactory->reset($this->configName);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadOverride($id) {
+ $all_overrides = $this->getConfig()->get('definitions');
+ $id = static::encodeId($id);
+ return $id && isset($all_overrides[$id]) ? $all_overrides[$id] : array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteMultipleOverrides(array $ids) {
+ $all_overrides = $this->getConfig()->get('definitions');
+ $save = FALSE;
+ foreach ($ids as $id) {
+ $id = static::encodeId($id);
+ if (isset($all_overrides[$id])) {
+ unset($all_overrides[$id]);
+ $save = TRUE;
+ }
+ }
+ if ($save) {
+ $this->getConfig()->set('definitions', $all_overrides)->save();
+ }
+ return $save;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteOverride($id) {
+ return $this->deleteMultipleOverrides(array($id));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadMultipleOverrides(array $ids) {
+ $result = array();
+ if ($ids) {
+ $all_overrides = $this->getConfig()->get('definitions') ?: array();
+ foreach ($ids as $id) {
+ $encoded_id = static::encodeId($id);
+ if (isset($all_overrides[$encoded_id])) {
+ $result[$id] = $all_overrides[$encoded_id];
+ }
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveOverride($id, array $definition) {
+ // Remove unexpected keys.
+ $expected = array(
+ 'menu_name' => 1,
+ 'parent' => 1,
+ 'weight' => 1,
+ 'expanded' => 1,
+ 'hidden' => 1,
+ );
+ $definition = array_intersect_key($definition, $expected);
+ if ($definition) {
+ $id = static::encodeId($id);
+ $all_overrides = $this->getConfig()->get('definitions');
+ // Combine with any existing data.
+ $all_overrides[$id] = $definition + $this->loadOverride($id);
+ $this->getConfig()->set('definitions', $all_overrides)->save();
+ }
+ return array_keys($definition);
+ }
+
+ /**
+ * Encodes the ID by replacing dots with double underscores.
+ *
+ * This is done because config schema uses dots for its internal type
+ * hierarchy.
+ *
+ * @param string $id
+ * The menu plugin ID.
+ *
+ * @return string
+ * The menu plugin ID with double underscore instead of dots.
+ */
+ protected static function encodeId($id) {
+ return str_replace('.', '__', $id);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php
new file mode 100644
index 0000000..5f0549b
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php
@@ -0,0 +1,83 @@
+menuTree = $menu_tree;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function convert($value, $definition, $name, array $defaults, Request $request) {
+ if ($value) {
+ try {
+ return $this->menuTree->createInstance($value);
+ }
+ catch (PluginException $e) {
+ // Suppress the error.
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applies($definition, $name, Route $route) {
+ return (!empty($definition['type']) && $definition['type'] === 'menu_link_plugin');
+ }
+
+}
diff --git a/core/modules/menu_link_content/menu_link_content.info.yml b/core/modules/menu_link_content/menu_link_content.info.yml
new file mode 100644
index 0000000..4c69873
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.info.yml
@@ -0,0 +1,6 @@
+name: 'Menu Links Content'
+type: module
+description: 'Allows administrators to create custom links'
+package: Core
+version: VERSION
+core: 8.x
diff --git a/core/modules/menu_link_content/menu_link_content.install b/core/modules/menu_link_content/menu_link_content.install
new file mode 100644
index 0000000..606ca6a
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.install
@@ -0,0 +1,19 @@
+fetchCol();
+ $menu_tree = \Drupal::menuTree();
+ foreach ($uuids as $uuid) {
+ // Manually build the plugin ID, and remove it from the menu tree.
+ $menu_tree->deleteLink("menu_link_content:$uuid", FALSE);
+ }
+}
diff --git a/core/modules/menu_link_content/menu_link_content.local_tasks.yml b/core/modules/menu_link_content/menu_link_content.local_tasks.yml
new file mode 100644
index 0000000..8195236
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.local_tasks.yml
@@ -0,0 +1,4 @@
+menu_link_content.link_edit:
+ route_name: menu_link_content.link_edit
+ base_route: menu_link_content.link_edit
+ title: Edit
diff --git a/core/modules/menu_link_content/menu_link_content.module b/core/modules/menu_link_content/menu_link_content.module
new file mode 100644
index 0000000..6648280
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.module
@@ -0,0 +1,17 @@
+getStorage('menu_link_content');
+ $menu_links = $storage->loadByProperties(array('menu_name' => $menu->id()));
+ $storage->delete($menu_links);
+}
diff --git a/core/modules/menu_link_content/menu_link_content.routing.yml b/core/modules/menu_link_content/menu_link_content.routing.yml
new file mode 100644
index 0000000..f96b22d
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.routing.yml
@@ -0,0 +1,23 @@
+menu_link_content.link_add:
+ path: '/admin/structure/menu/manage/{menu}/add'
+ defaults:
+ _content: '\Drupal\menu_link_content\Controller\MenuController::addLink'
+ _title: 'Add menu link'
+ requirements:
+ _entity_create_access: 'menu_link_content'
+
+menu_link_content.link_edit:
+ 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'
+
+menu_link_content.link_delete:
+ 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'
diff --git a/core/modules/menu_link_content/src/Controller/MenuController.php b/core/modules/menu_link_content/src/Controller/MenuController.php
new file mode 100644
index 0000000..7ffa9f8
--- /dev/null
+++ b/core/modules/menu_link_content/src/Controller/MenuController.php
@@ -0,0 +1,34 @@
+entityManager()->getStorage('menu_link_content')->create(array(
+ 'id' => '',
+ 'parent' => '',
+ 'menu_name' => $menu->id(),
+ 'bundle' => 'menu_link_content',
+ ));
+ return $this->entityFormBuilder()->getForm($menu_link);
+ }
+
+}
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
new file mode 100644
index 0000000..c98ef42
--- /dev/null
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -0,0 +1,382 @@
+insidePlugin = TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTitle() {
+ return $this->get('title')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRouteName() {
+ return $this->get('route_name')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRouteParameters() {
+ return $this->get('route_parameters')->first()->getValue();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRouteParameters(array $route_parameters) {
+ $this->set('route_parameters', array($route_parameters));
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUrl() {
+ return $this->get('url')->value ?: NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUrlObject() {
+ if ($route_name = $this->getRouteName()) {
+ $url = new Url($route_name, $this->getRouteParameters(), $this->getOptions());
+ }
+ else {
+ $path = $this->getUrl();
+ if (isset($path)) {
+ $url = Url::createFromPath($path);
+ }
+ else {
+ $url = new Url('');
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMenuName() {
+ return $this->get('menu_name')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOptions() {
+ return $this->get('options')->first()->getValue();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOptions(array $options) {
+ $this->set('options', array($options));
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->get('description')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPluginId() {
+ return 'menu_link_content:' . $this->uuid();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return (bool) $this->get('hidden')->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;
+ }
+
+ /**
+ * 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
+ */
+ protected function getMenuDefinition() {
+ $definition = array();
+ $definition['class'] = 'Drupal\menu_link_content\Plugin\Menu\MenuLinkContent';
+ $definition['menu_name'] = $this->getMenuName();
+ $definition['route_name'] = $this->getRouteName();
+ $definition['route_parameters'] = $this->getRouteParameters();
+ $definition['url'] = $this->getUrl();
+ $definition['options'] = $this->getOptions();
+ // Don't bother saving title and description strings, since they are never
+ // used.
+ $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['hidden'] = $this->isHidden() ? 1 : 0;
+ $definition['expanded'] = $this->isExpanded() ? 1 : 0;
+ $definition['provider'] = 'menu_link_content';
+ $definition['discovered'] = 0;
+ $definition['parent'] = $this->getParentId();
+
+ return $definition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+ parent::postSave($storage, $update);
+
+ /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */
+ $menu_tree = \Drupal::menuTree();
+
+ // The menu link can just be updated if there is already an menu link entry
+ // on both entity and menu tree level.
+ if ($update && $menu_tree->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_tree->updateLink($this->getPluginId(), $this->getMenuDefinition(), FALSE);
+ }
+ }
+ else {
+ $menu_tree->createLink($this->getPluginId(), $this->getMenuDefinition());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function preDelete(EntityStorageInterface $storage, array $entities) {
+ parent::preDelete($storage, $entities);
+
+ foreach ($entities as $menu_link) {
+ /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */
+ \Drupal::menuTree()->deleteLink($menu_link->getPluginId(), FALSE);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+ $fields['id'] = FieldDefinition::create('integer')
+ ->setLabel(t('Content menu link ID'))
+ ->setDescription(t('The menu link ID.'))
+ ->setReadOnly(TRUE)
+ ->setSetting('unsigned', TRUE);
+
+ $fields['uuid'] = FieldDefinition::create('uuid')
+ ->setLabel(t('UUID'))
+ ->setDescription(t('The content menu link UUID.'))
+ ->setReadOnly(TRUE);
+
+ $fields['bundle'] = FieldDefinition::create('string')
+ ->setLabel(t('Bundle'))
+ ->setDescription(t('The content menu link bundle.'))
+ ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH)
+ ->setReadOnly(TRUE);
+
+ $fields['title'] = FieldDefinition::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(
+ 'default_value' => '',
+ 'max_length' => 255,
+ ))
+ ->setDisplayOptions('view', array(
+ 'label' => 'hidden',
+ 'type' => 'string',
+ 'weight' => -5,
+ ))
+ ->setDisplayOptions('form', array(
+ 'type' => 'string',
+ 'weight' => -5,
+ ))
+ ->setDisplayConfigurable('form', TRUE);
+
+ $fields['description'] = FieldDefinition::create('string')
+ ->setLabel(t('Description'))
+ ->setDescription(t('Shown when hovering over the menu link.'))
+ ->setTranslatable(TRUE)
+ ->setSettings(array(
+ 'default_value' => '',
+ 'max_length' => 255,
+ ))
+ ->setDisplayOptions('view', array(
+ 'label' => 'hidden',
+ 'type' => 'string',
+ 'weight' => 0,
+ ))
+ ->setDisplayOptions('form', array(
+ 'type' => 'string',
+ 'weight' => 0,
+ ));
+
+ $fields['menu_name'] = FieldDefinition::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.'))
+ ->setSetting('default_value', 'tools');
+
+ // @todo use a link field in the end? see https://drupal.org/node/2235457
+ $fields['route_name'] = FieldDefinition::create('string')
+ ->setLabel(t('Route name'))
+ ->setDescription(t('The machine name of a defined Symfony Route this menu item represents.'));
+
+ $fields['route_parameters'] = FieldDefinition::create('map')
+ ->setLabel(t('Route parameters'))
+ ->setDescription(t('A serialized array of route parameters of this menu link.'));
+
+ $fields['url'] = FieldDefinition::create('string')
+ ->setLabel(t('External link url'))
+ ->setDescription(t('The url of the link, in case you have an external link.'));
+
+ $fields['options'] = FieldDefinition::create('map')
+ ->setLabel(t('Options'))
+ ->setDescription(t('A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.'))
+ ->setSetting('default_value', array());
+
+ $fields['external'] = FieldDefinition::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).'))
+ ->setSetting('default_value', 0);
+
+ $fields['expanded'] = FieldDefinition::create('boolean')
+ ->setLabel(t('Expanded'))
+ ->setDescription(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).'))
+ ->setSetting('default_value', 0)
+ ->setDisplayOptions('view', array(
+ 'label' => 'hidden',
+ 'type' => 'boolean',
+ 'weight' => 0,
+ ))
+ ->setDisplayOptions('form', array(
+ 'type' => 'options_onoff',
+ 'weight' => 0,
+ ));
+
+ $fields['hidden'] = FieldDefinition::create('boolean')
+ ->setLabel(t('Hidden'))
+ ->setDescription(t('A flag for whether the link should be rendered in menus. (1 = a disabled menu item that may be shown on admin screens, -1 = a menu callback, 0 = a normal, visible link).'))
+ ->setSetting('default_value', 0);
+
+ $fields['weight'] = FieldDefinition::create('integer')
+ ->setLabel(t('Weight'))
+ ->setDescription(t('Link weight among links in the same menu at the same depth.'))
+ ->setSetting('default_value', 0)
+ ->setDisplayOptions('view', array(
+ 'label' => 'hidden',
+ 'type' => 'integer',
+ 'weight' => 0,
+ ))
+ ->setDisplayOptions('form', array(
+ 'type' => 'integer',
+ 'weight' => 0,
+ ));
+
+ $fields['langcode'] = FieldDefinition::create('language')
+ ->setLabel(t('Language code'))
+ ->setDescription(t('The node language code.'));
+
+ $fields['parent'] = FieldDefinition::create('string')
+ ->setLabel(t('Parent menu link ID'))
+ ->setDescription(t('The parent menu link ID.'));
+
+ return $fields;
+ }
+
+}
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php
new file mode 100644
index 0000000..35a24a7
--- /dev/null
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php
@@ -0,0 +1,147 @@
+t('Are you sure you want to delete the custom menu link %item?', array('%item' => $this->entity->getTitle()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelRoute() {
+ return new Url('menu_ui.menu_edit', array('menu' => $this->entity->getMenuName()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submit(array $form, array &$form_state) {
+ $storage = $this->entityManager->getStorage('menu_link_content');
+ $storage->delete(array($this->entity));
+ $t_args = array('%title' => $this->entity->getTitle());
+ drupal_set_message($this->t('The menu link %title has been deleted.', $t_args));
+ watchdog('menu', 'Deleted menu link %title.', $t_args, WATCHDOG_NOTICE);
+ $form_state['redirect_route'] = array(
+ 'route_name' => '',
+ );
+ }
+
+}
diff --git a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
new file mode 100644
index 0000000..272ac64
--- /dev/null
+++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
@@ -0,0 +1,400 @@
+menuTree = $menu_tree;
+ $this->pathAliasManager = $alias_manager;
+ $this->moduleHandler = $module_handler;
+ $this->requestContext = $request_context;
+ $this->languageManager = $language_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager'),
+ $container->get('menu.link_tree'),
+ $container->get('path.alias_manager'),
+ $container->get('module_handler'),
+ $container->get('router.request_context'),
+ $container->get('language_manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMenuLinkInstance(MenuLinkInterface $menu_link) {
+ // Load the entity for the entity form.
+ $metadata = $menu_link->getMetaData();
+ if (!empty($metadata['entity_id'])) {
+ $this->entity = $this->entityManager->getStorage('menu_link_content')->load($metadata['entity_id']);
+ }
+ else {
+ // Fallback to the loading by the uuid.
+ $links = $this->entityManager->getStorage('menu_link_content')->loadByProperties(array('uuid' => $menu_link->getDerivativeId()));
+ $this->entity = reset($links);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildEditForm(array &$form, array &$form_state) {
+ $this->setOperation('default');
+ $this->init($form_state);
+
+ return $this->form($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateEditForm(array &$form, array &$form_state) {
+ $this->doValidate($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitEditForm(array &$form, array &$form_state) {
+ // Remove button and internal Form API values from submitted values.
+ form_state_values_clean($form_state);
+ $this->entity = $this->buildEntity($form, $form_state);
+ $this->entity->save();
+ return $this->menuTree->createInstance($this->entity->getPluginId());
+ }
+
+ /**
+ * Break up a user-entered URL or path into all the relevant parts.
+ *
+ * @param string $url
+ * The user-entered URL or path.
+ *
+ * @return array
+ * The extracted parts.
+ */
+ protected function extractUrl($url) {
+ $extracted = UrlHelper::parse($url);
+ $external = UrlHelper::isExternal($url);
+ if ($external) {
+ $extracted['url'] = $extracted['path'];
+ $extracted['route_name'] = NULL;
+ $extracted['route_parameters'] = array();
+ }
+ else {
+ $extracted['url'] = '';
+ // If the path doesn't match a Drupal path, the route should end up empty.
+ $extracted['route_name'] = NULL;
+ $extracted['route_parameters'] = array();
+ try {
+ // Find the route_name.
+ $normal_path = $this->pathAliasManager->getPathByAlias($extracted['path']);
+ $url_obj = Url::createFromPath($normal_path);
+ $extracted['route_name'] = $url_obj->getRouteName();
+ $extracted['route_parameters'] = $url_obj->getRouteParameters();
+ }
+ catch (MatchingRouteNotFoundException $e) {
+ // The path doesn't match a Drupal path.
+ }
+ catch (ParamNotConvertedException $e) {
+ // A path like node/99 matched a route, but the route parameter was
+ // invalid (e.g. node with ID 99 does not exist).
+ }
+ }
+ return $extracted;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractFormValues(array &$form, array &$form_state) {
+
+ $new_definition = array();
+ $new_definition['expanded'] = !empty($form_state['values']['expanded']) ? 1 : 0;
+ $new_definition['hidden'] = empty($form_state['values']['enabled']) ? 1 : 0;
+ list($menu_name, $parent) = explode(':', $form_state['values']['menu_parent'], 2);
+ if (!empty($menu_name)) {
+ $new_definition['menu_name'] = $menu_name;
+ }
+ $new_definition['parent'] = isset($parent) ? $parent : '';
+
+ $extracted = $this->extractUrl($form_state['values']['url']);
+ $new_definition['url'] = $extracted['url'];
+ $new_definition['route_name'] = $extracted['route_name'];
+ $new_definition['route_parameters'] = $extracted['route_parameters'];
+ $new_definition['options'] = array();
+ if ($extracted['query']) {
+ $new_definition['options']['query'] = $extracted['query'];
+ }
+ if ($extracted['fragment']) {
+ $new_definition['options']['fragment'] = $extracted['fragment'];
+ }
+ $new_definition['title'] = $form_state['values']['title'][0]['value'];
+ $new_definition['description'] = $form_state['values']['description'][0]['value'];
+ $new_definition['weight'] = (int) $form_state['values']['weight'][0]['value'];
+
+ return $new_definition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, array &$form_state) {
+
+ $form = parent::form($form, $form_state);
+
+ $language_configuration = $this->moduleHandler->invoke('language', 'get_default_configuration', array('menu_link_content', 'menu_link_content'));
+ if ($this->entity->isNew()) {
+ $default_language = isset($language_configuration['langcode']) ? $language_configuration['langcode'] : $this->languageManager->getDefaultLanguage()->id;
+ }
+ else {
+ $default_language = $this->entity->getUntranslated()->language()->id;
+ }
+ $form['langcode'] = array(
+ '#title' => t('Language'),
+ '#type' => 'language_select',
+ '#default_value' => $default_language,
+ '#languages' => Language::STATE_ALL,
+ '#access' => !empty($language_configuration['language_show']),
+ );
+
+ $form['enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enable menu link'),
+ '#description' => $this->t('Menu links that are not enabled will not be listed in any menu.'),
+ '#default_value' => !$this->entity->isHidden(),
+ );
+
+ // @TODO For whatever reason the expanded widget is not autogenerated.
+ $form['expanded'] = array(
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show as expanded'),
+ '#description' => $this->t('If selected and this menu link has children, the menu will always appear expanded.'),
+ '#default_value' => $this->entity->isExpanded(),
+ );
+
+ // We always show the internal path here.
+ $url = $this->getEntity()->getUrlObject();
+ if ($url->isExternal()) {
+ $default_value = $url->toString();
+ }
+ elseif ($url->getRouteName() == '') {
+ // The default route for new entities is , but we just want an
+ // empty form field.
+ $default_value = $this->getEntity()->isNew() ? '' : '';
+ }
+ else {
+ // @TODO Maybe support options in
+ // \Drupal\Core\Routing\UrlGeneratorInterface::getInternalPath().
+ // or a helper method to render just options?
+ $default_value = $url->getInternalPath();
+ $options = $url->getOptions();
+ if (isset($options['query'])) {
+ $default_value .= $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : '';
+ }
+ if (isset($options['fragment']) && $options['fragment'] !== '') {
+ $default_value .= '#' . $options['fragment'];
+ }
+ }
+ $form['url'] = array(
+ '#title' => $this->t('Link path'),
+ '#type' => 'textfield',
+ '#description' => $this->t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
+ '#default_value' => $default_value,
+ '#required' => TRUE,
+ );
+
+ $options = $this->menuTree->getParentSelectOptions($this->entity->getPluginId());
+ $menu_parent = $this->entity->getMenuName() . ':' . $this->entity->getParentId();
+
+ if (!isset($options[$menu_parent])) {
+ // Put it at the top level in the current menu.
+ $menu_parent = $this->entity->getMenuName() . ':';
+ }
+ $form['menu_parent'] = array(
+ '#type' => 'select',
+ '#title' => $this->t('Parent link'),
+ '#options' => $options,
+ '#default_value' => $menu_parent,
+ '#description' => $this->t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => $this->menuTree->maxDepth())),
+ '#attributes' => array('class' => array('menu-title-select')),
+ );
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, array &$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 validate(array $form, array &$form_state) {
+ $this->doValidate($form, $form_state);
+
+ parent::validate($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildEntity(array $form, array &$form_state) {
+ /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $entity */
+ $entity = parent::buildEntity($form, $form_state);
+ $new_definition = $this->extractFormValues($form, $form_state);
+
+ $entity->parent->value = $new_definition['parent'];
+ $entity->menu_name->value = $new_definition['menu_name'];
+ $entity->hidden->value = (bool) $new_definition['hidden'];
+ $entity->expanded->value = $new_definition['expanded'];
+
+ $entity->url->value = $new_definition['url'];
+ $entity->route_name->value = $new_definition['route_name'];
+ $entity->setRouteParameters($new_definition['route_parameters']);
+ $entity->setOptions($new_definition['options']);
+
+ return $entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, array &$form_state) {
+ // The entity is rebuilt in parent::submit().
+ $menu_link = $this->entity;
+ $saved = $menu_link->save();
+
+ if ($saved) {
+ drupal_set_message(t('The menu link has been saved.'));
+ $form_state['redirect_route'] = array(
+ 'route_name' => 'menu_link_content.link_edit',
+ 'route_parameters' => array(
+ 'menu_link_content' => $menu_link->id(),
+ ),
+ );
+ }
+ else {
+ drupal_set_message(t('There was an error saving the menu link.'), 'error');
+ $form_state['rebuild'] = TRUE;
+ }
+ }
+
+ /**
+ * Validates the form, both on the menu link edit and content menu link form.
+ */
+ protected function doValidate(array $form, array &$form_state) {
+ $extracted = $this->extractUrl($form_state['values']['url']);
+
+ // If both url and route_nae are empty, the entered value is not valid.
+ $valid = FALSE;
+ if ($extracted['url']) {
+ // This is an external link.
+ $valid = TRUE;
+ }
+ elseif ($extracted['route_name']) {
+ // Users are not allowed to add a link to a page they cannot access.
+ $valid = \Drupal::service('access_manager')->checkNamedRoute($extracted['route_name'], $extracted['route_parameters'], \Drupal::currentUser());
+ }
+ if (!$valid) {
+ $this->setFormError('url', $form_state, $this->t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $form_state['values']['url'])));
+ }
+ elseif ($extracted['route_name']) {
+ // The user entered a Drupal path.
+ $normal_path = $this->pathAliasManager->getPathByAlias($extracted['path']);
+ if ($extracted['path'] != $normal_path) {
+ drupal_set_message($this->t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array(
+ '%link_path' => $extracted['path'],
+ '%normal_path' => $normal_path,
+ )));
+ }
+ }
+ }
+
+}
diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessController.php b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php
new file mode 100644
index 0000000..d895478
--- /dev/null
+++ b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php
@@ -0,0 +1,55 @@
+hasPermission('administer menu') && ($entity->getUrl() || $this->accessManager()->checkNamedRoute($entity->getRouteName(), $entity->getRouteParameters(), $account));
+
+ case 'delete':
+ return !$entity->isNew() && $account->hasPermission('administer menu');
+ }
+ }
+
+ /**
+ * Returns the access manager.
+ *
+ * @return \Drupal\Core\Access\AccessManager
+ * The route provider.
+ */
+ protected function accessManager() {
+ if (!$this->accessManager) {
+ $this->accessManager = \Drupal::service('access_manager');
+ }
+ return $this->accessManager;
+ }
+}
diff --git a/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
new file mode 100644
index 0000000..15c5370
--- /dev/null
+++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
@@ -0,0 +1,236 @@
+ 1,
+ 'parent' => 1,
+ 'weight' => 1,
+ 'expanded' => 1,
+ 'hidden' => 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\Entity\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'];
+ static::$entityIdsToLoad[$entity_id] = $entity_id;
+ }
+
+ $this->entityManager = $entity_manager;
+ $this->langaugeManager = $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\Entity\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'];
+ 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();
+ }
+ else {
+ // Fallback to the loading by the uuid.
+ $uuid = $this->getDerivativeId();
+ $links = $storage->loadByProperties(array('uuid' => $uuid));
+ $entity = reset($links);
+ }
+ if (!$entity) {
+ throw new PluginException("Invalid entity ID or uuid");
+ }
+ // Clone the entity object to avoid tampering with the static cache.
+ $this->entity = clone $entity;
+ $this->entity = $this->entityManager->getTranslationFromContext($this->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 configured to be multilingual.
+ if ($this->langaugeManager->isMultilingual()) {
+ return $this->getEntity()->getTitle();
+ }
+ return $this->pluginDefinition['title'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ if ($this->langaugeManager->isMultilingual()) {
+ return $this->getEntity()->getDescription();
+ }
+ return $this->pluginDefinition['description'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeleteRoute() {
+ return array(
+ 'route_name' => 'menu_link_content.link_delete',
+ 'route_parameters' => array('menu_link_content' => $this->getEntity()->id()),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEditRoute() {
+ return array(
+ 'route_name' => 'menu_link_content.link_edit',
+ 'route_parameters' => array('menu_link_content' => $this->getEntity()->id()),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTranslateRoute() {
+ $entity_type = $this->getEntity()->getEntityType()->id();
+ return array(
+ 'route_name' => 'content_translation.translation_overview_' . $entity_type,
+ 'route_parameters' => array(
+ $entity_type => $this->getEntity()->id(),
+ ),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function updateLink(array $new_definition_values, $persist) {
+ $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() {
+ // @todo: Flag this call if possible so we don't call the menu tree manager.
+ $this->getEntity()->delete();
+ }
+
+}
diff --git a/core/modules/menu_link_content/src/Tests/MenuLinkContentUITest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentUITest.php
new file mode 100644
index 0000000..6d2bed0
--- /dev/null
+++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentUITest.php
@@ -0,0 +1,63 @@
+ 'Menu link content translation UI',
+ 'description' => 'Tests the basic menu link content translation UI.',
+ 'group' => 'Menu',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ $this->entityTypeId = 'menu_link_content';
+ $this->bundle = 'menu_link_content';
+ $this->fieldName = 'title';
+ parent::setUp();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getTranslatorPermissions() {
+ return array_merge(parent::getTranslatorPermissions(), array('administer menu'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function createEntity($values, $langcode, $bundle_name = NULL) {
+ $values['menu_name'] = 'tools';
+ $values['route_name'] = 'menu_ui.overview_page';
+ $values['title'] = 'Test title';
+
+ return parent::createEntity($values, $langcode, $bundle_name);
+ }
+
+}
diff --git a/core/modules/menu_ui/src/Form/MenuLinkEditForm.php b/core/modules/menu_ui/src/Form/MenuLinkEditForm.php
new file mode 100644
index 0000000..7ac65ac
--- /dev/null
+++ b/core/modules/menu_ui/src/Form/MenuLinkEditForm.php
@@ -0,0 +1,93 @@
+menuTree = $menu_tree;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('menu.link_tree')
+ );
+ }
+
+ public function getFormId() {
+ return 'menu_link_edit';
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin
+ * The plugin instance to use for this form.
+ */
+ public function buildForm(array $form, array &$form_state, MenuLinkInterface $menu_link_plugin = NULL) {
+
+ $form['menu_link_id'] = array(
+ '#type' => 'value',
+ '#value' => $menu_link_plugin->getPluginId(),
+ );
+
+ $form['#plugin_form'] = $this->menuTree->getPluginForm($menu_link_plugin);
+
+ $form += $form['#plugin_form']->buildEditForm($form, $form_state);
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ '#button_type' => 'primary',
+ );
+ return $form;
+ }
+
+ public function validateForm(array &$form, array &$form_state) {
+ $form['#plugin_form']->validateEditForm($form, $form_state);
+ }
+
+ public function submitForm(array &$form, array &$form_state) {
+ $link = $form['#plugin_form']->submitEditForm($form, $form_state);
+
+ drupal_set_message($this->t('The menu link has been saved.'));
+ $form_state['redirect_route'] = array(
+ 'route_name' => 'menu_ui.menu_edit',
+ 'route_parameters' => array(
+ 'menu' => $link->getMenuName(),
+ ),
+ );
+ }
+
+}
+
diff --git a/core/modules/menu_ui/src/Plugin/Menu/LocalAction/MenuLinkAdd.php b/core/modules/menu_ui/src/Plugin/Menu/LocalAction/MenuLinkAdd.php
new file mode 100644
index 0000000..88e1898
--- /dev/null
+++ b/core/modules/menu_ui/src/Plugin/Menu/LocalAction/MenuLinkAdd.php
@@ -0,0 +1,32 @@
+attributes->has('_system_path')) {
+ // @todo: is there a better value to get from the request?
+ $options['query']['destination'] = $request->attributes->get('_system_path');
+ }
+ return $options;
+ }
+
+}
diff --git a/core/modules/system/config/install/menu_link.static.overrides.yml b/core/modules/system/config/install/menu_link.static.overrides.yml
new file mode 100644
index 0000000..ca4ba7f
--- /dev/null
+++ b/core/modules/system/config/install/menu_link.static.overrides.yml
@@ -0,0 +1 @@
+definitions: []
diff --git a/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php
new file mode 100644
index 0000000..4665d7a
--- /dev/null
+++ b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php
@@ -0,0 +1,109 @@
+ 'Tests \Drupal\Core\Menu\MenuLinkTree',
+ 'description' => '',
+ 'group' => 'Menu'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->installSchema('system', array('router'));
+ $this->installEntitySchema('menu_link_content');
+
+ $this->linkTree = \Drupal::menuTree();
+ }
+
+ public function testDeleteLinksInMenu() {
+ \Drupal::service('router.builder')->rebuild();
+
+ \Drupal::entityManager()->getStorage('menu')->create(array('id' => 'menu1'))->save();
+ \Drupal::entityManager()->getStorage('menu')->create(array('id' => 'menu2'))->save();
+
+ \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'))->save();
+ \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'))->save();
+ \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu2', 'bundle' => 'menu_link_content'))->save();
+
+ $output = $this->linkTree->buildTree('menu1');
+ $this->assertEqual(count($output), 2);
+ $output = $this->linkTree->buildTree('menu2');
+ $this->assertEqual(count($output), 1);
+
+ $this->linkTree->deleteLinksInMenu('menu1');
+ $this->linkTree->resetDefinitions();
+
+ $output = $this->linkTree->buildTree('menu1');
+ $this->assertEqual(count($output), 0);
+
+ $output = $this->linkTree->buildTree('menu2');
+ $this->assertEqual(count($output), 1);
+ }
+
+ public function testGetParentDepthLimit() {
+ \Drupal::service('router.builder')->rebuild();
+
+ $storage = \Drupal::entityManager()->getStorage('menu_link_content');
+
+ // root
+ // - child1
+ // -- child2
+ // --- child3
+ // ---- child4
+ $root = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'));
+ $root->save();
+ $child1 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $root->getPluginId()));
+ $child1->save();
+ $child2 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child1->getPluginId()));
+ $child2->save();
+ $child3 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child2->getPluginId()));
+ $child3->save();
+ $child4 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child3->getPluginId()));
+ $child4->save();
+
+ $this->assertEqual($this->linkTree->getParentDepthLimit($root->getPluginId()), 4);
+ $this->assertEqual($this->linkTree->getParentDepthLimit($child1->getPluginId()), 5);
+ $this->assertEqual($this->linkTree->getParentDepthLimit($child2->getPluginId()), 6);
+ $this->assertEqual($this->linkTree->getParentDepthLimit($child3->getPluginId()), 7);
+ $this->assertEqual($this->linkTree->getParentDepthLimit($child4->getPluginId()), 8);
+ }
+
+}
+
diff --git a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php
new file mode 100644
index 0000000..d336099
--- /dev/null
+++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php
@@ -0,0 +1,359 @@
+ 'Menu tree storage tests',
+ 'description' => 'Tests menu tree storage tests',
+ 'group' => 'Menu'
+ );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->treeStorage = new MenuTreeStorage($this->container->get('database'), $this->container->get('url_generator'), 'menu_tree');
+ $this->connection = $this->container->get('database');
+ }
+
+ /**
+ * Tests the tree storage when no tree was built yet.
+ */
+ public function testBasicMethods() {
+ $this->doTestEmptyStorage();
+ $this->doTestTable();
+ }
+
+ /**
+ * Ensures that there are no menu links by default.
+ */
+ protected function doTestEmptyStorage() {
+ $this->assertEqual(0, $this->treeStorage->countMenuLinks());
+ }
+
+ /**
+ * Ensures that table gets created on the fly.
+ */
+ protected function doTestTable() {
+ // Test that we can create a tree storage with an arbitrary table name and
+ // that selecting from the storage creates the table.
+ $tree_storage = new MenuTreeStorage($this->container->get('database'), $this->container->get('url_generator'), 'test_menu_tree');
+ $this->assertFalse($this->connection->schema()->tableExists('test_menu_tree'), 'Test table is not yet created');
+ $tree_storage->countMenuLinks();
+ $this->assertTrue($this->connection->schema()->tableExists('test_menu_tree'), 'Test table was created');
+ }
+
+ /**
+ * Tests with a simple linear hierarchy.
+ */
+ public function testSimpleHierarchy() {
+ // Add some links with parent on the previous one and test some values.
+ //
+ // - test1
+ // -- test2
+ // --- test3
+ $this->addMenuLink('test1', '');
+ $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+
+ $this->addMenuLink('test2', 'test1');
+ $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2'));
+ $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2), array('test1'));
+
+ $this->addMenuLink('test3', 'test2');
+ $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3'));
+ $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3'));
+ $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 3), array('test2', 'test1'));
+ }
+
+ /**
+ * Tests the tree with moving links inside the hierarchy.
+ */
+ public function testMenuLinkMoving() {
+ // Before the move.
+ //
+ // - test1
+ // -- test2
+ // --- test3
+ // - test4
+ // -- test5
+ // --- test6
+
+ $this->addMenuLink('test1', '');
+ $this->addMenuLink('test2', 'test1');
+ $this->addMenuLink('test3', 'test2');
+ $this->addMenuLink('test4', '');
+ $this->addMenuLink('test5', 'test4');
+ $this->addMenuLink('test6', 'test5');
+
+ $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3'));
+ $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3'));
+ $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test6'));
+ $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test6'));
+ $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4'));
+
+ $this->moveMenuLink('test2', 'test5');
+ // After the 1st move.
+ //
+ // - test1
+ // - test4
+ // -- test5
+ // --- test2
+ // ---- test3
+ // --- test6
+
+ $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+ $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 3), array('test5', 'test4'), array('test3'));
+ $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 4), array('test2', 'test5', 'test4'));
+ $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6'));
+ $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test2', 'test3', 'test6'));
+ $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4'));
+
+ $this->moveMenuLink('test4', 'test1');
+ $this->moveMenuLink('test3', 'test1');
+ // After the next 2 moves.
+ //
+ // - test1
+ // -- test3
+ // -- test4
+ // --- test5
+ // ---- test2
+ // ---- test6
+
+ $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test4', 'test5', 'test2', 'test3', 'test6'));
+ $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1'));
+ $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1'));
+ $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test5', 'test6'));
+ $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 3), array('test4', 'test1'), array('test2', 'test6'));
+ $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1'));
+
+ // Deleting a link in the middle should re-attach child links to the parent
+ $this->treeStorage->delete('test4');
+ // After the delete.
+ //
+ // - test1
+ // -- test3
+ // -- test5
+ // --- test2
+ // --- test6
+ $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6'));
+ $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 3), array('test5', 'test1'));
+ $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1'));
+ $this->assertFalse($this->treeStorage->load('test4'));
+ $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test6'));
+ $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test1'));
+ }
+
+ /**
+ * Tests with hidden child links.
+ */
+ public function testMenuHiddenChildLinks() {
+ // Add some links with parent on the previous one and test some values.
+ //
+ // - test1
+ // -- test2 (hidden)
+
+ $this->addMenuLink('test1', '');
+ $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+
+ $this->addMenuLink('test2', 'test1', '', array(), 'tools', array('hidden' => 1));
+ // The 1st link does not have any visible children, so has_children is still 0.
+ $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+ $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2, 'hidden' => 1), array('test1'));
+
+ // Add more links with parent on the previous one.
+ //