diff --git a/.htaccess b/.htaccess index 5ca7305..00830a5 100644 --- a/.htaccess +++ b/.htaccess @@ -126,20 +126,17 @@ DirectoryIndex index.php index.html index.htm RewriteCond %{REQUEST_URI} !=/favicon.ico RewriteRule ^ index.php [L] - # For security reasons, deny access to other PHP files on public sites. - # Note: The following URI conditions are not anchored at the start (^), - # because Drupal may be located in a subdirectory. To further improve - # security, you can replace '!/' with '!^/'. - # Allow access to PHP files in /core (like update.php or install.php): - RewriteCond %{REQUEST_URI} !/core/[^/]*\.php$ - # Allow access to test-specific PHP files: - RewriteCond %{REQUEST_URI} !/core/modules/system/tests/https?.php$ - # Allow access to Statistics module's custom front controller. - # Copy and adapt this rule to directly execute PHP files in contributed or - # custom modules or to run another PHP application in the same directory. - RewriteCond %{REQUEST_URI} !/core/modules/statistics/statistics.php$ - # Deny access to any other PHP files that do not match the rules above. - RewriteRule "^.+/.*\.php$" - [F] + # If this is a production site you may want to forbid access to PHP files in + # subfolders for security reasons. If you need to directly execute PHP files + # in a module or want to run another PHP application somewhere in your + # docroot tree you might want to modify this. Uncomment the following two + # lines to only allow PHP files in the webroot and in "/core": + # RewriteCond %{REQUEST_URI} !^/core/[^/]*\.php$ + # RewriteRule "^.+/.*\.php$" - [F] + # Example for allowing just one PHP file of statistics module: + # RewriteCond %{REQUEST_URI} !^/core/[^/]*\.php$ + # RewriteCond %{REQUEST_URI} !^/core/modules/statistics/statistics.php$ + # RewriteRule "^.+/.*\.php$" - [F] # Rules to correctly serve gzip compressed CSS and JS files. # Requires both mod_rewrite and mod_headers to be enabled. diff --git a/core/core.services.yml b/core/core.services.yml index 0bb13d0..fe354e1 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -63,6 +63,13 @@ services: factory_method: get factory_service: cache_factory arguments: [entity] + cache.menu: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory_method: get + factory_service: cache_factory + arguments: [menu] cache.render: class: Drupal\Core\Cache\CacheBackendInterface tags: @@ -265,6 +272,21 @@ services: plugin.manager.action: class: Drupal\Core\Action\ActionManager arguments: ['@container.namespaces', '@cache.discovery', '@module_handler'] + plugin.manager.menu.link: + class: Drupal\Core\Menu\MenuLinkManager + arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler'] + menu.link_tree: + class: Drupal\Core\Menu\MenuLinkTree + arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver'] + menu.default_tree_manipulators: + class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators + arguments: ['@access_manager', '@current_user'] + menu.active_trail: + class: Drupal\Core\Menu\MenuActiveTrail + arguments: ['@plugin.manager.menu.link', '@current_route_match'] + menu.parent_form_selector: + class: Drupal\Core\Menu\MenuParentFormSelector + arguments: ['@menu.link_tree', '@entity.manager'] plugin.manager.menu.local_action: class: Drupal\Core\Menu\LocalActionManager arguments: ['@controller_resolver', '@request_stack', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user'] @@ -279,6 +301,13 @@ services: parent: default_plugin_manager plugin.cache_clearer: class: Drupal\Core\Plugin\CachedDiscoveryClearer + menu.tree_storage: + class: Drupal\Core\Menu\MenuTreeStorage + arguments: ['@database', '@cache.menu', 'menu_tree'] + public: false # Private to plugin.manager.menu.link and menu.link_tree + menu_link.static.overrides: + class: Drupal\Core\Menu\StaticMenuLinkOverrides + arguments: ['@config.factory'] request: class: Symfony\Component\HttpFoundation\Request synthetic: true diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index f4cf83e..7f8b3da 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -1150,6 +1150,18 @@ function module_invoke($module, $hook) { } /** + * Passes alterable variables to specific hook_TYPE_alter() implementations. + * + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. + * Use \Drupal::moduleHandler()->alter($hook). + * + * @see \Drupal\Core\Extension\ModuleHandler::alter() + */ +function drupal_alter($type, &$data, &$context1 = NULL, &$context2 = NULL) { + return \Drupal::moduleHandler()->alter($type, $data, $context1, $context2); +} + +/** * Returns the test prefix if this is an internal request from SimpleTest. * * @param string $new_prefix @@ -1264,6 +1276,21 @@ function drupal_installation_attempted() { } /** + * Returns the language object for a given language type. + * + * @see \Drupal\Core\Language\LanguageManager + * + * @param string $type + * The type of language object needed, e.g. Language::TYPE_INTERFACE. + * + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. + * Use \Drupal::languageManager()->getCurrentLanguage(). + */ +function language($type) { + return \Drupal::languageManager()->getCurrentLanguage($type); +} + +/** * Returns a list of languages set up on the site. * * @param $flags @@ -1666,3 +1693,4 @@ function _drupal_shutdown_function() { error_log($exception); } } + diff --git a/core/includes/errors.inc b/core/includes/errors.inc index fd949be..992ffaa 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -210,7 +210,7 @@ function _drupal_log_error($error, $fatal = FALSE) { // once more in the backtrace. array_shift($backtrace); // Generate a backtrace containing only scalar argument values. - $message .= '
' . Error::formatBacktrace($backtrace) . '
'; + $message .= '
' . format_backtrace($backtrace) . '
'; } drupal_set_message($message, $class, TRUE); } @@ -273,3 +273,21 @@ function _drupal_get_error_level() { // request on a public site, so use the non-verbose default value. return $error_level ?: ERROR_REPORTING_DISPLAY_ALL; } + +/** + * Formats a backtrace into a plain-text string. + * + * The calls show values for scalar arguments and type names for complex ones. + * + * @param array $backtrace + * A standard PHP backtrace. + * + * @return string + * A plain-text line-wrapped string ready to be put inside
.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ *   Use \Drupal\Core\Utility\Error::formatBacktrace().
+ */
+function format_backtrace(array $backtrace) {
+  return Error::formatBacktrace($backtrace);
+}
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 1b58d12..926b149 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -143,6 +143,18 @@ function drupal_build_form($form_id, &$form_state) {
 }
 
 /**
+ * Retrieves default values for the $form_state array.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ *   Use \Drupal::formBuilder()->getFormStateDefaults().
+ *
+ * @see \Drupal\Core\Form\FormBuilderInterface::getFormStateDefaults().
+ */
+function form_state_defaults() {
+  return \Drupal::formBuilder()->getFormStateDefaults();
+}
+
+/**
  * Constructs a new $form from the information in $form_state.
  *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
@@ -264,6 +276,30 @@ function drupal_process_form($form_id, &$form, &$form_state) {
 }
 
 /**
+ * Prepares a structured form array.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ *   Use \Drupal::formBuilder()->prepareForm().
+ *
+ * @see \Drupal\Core\Form\FormBuilderInterface::prepareForm().
+ */
+function drupal_prepare_form($form_id, &$form, &$form_state) {
+  \Drupal::formBuilder()->prepareForm($form_id, $form, $form_state);
+}
+
+/**
+ * Validates user-submitted form data in the $form_state array.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ *   Use \Drupal::formBuilder()->validateForm().
+ *
+ * @see \Drupal\Core\Form\FormValidatorInterface::validateForm().
+ */
+function drupal_validate_form($form_id, &$form, &$form_state) {
+  \Drupal::formBuilder()->validateForm($form_id, $form, $form_state);
+}
+
+/**
  * Redirects the user to a URL after a form has been processed.
  *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
diff --git a/core/includes/path.inc b/core/includes/path.inc
index a050217..616bb47 100644
--- a/core/includes/path.inc
+++ b/core/includes/path.inc
@@ -15,12 +15,20 @@
  *
  * @return
  *   Boolean value: TRUE if the current page is the front page; FALSE if otherwise.
- *
- * @deprecated as of Drupal 8.0. Use
- *   \Drupal\Core\Path\PathMatcherInterface::isFrontPage() instead.
  */
 function drupal_is_front_page() {
-  return \Drupal::service('path.matcher')->isFrontPage();
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['is_front_page'] = &drupal_static(__FUNCTION__);
+  }
+  $is_front_page = &$drupal_static_fast['is_front_page'];
+
+  if (!isset($is_front_page)) {
+    $is_front_page = (current_path() == \Drupal::config('system.site')->get('page.front'));
+  }
+
+  return $is_front_page;
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
index 4f30d47..2294794 100644
--- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
@@ -240,7 +240,7 @@ protected function getFieldSchemaData($field_name, array $field_schema, array $c
       // field name to avoid clashes when multiple fields of the same type are
       // added to an entity type.
       $entity_type_id = $this->entityType->id();
-      $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
+      $real_key = "{$entity_type_id}_field__{$field_name}__{$key}";
       foreach ($columns as $column) {
         // Allow for indexes and unique keys to specified as an array of column
         // name and length.
@@ -258,34 +258,6 @@ protected function getFieldSchemaData($field_name, array $field_schema, array $c
   }
 
   /**
-   * Generates a safe schema identifier (name of an index, column name etc.).
-   *
-   * @param string $entity_type_id
-   *   The ID of the entity type.
-   * @param string $field_name
-   *   The name of the field.
-   * @param string $key
-   *   The key of the field.
-   *
-   * @return string
-   *   The field identifier name.
-   */
-  protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key) {
-    $real_key = "{$entity_type_id}_field__{$field_name}__{$key}";
-    // Limit the string to 48 characters, keeping a 16 characters margin for db
-    // prefixes.
-    if (strlen($real_key) > 48) {
-      // Use a shorter separator, a truncated entity_type, and a hash of the
-      // field name.
-      // Truncate to the same length for the current and revision tables.
-      $entity_type = substr($entity_type_id, 0, 36);
-      $field_hash = substr(hash('sha256', $real_key), 0, 10);
-      $real_key = $entity_type . '__' . $field_hash;
-    }
-    return $real_key;
-  }
-
-  /**
    * Returns field foreign keys.
    *
    * @param string $field_name
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php
index 544ae46..20dcb80 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php
@@ -19,7 +19,6 @@
  *   label = @Translation("Plain text"),
  *   field_types = {
  *     "string",
- *     "string_long",
  *     "email"
  *   },
  *   quickedit = {
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php
index ec79281..6337c12 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php
@@ -16,8 +16,6 @@
  *   id = "string_long",
  *   label = @Translation("Long string"),
  *   description = @Translation("An entity field containing a long string value."),
- *   default_widget = "string_textarea",
- *   default_formatter = "string",
  *   no_ui = TRUE
  * )
  */
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextareaWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextareaWidget.php
deleted file mode 100644
index fae98b1..0000000
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextareaWidget.php
+++ /dev/null
@@ -1,86 +0,0 @@
- '5',
-      'placeholder' => '',
-    ) + parent::defaultSettings();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function settingsForm(array $form, array &$form_state) {
-    $element['rows'] = array(
-      '#type' => 'number',
-      '#title' => t('Rows'),
-      '#default_value' => $this->getSetting('rows'),
-      '#required' => TRUE,
-      '#min' => 1,
-    );
-    $element['placeholder'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Placeholder'),
-      '#default_value' => $this->getSetting('placeholder'),
-      '#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
-    );
-    return $element;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function settingsSummary() {
-    $summary = array();
-
-    $summary[] = t('Number of rows: !rows', array('!rows' => $this->getSetting('rows')));
-    $placeholder = $this->getSetting('placeholder');
-    if (!empty($placeholder)) {
-      $summary[] = t('Placeholder: @placeholder', array('@placeholder' => $placeholder));
-    }
-
-    return $summary;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, array &$form_state) {
-    $element['value'] = $element + array(
-      '#type' => 'textarea',
-      '#default_value' => $items[$delta]->value,
-      '#rows' => $this->getSetting('rows'),
-      '#placeholder' => $this->getSetting('placeholder'),
-      '#attributes' => array('class' => array('text-full')),
-    );
-
-    return $element;
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
new file mode 100644
index 0000000..2238a75
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
@@ -0,0 +1,175 @@
+accessManager = $access_manager;
+    $this->account = $account;
+  }
+
+  /**
+   * Menu link tree manipulator that performs access checks.
+   *
+   * Removes menu links from the given menu tree whose links are inaccessible
+   * for the current user, sets the 'access' property to TRUE on tree elements
+   * that are accessible for the current user.
+   *
+   * Makes the resulting menu tree impossible to render cache, unless render
+   * caching per user is acceptable.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function checkAccess(array $tree) {
+    foreach ($tree as $key => $element) {
+      // Other menu tree manipulators may already have calculated access, do
+      // not overwrite the existing value in that case.
+      if (!isset($element->access)) {
+        $tree[$key]->access = $this->menuLinkCheckAccess($element->link);
+      }
+      if ($tree[$key]->access) {
+        if ($tree[$key]->subtree) {
+          $tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
+        }
+      }
+      else {
+        unset($tree[$key]);
+      }
+    }
+    return $tree;
+  }
+
+  /**
+   * Check access for one menu link instance.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $instance
+   *   The menu link instance.
+   *
+   * @return bool
+   *   TRUE if the current user can access the link, FALSE otherwise.
+   */
+  protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
+    // Use the definition here since that's a lot faster than creating a Url
+    // object that we don't need.
+    $definition = $instance->getPluginDefinition();
+    // '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);
+    }
+    return $access;
+  }
+
+  /**
+   * Menu link tree manipulator that generates a unique index, and sorts by it.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function generateIndexAndSort(array $tree) {
+    $new_tree = array();
+    foreach ($tree as $key => $v) {
+      if ($tree[$key]->subtree) {
+        $tree[$key]->subtree = $this->generateIndexAndSort($tree[$key]->subtree);
+      }
+      $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];
+    }
+    ksort($new_tree);
+    return $new_tree;
+  }
+
+  /**
+   * Menu link tree manipulator that flattens the tree to a single level.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function flatten(array $tree) {
+    foreach ($tree as $key => $element) {
+      if ($tree[$key]->subtree) {
+        $tree += $this->flatten($tree[$key]->subtree);
+      }
+      $tree[$key]->subtree = array();
+    }
+    return $tree;
+  }
+
+  /**
+   * Menu link tree manipulator that extracts a subtree of the active trail.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   * @param int $level
+   *   The level in the active trail to extract.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function extractSubtreeOfActiveTrail(array $tree, $level) {
+    // Go down the active trail until the right level is reached.
+    while ($level-- > 0 && $tree) {
+      // Loop through the current level's elements  until we find one that is in
+      // the active trail.
+      while ($element = array_shift($tree)) {
+        if ($element->inActiveTrail) {
+          // If the element is in the active trail, we continue in the subtree.
+          $tree = $element->subtree;
+          break;
+        }
+      }
+    }
+    return $tree;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php
new file mode 100644
index 0000000..feea82a
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php
@@ -0,0 +1,97 @@
+menuLinkManager = $menu_link_manager;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * {@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('' => '');
+
+    // If a link in the given menu indeed matches the route, then use it to
+    // complete the active trail.
+    if ($active_link = $this->getActiveLink($menu_name)) {
+      if ($parents = $this->menuLinkManager->getParentIds($active_link->getPluginId())) {
+        $active_trail = $parents + $active_trail;
+      }
+    }
+
+    return $active_trail;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActiveTrailCacheKey($menu_name) {
+    return 'menu_trail.' . implode('|', $this->getActiveTrailIds($menu_name));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActiveLink($menu_name = NULL) {
+    // Note: this is a very simple implementation. If you need more control
+    // over the return value, such as matching a prioritized list of menu names,
+    // you should substitute your own implementation for the 'menu.active_trail'
+    // service in the container.
+    $found = NULL;
+
+    $route_name = $this->routeMatch->getRouteName();
+    // On a default (not custom) 403 page the route name is NULL. On a custom
+    // 403 page we will get the route name for that page, so we can consider
+    // it a feature that a relevant menu tree may be displayed.
+    if ($route_name) {
+      $route_parameters = $this->routeMatch->getRawParameters()->all();
+
+      // Load links matching this route.
+      $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name);
+      // Select the first matching link.
+      if ($links) {
+        $found = reset($links);
+      }
+    }
+    return $found;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php
new file mode 100644
index 0000000..50995ba
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php
@@ -0,0 +1,54 @@
+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 isResetable() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTranslatable() {
+    return (bool) $this->getTranslateRoute();
+  }
+
+  /**
+   * {@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 isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@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..24cf8d2
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php
@@ -0,0 +1,87 @@
+ 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 has an override.
+    return (bool) $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;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
new file mode 100644
index 0000000..a269495
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
@@ -0,0 +1,231 @@
+ '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,
+    // The name of the module providing this link.
+    '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 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 module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+
+  /**
+   * 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 \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, ModuleHandlerInterface $module_handler) {
+    $this->treeStorage = $tree_storage;
+    $this->overrides = $overrides;
+    $this->factory = new ContainerFactory($this);
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * 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.
+   *
+   * @param array $definition
+   *   The definition to be processed and modified by reference.
+   * @param $plugin_id
+   *   The ID of the plugin this definition is being used for.
+   */
+  protected function processDefinition(array &$definition, $plugin_id) {
+    $definition = NestedArray::mergeDeep($this->defaults, $definition);
+    $definition['parent'] = (string) $definition['parent'];
+    $definition['id'] = $plugin_id;
+  }
+
+  /**
+   * Instantiates if necessary and returns a YamlDiscovery instance.
+   *
+   * Since the discovery is very rarely used - only when the rebuild() method
+   * is called - it's instantiated only when actually needed instead of in the
+   * constructor.
+   *
+   * @return \Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator
+   *   A plugin discovery instance.
+   */
+  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_discovered', $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]);
+      }
+    }
+    return $definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rebuild() {
+    $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);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
+    $definition = $this->treeStorage->load($plugin_id);
+    if (empty($definition) && $exception_on_invalid) {
+      throw new PluginNotFoundException("$plugin_id could not be found.");
+    }
+    return $definition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasDefinition($plugin_id) {
+    return (bool) $this->getDefinition($plugin_id, FALSE);
+  }
+
+  /**
+   * Returns a pre-configured meu link plugin instance.
+   *
+   * @param string $plugin_id
+   *   The ID of the plugin being instantiated.
+   * @param array $configuration
+   *   An array of configuration relevant to the plugin instance.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   A menu link instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the instance cannot be created, such as if the ID is invalid.
+   */
+  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']);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteLinksInMenu($menu_name) {
+    foreach ($this->treeStorage->loadByProperties(array('menu_name' => $menu_name)) as $plugin_id => $definition) {
+      $instance = $this->createInstance($plugin_id);
+      if ($instance->isDeletable()) {
+        $this->deleteInstance($instance, TRUE);
+      }
+      elseif ($instance->isResetable()) {
+        $new_instance = $this->resetInstance($instance);
+        $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
+      }
+    }
+  }
+
+  /**
+   * Deletes a specific instance.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $instance
+   *   The plugin instance to be deleted.
+   * @param bool $persist
+   *   If TRUE, calls MenuLinkInterface::deleteLink() on the instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the plugin instance does not support deletion.
+   */
+  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 removeDefinition($id, $persist = TRUE) {
+    $definition = $this->treeStorage->load($id);
+    // It's possible the definition has already been deleted, or doesn't exist.
+    if ($definition) {
+      $instance = $this->createInstance($id);
+      $this->deleteInstance($instance, $persist);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function menuNameInUse($menu_name) {
+    $this->treeStorage->menuNameInUse($menu_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countMenuLinks($menu_name = NULL) {
+    return $this->treeStorage->countMenuLinks($menu_name);
+  }
+
+  /**
+   * {@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 loadLinksByRoute($route_name, array $route_parameters = array(), $menu_name = NULL) {
+    $instances = array();
+    $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $menu_name);
+    foreach ($loaded as $plugin_id => $definition) {
+      $instances[$plugin_id] = $this->createInstance($plugin_id);
+    }
+    return $instances;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addDefinition($id, array $definition) {
+    if ($this->treeStorage->load($id) || $id === '') {
+      throw new PluginException(sprintf('The ID %s already exists as a plugin definition or is not valid', $id));
+    }
+    // Add defaults, so there is no requirement to specify everything.
+    $this->processDefinition($definition, $id);
+    // Store the new link in the tree.
+    $this->treeStorage->save($definition);
+    return $this->createInstance($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateDefinition($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);
+      $this->treeStorage->save($changed_definition);
+    }
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetLink($id) {
+    $instance = $this->createInstance($id);
+    $new_instance = $this->resetInstance($instance);
+    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->treeStorage->save($definition);
+    return $this->createInstance($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetDefinitions() {
+    $this->treeStorage->resetDefinitions();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php
new file mode 100644
index 0000000..f6757a7
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php
@@ -0,0 +1,183 @@
+treeStorage = $tree_storage;
+    $this->menuLinkManager = $menu_link_manager;
+    $this->routeProvider = $route_provider;
+    $this->menuActiveTrail = $menu_active_trail;
+    $this->controllerResolver = $controller_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCurrentRouteMenuTreeParameters($menu_name) {
+    $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name);
+
+    $parameters = new MenuTreeParameters();
+    $parameters->setActiveTrail($active_trail)
+      // We want links in the active trail to be expanded.
+      ->addExpandedParents($active_trail)
+      // We marked the links in the active trail to be expanded, but we also
+      // want their descendants that have the "expanded" flag enabled to be
+      // expanded.
+      ->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail));
+
+    return $parameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load($menu_name, MenuTreeParameters $parameters) {
+    $data = $this->treeStorage->loadTreeData($menu_name, $parameters);
+    // Pre-load all the route objects in the tree for access checks.
+    if ($data['route_names']) {
+      $this->routeProvider->getRoutesByNames($data['route_names']);
+    }
+    return $this->createInstances($data['tree']);
+  }
+
+  /**
+   * Helper function that recursively instantiates the plugins.
+   */
+  protected function createInstances($data_tree) {
+    $tree = array();
+    foreach ($data_tree as $key => $element) {
+      $subtree = $this->createInstances($element['subtree']);
+      // Build a MenuLinkTreeElement out of the menu tree link definition:
+      // transform the tree link definition into a link definition and store
+      // tree metadata.
+      $tree[$key] = new MenuLinkTreeElement(
+        $this->menuLinkManager->createInstance($element['definition']['id']),
+        (bool) $element['has_children'],
+        (int) $element['depth'],
+        (bool) $element['in_active_trail'],
+        $subtree
+      );
+    }
+    return $tree;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transform(array $tree, array $manipulators) {
+    foreach ($manipulators as $manipulator) {
+      $callable = $manipulator['callable'];
+      if (!is_callable($callable)) {
+        $callable = $this->controllerResolver->getControllerFromDefinition($callable);
+      }
+      // Prepare the arguments for the menu tree manipulator callable; the first
+      // argument is always the menu link tree.
+      if (isset($manipulator['args'])) {
+        array_unshift($manipulator['args'], $tree);
+        $tree = call_user_func_array($callable, $manipulator['args']);
+      }
+      else {
+        $tree = call_user_func($callable, $tree);
+      }
+    }
+    return $tree;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $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->hasChildren && !empty($data->subtree)) { + $class[] = 'expanded'; + } + elseif ($data->hasChildren) { + $class[] = 'collapsed'; + } + else { + $class[] = 'leaf'; + } + // Set a class if the link is in the active trail. + if ($data->inActiveTrail) { + $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->subtree ? $this->build($data->subtree) : array(); + if (isset($data->options)) { + $element['#url']->setOptions(NestedArray::mergeDeep($element['#url']->getOptions(), $data->options)); + } + $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 maxDepth() { + return $this->treeStorage->maxDepth(); + } + + /** + * {@inheritdoc} + */ + public function getSubtreeHeight($id) { + return $this->treeStorage->getSubtreeHeight($id); + } + + /** + * {@inheritdoc} + */ + public function getExpanded($menu_name, array $parents) { + return $this->treeStorage->getExpanded($menu_name, $parents); + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php new file mode 100644 index 0000000..db5cbc9 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php @@ -0,0 +1,125 @@ +link = $link; + $this->hasChildren = $has_children; + $this->depth = $depth; + $this->subtree = $subtree; + $this->inActiveTrail = $in_active_trail; + } + + /** + * Counts all menu links in the current subtree. + * + * @return int + * The number of menu links in this subtree (one plus the number of menu + * links in all descendants). + */ + public function count() { + $sum = function ($carry, MenuLinkTreeElement $element) { + return $carry + $element->count(); + }; + return 1 + array_reduce($this->subtree, $sum); + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php new file mode 100644 index 0000000..9b1f5df --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php @@ -0,0 +1,142 @@ +menuLinkTree = $menu_link_tree; + $this->entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public function getParentSelectOptions($id = '', array $menus = NULL) { + if (!isset($menus)) { + $menus = $this->getMenuOptions(); + } + + $options = array(); + $depth_limit = $this->getParentDepthLimit($id); + foreach ($menus as $menu_name => $menu_title) { + $options[$menu_name . ':'] = '<' . $menu_title . '>'; + + $parameters = new MenuTreeParameters(); + $parameters->setMaxDepth($depth_limit); + $tree = $this->menuLinkTree->load($menu_name, $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $tree = $this->menuLinkTree->transform($tree, $manipulators); + $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit); + } + return $options; + } + + /** + * {@inheritdoc} + */ + public function parentSelectElement($menu_parent, $id = '', array $menus = NULL) { + $options = $this->getParentSelectOptions($id, $menus); + // If no options were found, there is nothing to select. + if ($options) { + if (!isset($options[$menu_parent])) { + // Try putting it at the top level in the current menu. + list($menu_name, $parent) = explode(':', $menu_parent, 2); + $menu_parent = $menu_name . ':'; + } + if (isset($options[$menu_parent])) { + return array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $menu_parent, + ); + } + } + return array(); + } + + /** + * Returns the maximum depth of the possible parents of the menu link. + * + * @param string $id + * The menu link plugin ID or an empty value for a new link. + * + * @return int + * The depth related to the depth of the given menu link. + */ + protected function getParentDepthLimit($id) { + if ($id) { + $limit = $this->menuLinkTree->maxDepth() - $this->menuLinkTree->getSubtreeHeight($id); + } + else { + $limit = $this->menuLinkTree->maxDepth() - 1; + } + return $limit; + } + + /** + * Iterates over all items in the tree to prepare the parents select options. + * + * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $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 $element) { + if ($element->depth > $depth_limit) { + // Don't iterate through any links on this level. + break; + } + $link = $element->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 (!empty($element->subtree)) { + $this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit); + } + } + } + } + + /** + * Gets a list of menu names for use as options. + * + * @param array $menu_names + * Optional array of menu names to limit the options, or NULL to load all. + * + * @return array + * Keys are menu names (ids) values are the menu labels. + */ + protected 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; + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php new file mode 100644 index 0000000..fdcc4b6 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php @@ -0,0 +1,56 @@ +root = (string) $root; + return $this; + } + + /** + * Sets a minimum depth; loads a menu tree from the given level. + * + * @param int $min_depth + * The (root-relative) minimum depth to apply. + * + * @return $this + */ + public function setMinDepth($min_depth) { + $this->minDepth = max(1, $min_depth); + return $this; + } + + /** + * Sets a minimum depth; loads a menu tree up to the given level. + * + * @param int $max_depth + * The (root-relative) maximum depth to apply. + * + * @return $this + * + * @codeCoverageIgnore + */ + public function setMaxDepth($max_depth) { + $this->maxDepth = $max_depth; + return $this; + } + + /** + * Adds parent menu links IDs to restrict the tree (only show children). + * + * @param string[] $parents + * An array containing the parent IDs to limit the tree. + * + * @return $this + */ + public function addExpandedParents(array $parents) { + $this->expandedParents = array_merge($this->expandedParents, $parents); + $this->expandedParents = array_unique($this->expandedParents); + return $this; + } + + /** + * Sets the active trail IDs used to set the inActiveTrail property. + * + * @param string[] $active_trail + * An array containing the active trail: a list of menu link plugin IDs. + * + * @return $this + * + * @see \Drupal\Core\Menu\MenuActiveTrail::getActiveTrailIds() + * + * @codeCoverageIgnore + */ + public function setActiveTrail(array $active_trail) { + $this->activeTrail = $active_trail; + return $this; + } + + /** + * Adds a custom query condition. + * + * @param string $definition_field + * Only conditions that are testing menu link definition fields are allowed. + * @param mixed $value + * The value to test the link definition field against. In most cases, this + * is a scalar. For more complex options, it is an array. The meaning of + * each element in the array is dependent on the $operator. + * @param string|NULL $operator + * (optional) The comparison operator, such as =, <, or >=. It also accepts + * more complex options such as IN, LIKE, or BETWEEN. If NULL, defaults to + * the = operator. + * + * @return $this + */ + public function addCondition($definition_field, $value, $operator = NULL) { + if (!isset($operator)) { + $this->conditions[$definition_field] = $value; + } + else { + $this->conditions[$definition_field] = array($value, $operator); + } + return $this; + } + + /** + * Excludes hidden links. + * + * @return $this + */ + public function excludeHiddenLinks() { + $this->addCondition('hidden', 0); + return $this; + } + + /** + * Ensures only the top level of the tree is loaded. + * + * @return $this + */ + public function topLevelOnly() { + $this->setMaxDepth(1); + return $this; + } + + /** + * Excludes the root menu link from the tree. + * + * Note that this is only necessary when you specified a custom root, because + * the normal root ID is the empty string, '', which does not correspond to an + * actual menu link. Hence when loading a menu link tree without specifying a + * custom root the tree will start at the children even if this method has not + * been called. + * + * @return $this + */ + public function excludeRoot() { + $this->setMinDepth(1); + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php new file mode 100644 index 0000000..0382f0e --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php @@ -0,0 +1,1426 @@ +connection = $connection; + $this->menuCacheBackend = $menu_cache_backend; + $this->table = $table; + $this->options = $options; + } + + /** + * {@inheritdoc} + */ + public function maxDepth() { + return static::MAX_DEPTH; + } + + /** + * {@inheritdoc} + */ + public function resetDefinitions() { + $this->definitions = array(); + } + + /** + * {@inheritdoc} + */ + public function rebuild(array $definitions) { + $links = array(); + $children = array(); + $top_links = array(); + // Fetch the list of existing menus, in case some are not longer populated + // after the rebuild. + $before_menus = $this->getMenuNames(); + if ($definitions) { + foreach ($definitions as $id => $link) { + // Flag this link as discovered, i.e. saved via rebuild(). + $link['discovered'] = 1; + 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); + } + $this->resetDefinitions(); + $affected_menus = $this->getMenuNames() + $before_menus; + // Invalidate any cache tagged with any menu name. + Cache::invalidateTags(array('menu' => $affected_menus)); + $this->resetDefinitions(); + // Every item in the cache bin should have one of the menu cache tags but it + // is not guaranteed, so invalidate everything in the bin. + $this->menuCacheBackend->invalidateAll(); + } + + /** + * Purges multiple menu links that no longer exist. + * + * @param array $ids + * An array of menu link IDs. + */ + protected function purgeMultiple(array $ids) { + $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 + * Thrown 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) { + $affected_menus = $this->doSave($link); + $this->resetDefinitions(); + Cache::invalidateTags(array('menu' => $affected_menus)); + return $affected_menus; + } + + /** + * Saves a link without clearing caches. + * + * @param array $link + * A definition, according to $definitionFields, for a + * \Drupal\Core\Menu\MenuLinkInterface plugin. + * + * @return array + * The menu names affected by the save operation (1 or 2 names). + * + * @throws \Exception + * Thrown if the storage back-end does not exist and could not be created. + * @throws \Drupal\Component\Plugin\Exception\PluginException + * Thrown if the definition is invalid, for example, if the specified parent + * would cause the links children to be moved to greater than the maximum + * depth. + */ + protected function doSave(array $link) { + $original = $this->loadFull($link['id']); + // @todo Should we just return here if the link values match the original + // values completely? + // https://www.drupal.org/node/2302137 + $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); + } + catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + return $affected_menus; + } + + /** + * Fills in all the fields the database save needs, using the link definition. + * + * @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. + // https://www.drupal.org/node/2302149 + 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); + // Many children may have moved. + $this->resetDefinitions(); + Cache::invalidateTags(array('menu' => $item['menu_name'])); + } + } + + /** + * {@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 find the parent of. + * @param array|false $original + * The original link that might be used to find the parent if the parent + * is not set on the $link, or FALSE if the original could not be loaded. + * + * @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']) { + // Otherwise, fall back to the original parent. + $candidates[] = $original['parent']; + } + + foreach ($candidates as $id) { + $parent = $this->loadFull($id); + if ($parent) { + break; + } + } + return $parent; + } + + /** + * Sets has_children 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(); + } + } + + /** + * Prepares a link by unserializing values and saving the definition. + * + * @param array $link + * The data loaded in the query. + * @param bool $intersect + * If TRUE, filter out values that are not part of the actual definition. + * + * @return array + * The prepared link data. + */ + protected function prepareLink(array $link, $intersect = FALSE) { + foreach ($this->serializedFields() as $name) { + $link[$name] = unserialize($link[$name]); + } + if ($intersect) { + $link = array_intersect_key($link, array_flip($this->definitionFields())); + } + $this->definitions[$link['id']] = $link; + return $link; + } + + /** + * {@inheritdoc} + */ + public function loadByProperties(array $properties) { + // @todo Only allow loading by plugin definition properties. + https://www.drupal.org/node/2302165 + $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 $id => $link) { + $loaded[$id] = $this->prepareLink($link); + } + return $loaded; + } + + /** + * {@inheritdoc} + */ + public function loadByRoute($route_name, array $route_parameters = array(), $menu_name = NULL) { + asort($route_parameters); + // Since this will be urlencoded, it's safe to store and match against a + // text field. + // @todo Standardize an efficient way to load by route name and parameters + // in place of system path. https://www.drupal.org/node/2302139 + $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 ($menu_name) { + $query->condition('menu_name', $menu_name); + } + // Make the ordering deterministic. + $query->orderBy('depth'); + $query->orderBy('weight'); + $query->orderBy('id'); + $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + foreach ($loaded as $id => $link) { + $loaded[$id] = $this->prepareLink($link); + } + 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 $id => $link) { + $loaded[$id] = $this->prepareLink($link); + } + return $loaded; + } + + /** + * {@inheritdoc} + */ + public function load($id) { + if (isset($this->definitions[$id])) { + return $this->definitions[$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. + // https://www.drupal.org/node/2302043 + $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 is being used more + // than once per page load. https://www.drupal.org/node/2302185 + 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? https://www.drupal.org/node/2302187 + 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. + * + * @param string $id + * The definition ID. + * @param array $children + * An array of IDs of child links collected by parent ID. + * @param array $links + * An array of all definitions keyed by ID. + */ + 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->doSave($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 loadTreeData($menu_name, MenuTreeParameters $parameters) { + // Build the cache id; sort 'expanded' and 'conditions' to prevent duplicate + // cache items. + sort($parameters->expandedParents); + sort($parameters->conditions); + $tree_cid = "tree-data:$menu_name:" . serialize($parameters); + $cache = $this->menuCacheBackend->get($tree_cid); + if ($cache && isset($cache->data)) { + $data = $cache->data; + // Cache the definitions in memory so they don't need to be loaded again. + $this->definitions += $data['definitions']; + unset($data['definitions']); + } + else { + $links = $this->loadLinks($menu_name, $parameters); + $data['tree'] = $this->doBuildTreeData($links, $parameters->activeTrail, $parameters->minDepth); + $data['definitions'] = array(); + $data['route_names'] = $this->collectRoutesAndDefinitions($data['tree'], $data['definitions']); + $this->menuCacheBackend->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name)); + // The definitions were already added to $this->definitions in + // $this->doBuildTreeData() + unset($data['definitions']); + } + return $data; + } + + /** + * Loads links in the given menu, according to the given tree parameters. + * + * @param string $menu_name + * A menu name. + * @param \Drupal\Core\Menu\MenuTreeParameters $parameters + * The parameters to determine which menu links to be loaded into a tree. + * This method will set the absolute minimum depth, which is used in + * MenuTreeStorage::doBuildTreeData(). + * + * @return array + * 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_tree} table. This array must be ordered + * depth-first. + */ + protected function loadLinks($menu_name, MenuTreeParameters $parameters) { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table); + + // Allow a custom root to be specified for loading a menu link tree. If + // omitted, the default root (i.e. the actual root, '') is used. + if ($parameters->root !== '') { + $root = $this->loadFull($parameters->root); + + // If the custom root does not exist, we cannot load the links below it. + if (!$root) { + return array(); + } + + // When specifying a custom root, we only want to find links whose + // parent IDs match that of the root; that's how ignore the rest of the + // tree. In other words: we exclude everything unreachable from the + // custom root. + for ($i = 1; $i <= $root['depth']; $i++) { + $query->condition("p$i", $root["p$i"]); + } + + // When specifying a custom root, the menu is determined by that root. + $menu_name = $root['menu_name']; + + // If the custom root exists, then we must rewrite some of our + // parameters; parameters are relative to the root (default or custom), + // but the queries require absolute numbers, so adjust correspondingly. + if (isset($parameters->minDepth)) { + $parameters->minDepth += $root['depth']; + } + else { + $parameters->minDepth = $root['depth']; + } + if (isset($parameters->maxDepth)) { + $parameters->maxDepth += $root['depth']; + } + } + + // If no minimum depth is specified, then set the actual minimum depth, + // depending on the root. + if (!isset($parameters->minDepth)) { + if ($parameters->root !== '' && $root) { + $parameters->minDepth = $root['depth']; + } + else { + $parameters->minDepth = 1; + } + } + + for ($i = 1; $i <= $this->maxDepth(); $i++) { + $query->orderBy('p' . $i, 'ASC'); + } + + $query->condition('menu_name', $menu_name); + + if (!empty($parameters->expandedParents)) { + $query->condition('parent', $parameters->expandedParents, 'IN'); + } + if (isset($parameters->minDepth) && $parameters->minDepth > 1) { + $query->condition('depth', $parameters->minDepth, '>='); + } + if (isset($parameters->maxDepth)) { + $query->condition('depth', $parameters->maxDepth, '<='); + } + // 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) { + if (!is_array($value)) { + $query->condition($column, $value); + } + else { + $operator = $value[1]; + $value = $value[0]; + $query->condition($column, $value, $operator); + } + } + } + + $links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + + return $links; + } + + /** + * Traverses the menu tree and collects all the route names and definitions. + * + * @param array $tree + * The menu tree you wish to operate on. + * @param array $definitions + * An array to accumulate definitions by reference. + * + * @return array + * Array of route names, with all values being unique. + */ + protected function collectRoutesAndDefinitions(array $tree, array &$definitions) { + return array_values($this->doCollectRoutesAndDefinitions($tree, $definitions)); + } + + /** + * Collects all the route names and definitions. + * + * @param array $tree + * A menu link tree from MenuTreeStorage::doBuildTreeData() + * @param array $definitions + * The collected definitions which are populated by reference. + * + * @return array + * The collected route names. + */ + protected function doCollectRoutesAndDefinitions(array $tree, array &$definitions) { + $route_names = array(); + foreach (array_keys($tree) as $id) { + $definitions[$id] = $this->definitions[$id]; + if (!empty($definition['route_name'])) { + $route_names[$definition['route_name']] = $definition['route_name']; + } + if ($tree[$id]['subtree']) { + $route_names += $this->doCollectRoutesAndDefinitions($tree[$id]['subtree'], $definitions); + } + } + return $route_names; + } + + /** + * {@inheritdoc} + */ + public function loadSubtreeData($id, $max_relative_depth = NULL) { + $tree = array(); + $root = $this->loadFull($id); + if (!$root) { + return $tree; + } + $parameters = new MenuTreeParameters(); + $parameters->setRoot($id)->excludeHiddenLinks(); + return $this->loadTreeData($root['menu_name'], $parameters); + } + + /** + * {@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 loadAllChildren($id, $max_relative_depth = NULL) { + $parameters = new MenuTreeParameters(); + $parameters->setRoot($id)->excludeRoot()->setMaxDepth($max_relative_depth)->excludeHiddenLinks(); + $links = $this->loadLinks(NULL, $parameters); + foreach ($links as $id => $link) { + $links[$id] = $this->prepareLink($link); + } + return $links; + } + + /** + * 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 $this->table. This array must be ordered + * depth-first. MenuTreeStorage::loadTreeData() includes 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 built tree. + * + * @see \Drupal\Core\Menu\MenuTreeStorage::loadTreeData() + */ + protected function treeDataRecursive(array &$links, array $parents, $depth) { + $tree = array(); + while ($tree_link_definition = array_pop($links)) { + $tree[$tree_link_definition['id']] = array( + 'definition' => $this->prepareLink($tree_link_definition, TRUE), + 'has_children' => $tree_link_definition['has_children'], + // We need to determine if we're on the path to root so we can later + // build the correct active trail. + 'in_active_trail' => in_array($tree_link_definition['id'], $parents), + 'subtree' => array(), + 'depth' => $tree_link_definition['depth'], + ); + // 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[$tree_link_definition['id']]['subtree'] = $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; + } + + /** + * Determines serialized fields in the storage. + * + * @return array + * A list of fields that are serialized in the database. + */ + 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; + } + + /** + * Determines fields that are part of the plugin definition. + * + * @return array + * The list of the subset of fields that are part of the plugin definition. + */ + protected function definitionFields() { + return $this->definitionFields; + } + + /** + * Defines the schema for the tree table. + * + * @return array + * The schema API definition for the SQL storage 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. + // https://www.drupal.org/node/2302197 + '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..0deff75 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php @@ -0,0 +1,256 @@ +configFactory = $config_factory; + } + + /** + * Gets the configuration object when needed. + * + * 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. Double underscores are converted to triple underscores to + * avoid accidental conflicts. + * + * @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 strtr($id, array('.' => '__', '__' => '___')); + } + +} diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php new file mode 100644 index 0000000..3d7b4d5 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php @@ -0,0 +1,85 @@ +regexes[$patterns])) { + // Lazy-load front page config. + if (!isset($this->frontPage)) { + $this->frontPage = $this->configFactory->get('system.site')->get('page.front'); + } // Convert path settings to a regular expression. $to_replace = array( // Replace newlines with a logical 'or'. @@ -70,37 +67,11 @@ public function matchPath($path, $patterns) { $replacements = array( '|', '.*', - '\1' . preg_quote($this->getFrontPagePath(), '/') . '\2', + '\1' . preg_quote($this->frontPage, '/') . '\2', ); $patterns_quoted = preg_quote($patterns, '/'); $this->regexes[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/'; } return (bool) preg_match($this->regexes[$patterns], $path); } - - /** - * {@inheritdoc} - */ - public function isFrontPage() { - // Cache the result as this is called often. - if (!isset($this->isCurrentFrontPage)) { - $this->isCurrentFrontPage = (current_path() == $this->getFrontPagePath()); - } - return $this->isCurrentFrontPage; - } - - /** - * Gets the current front page path. - * - * @return string - * The front page path. - */ - protected function getFrontPagePath() { - // Lazy-load front page config. - if (!isset($this->frontPage)) { - $this->frontPage = $this->configFactory->get('system.site') - ->get('page.front'); - } - return $this->frontPage; - } } diff --git a/core/lib/Drupal/Core/Path/PathMatcherInterface.php b/core/lib/Drupal/Core/Path/PathMatcherInterface.php index 114d078..8c0b800 100644 --- a/core/lib/Drupal/Core/Path/PathMatcherInterface.php +++ b/core/lib/Drupal/Core/Path/PathMatcherInterface.php @@ -25,12 +25,4 @@ */ public function matchPath($path, $patterns); - /** - * Checks if the current page is the front page. - * - * @return bool - * TRUE if the current page is the front page. - */ - public function isFrontPage(); - } diff --git a/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php b/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php index 5febcd3..9ab429f 100644 --- a/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php +++ b/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php @@ -21,7 +21,7 @@ class CachedDiscoveryClearer { * * @var \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface[] */ - protected $cachedDiscoveries; + protected $cachedDiscoveries = array(); /** * Adds a plugin manager to the active list. diff --git a/core/lib/Drupal/Core/Plugin/PluginManagerPass.php b/core/lib/Drupal/Core/Plugin/PluginManagerPass.php index 8bfde35..9efdcf2 100644 --- a/core/lib/Drupal/Core/Plugin/PluginManagerPass.php +++ b/core/lib/Drupal/Core/Plugin/PluginManagerPass.php @@ -23,7 +23,9 @@ public function process(ContainerBuilder $container) { $cache_clearer_definition = $container->getDefinition('plugin.cache_clearer'); foreach ($container->getDefinitions() as $service_id => $definition) { if (strpos($service_id, 'plugin.manager.') === 0 || $definition->hasTag('plugin_manager_cache_clear')) { - $cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id))); + if (is_subclass_of($definition->getClass(), '\Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface')) { + $cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id))); + } } } } diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php index e0a0ddb..0e78018 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php @@ -294,8 +294,8 @@ public function testContactConfigEntityTranslation() { // Submit feedback. $edit = array( - 'subject[0][value]' => 'Test subject', - 'message[0][value]' => 'Test message', + 'subject' => 'Test subject', + 'message' => 'Test message', ); $this->drupalPostForm(NULL, $edit, t('Send message')); } diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module index e8485c1..340b518 100644 --- a/core/modules/contact/contact.module +++ b/core/modules/contact/contact.module @@ -85,11 +85,27 @@ function contact_entity_extra_field_info() { 'weight' => -30, ); } + $fields['contact_message'][$bundle]['form']['subject'] = array( + 'label' => t('Subject'), + 'description' => t('Text'), + 'weight' => -10, + ); + $fields['contact_message'][$bundle]['form']['message'] = array( + 'label' => t('Message'), + 'description' => t('Long text'), + 'weight' => 0, + ); $fields['contact_message'][$bundle]['form']['copy'] = array( 'label' => t('Send copy to sender'), 'description' => t('Option'), 'weight' => 50, ); + + $fields['contact_message'][$bundle]['display']['message'] = array( + 'label' => t('Message'), + 'description' => t('The main contact message'), + 'weight' => 0, + ); } $fields['user']['user']['form']['contact'] = array( diff --git a/core/modules/contact/src/Entity/Message.php b/core/modules/contact/src/Entity/Message.php index d3c8c38..f2c05fc 100644 --- a/core/modules/contact/src/Entity/Message.php +++ b/core/modules/contact/src/Entity/Message.php @@ -151,7 +151,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['category'] = FieldDefinition::create('entity_reference') ->setLabel(t('Category ID')) ->setDescription(t('The ID of the associated category.')) - ->setSetting('target_type', 'contact_category') + ->setSettings(array('target_type' => 'contact_category')) ->setRequired(TRUE); $fields['name'] = FieldDefinition::create('string') @@ -162,35 +162,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t("The sender's email")) ->setDescription(t('The email of the person that is sending the contact message.')); - // The subject of the contact message. $fields['subject'] = FieldDefinition::create('string') - ->setLabel(t('Subject')) - ->setRequired(TRUE) - ->setSetting('max_length', 100) - ->setDisplayOptions('form', array( - 'type' => 'string', - 'weight' => -10, - )) - ->setDisplayConfigurable('form', TRUE); - - // The text of the contact message. - $fields['message'] = FieldDefinition::create('string_long') - ->setLabel(t('Message')) - ->setRequired(TRUE) - ->setDisplayOptions('form', array( - 'type' => 'string_textarea', - 'weight' => 0, - 'settings' => array( - 'rows' => 12, - ), - )) - ->setDisplayConfigurable('form', TRUE) - ->setDisplayOptions('view', array( - 'type' => 'string', - 'weight' => 0, - 'label' => 'above', - )) - ->setDisplayConfigurable('view', TRUE); + ->setLabel(t('The message subject')) + ->setDescription(t('The subject of the contact message.')); + + $fields['message'] = FieldDefinition::create('string') + ->setLabel(t('The message text')) + ->setDescription(t('The text of the contact message.')); $fields['copy'] = FieldDefinition::create('boolean') ->setLabel(t('Copy')) @@ -199,7 +177,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['recipient'] = FieldDefinition::create('entity_reference') ->setLabel(t('Recipient ID')) ->setDescription(t('The ID of the recipient user for personal contact messages.')) - ->setSetting('target_type', 'user'); + ->setSettings(array('target_type' => 'user')); return $fields; } diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php index a9729f9..85dabd1 100644 --- a/core/modules/contact/src/MessageForm.php +++ b/core/modules/contact/src/MessageForm.php @@ -116,6 +116,19 @@ public function form(array $form, array &$form_state) { ); } + $form['subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#maxlength' => 100, + '#required' => TRUE, + ); + $form['message'] = array( + '#type' => 'textarea', + '#title' => t('Message'), + '#required' => TRUE, + '#rows' => 12, + ); + $form['copy'] = array( '#type' => 'checkbox', '#title' => t('Send yourself a copy.'), diff --git a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php index d88ba5a..d343f8d 100644 --- a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php +++ b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\contact\ContactAuthenticatedUserTest. + * Definition of Drupal\contact\ContactAuthenticatedUserTest. */ namespace Drupal\contact\Tests; diff --git a/core/modules/contact/src/Tests/ContactPersonalTest.php b/core/modules/contact/src/Tests/ContactPersonalTest.php index eb033bd..6f3e2f1 100644 --- a/core/modules/contact/src/Tests/ContactPersonalTest.php +++ b/core/modules/contact/src/Tests/ContactPersonalTest.php @@ -2,13 +2,12 @@ /** * @file - * Contains \Drupal\contact\Tests\ContactPersonalTest. + * Definition of Drupal\contact\Tests\ContactPersonalTest. */ namespace Drupal\contact\Tests; use Drupal\Component\Utility\String; -use Drupal\Core\Session\AccountInterface; use Drupal\simpletest\WebTestBase; /** @@ -74,13 +73,13 @@ function testSendPersonalContactMessage() { $this->assertEqual($mail['key'], 'user_mail'); $variables = array( '!site-name' => \Drupal::config('system.site')->get('name'), - '!subject' => $message['subject[0][value]'], + '!subject' => $message['subject'], '!recipient-name' => $this->contact_user->getUsername(), ); $this->assertEqual($mail['subject'], t('[!site-name] !subject', $variables), 'Subject is in sent message.'); $this->assertTrue(strpos($mail['body'], t('Hello !recipient-name,', $variables)) !== FALSE, 'Recipient name is in sent message.'); $this->assertTrue(strpos($mail['body'], $this->web_user->getUsername()) !== FALSE, 'Sender name is in sent message.'); - $this->assertTrue(strpos($mail['body'], $message['message[0][value]']) !== FALSE, 'Message body is in sent message.'); + $this->assertTrue(strpos($mail['body'], $message['message']) !== FALSE, 'Message body is in sent message.'); // Check there was no problems raised during sending. $this->drupalLogout(); @@ -269,22 +268,18 @@ protected function checkContactAccess($response, $contact_value = NULL) { /** * Fills out a user's personal contact form and submits it. * - * @param \Drupal\Core\Session\AccountInterface $account + * @param $account * A user object of the user being contacted. - * @param array $message + * @param $message * (optional) An array with the form fields being used. Defaults to an empty * array. - * - * @return array - * An array with the form fields being used. */ - protected function submitPersonalContact(AccountInterface $account, array $message = array()) { + protected function submitPersonalContact($account, array $message = array()) { $message += array( - 'subject[0][value]' => $this->randomName(16), - 'message[0][value]' => $this->randomName(64), + 'subject' => $this->randomName(16), + 'message' => $this->randomName(64), ); $this->drupalPostForm('user/' . $account->id() . '/contact', $message, t('Send message')); return $message; } - } diff --git a/core/modules/contact/src/Tests/ContactSitewideTest.php b/core/modules/contact/src/Tests/ContactSitewideTest.php index 2be4208..a89e7a8 100644 --- a/core/modules/contact/src/Tests/ContactSitewideTest.php +++ b/core/modules/contact/src/Tests/ContactSitewideTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\contact\Tests\ContactSitewideTest. + * Definition of Drupal\contact\Tests\ContactSitewideTest. */ namespace Drupal\contact\Tests; @@ -260,14 +260,14 @@ function testSiteWideContact() { // Submit the contact form and verify the content. $edit = array( - 'subject[0][value]' => $this->randomName(), - 'message[0][value]' => $this->randomName(), + 'subject' => $this->randomName(), + 'message' => $this->randomName(), $field_name . '[0][value]' => $this->randomName(), ); $this->drupalPostForm(NULL, $edit, t('Send message')); $mails = $this->drupalGetMails(); $mail = array_pop($mails); - $this->assertEqual($mail['subject'], t('[@label] @subject', array('@label' => $label, '@subject' => $edit['subject[0][value]']))); + $this->assertEqual($mail['subject'], t('[@label] @subject', array('@label' => $label, '@subject' => $edit['subject']))); $this->assertTrue(strpos($mail['body'], $field_label)); $this->assertTrue(strpos($mail['body'], $edit[$field_name . '[0][value]'])); } @@ -384,8 +384,8 @@ function submitContact($name, $mail, $subject, $id, $message) { $edit = array(); $edit['name'] = $name; $edit['mail'] = $mail; - $edit['subject[0][value]'] = $subject; - $edit['message[0][value]'] = $message; + $edit['subject'] = $subject; + $edit['message'] = $message; if ($id == \Drupal::config('contact.settings')->get('default_category')) { $this->drupalPostForm('contact', $edit, t('Send message')); } diff --git a/core/modules/field/src/Tests/FieldAttachOtherTest.php b/core/modules/field/src/Tests/FieldAttachOtherTest.php index a058dd6..8e54bc1 100644 --- a/core/modules/field/src/Tests/FieldAttachOtherTest.php +++ b/core/modules/field/src/Tests/FieldAttachOtherTest.php @@ -263,7 +263,7 @@ function testEntityFormDisplayBuildForm() { // Test generating widgets for all fields. $display = entity_get_form_display($entity_type, $this->instance->bundle, 'default'); $form = array(); - $form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $form_state = form_state_defaults(); $display->buildForm($entity, $form, $form_state); $this->assertEqual($form[$this->field_name]['widget']['#title'], $this->instance->getLabel(), "First field's form title is {$this->instance->getLabel()}"); @@ -285,7 +285,7 @@ function testEntityFormDisplayBuildForm() { } } $form = array(); - $form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $form_state = form_state_defaults(); $display->buildForm($entity, $form, $form_state); $this->assertFalse(isset($form[$this->field_name]), 'The first field does not exist in the form'); @@ -308,7 +308,7 @@ function testEntityFormDisplayExtractFormValues() { // Build the form for all fields. $display = entity_get_form_display($entity_type, $this->instance->bundle, 'default'); $form = array(); - $form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $form_state = form_state_defaults(); $display->buildForm($entity_init, $form, $form_state); // Simulate incoming values. @@ -342,7 +342,7 @@ function testEntityFormDisplayExtractFormValues() { $values_2[1]['value'] = 0; // Pretend the form has been built. - \Drupal::formBuilder()->prepareForm('field_test_entity_form', $form, $form_state); + drupal_prepare_form('field_test_entity_form', $form, $form_state); drupal_process_form('field_test_entity_form', $form, $form_state); $form_state['values'][$this->field_name] = $values; $form_state['values'][$this->field_name_2] = $values_2; diff --git a/core/modules/field/src/Tests/FormTest.php b/core/modules/field/src/Tests/FormTest.php index 2830abe..16f78c8 100644 --- a/core/modules/field/src/Tests/FormTest.php +++ b/core/modules/field/src/Tests/FormTest.php @@ -531,7 +531,7 @@ function testFieldFormAccess() { $display = entity_get_form_display($entity_type, $entity_type, 'default'); $form = array(); - $form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $form_state = form_state_defaults(); $display->buildForm($entity, $form, $form_state); $this->assertFalse($form[$field_name_no_access]['#access'], 'Field #access is FALSE for the field without edit access.'); diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 52a255a..a0576a9 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -1607,7 +1607,7 @@ function template_preprocess_file_link(&$variables) { '#theme' => 'image__file_icon', '#uri' => file_icon_url($file_entity, $icon_directory), '#alt' => '', - '#title' => String::checkPlain($file_entity->getFilename()), + '#title' => check_plain($file_entity->getFilename()), '#attributes' => array('class' => 'file-icon'), ); 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..5ea5cc7 --- /dev/null +++ b/core/modules/menu_link_content/menu_link_content.info.yml @@ -0,0 +1,6 @@ +name: 'Custom Menu Links' +type: module +description: 'Allows administrators to create custom menu links.' +package: Core +version: VERSION +core: 8.x 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..25dfb26 --- /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/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php new file mode 100644 index 0000000..ae84e07 --- /dev/null +++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php @@ -0,0 +1,387 @@ +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 getPluginDefinition() { + $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\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + + // The menu link can just be updated if there is already an menu link entry + // on both entity and menu link plugin level. + if ($update && $menu_link_manager->getDefinition($this->getPluginId())) { + // When the entity is saved via a plugin instance, we should not call + // the menu tree manager to update the definition a second time. + if (!$this->insidePlugin) { + $menu_link_manager->updateDefinition($this->getPluginId(), $this->getPluginDefinition(), FALSE); + } + } + else { + $menu_link_manager->addDefinition($this->getPluginId(), $this->getPluginDefinition()); + } + } + + /** + * {@inheritdoc} + */ + public static function preDelete(EntityStorageInterface $storage, array $entities) { + parent::preDelete($storage, $entities); + + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + + foreach ($entities as $menu_link) { + /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */ + $menu_link_manager->removeDefinition($menu_link->getPluginId(), FALSE); + } + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields['id'] = 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 https://www.drupal.org/node/2302205. + $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); + + // The form widget doesn't work yet for core fields, so we skip the + // for display and manually create form elements for the boolean fields. + // @see https://drupal.org/node/2226493 + // @see https://drupal.org/node/2150511 + $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, + )); + + // We manually create a form element for this, since the form logic is + // is inverted to show enabled. + $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..d64bab9 --- /dev/null +++ b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php @@ -0,0 +1,152 @@ + 2) + * @endcode + * + * @return $this + */ + public function setRouteParameters(array $route_parameters); + + /** + * Gets the external URL. + * + * @return string|NULL + * Returns the external URL if the menu link points to an external URL, + * otherwise NULL. + */ + public function getUrl(); + + /** + * Gets the url object pointing to the URL of the menu link content entity. + * + * @return \Drupal\Core\Url + * A Url object instance. + */ + public function getUrlObject(); + + /** + * Gets the menu name of the custom menu link. + * + * @return string + * The menu ID. + */ + public function getMenuName(); + + /** + * Gets the options for the menu link content entity. + * + * @return array + * The options that may be passed to the URL generator. + */ + public function getOptions(); + + /** + * Sets the query options of the menu link content entity. + * + * @param array $options + * The new option. + * + * @return $this + */ + public function setOptions(array $options); + + /** + * Gets the description of the menu link for the UI. + * + * @return string + * The description to use on admin pages or as a title attribute. + */ + public function getDescription(); + + /** + * Gets the menu plugin ID associated with this entity. + * + * @return string + * The plugin ID. + */ + public function getPluginId(); + + /** + * Returns whether the menu link is marked as hidden. + * + * @return bool + * TRUE if is not enabled, otherwise FALSE. + */ + public function isHidden(); + + /** + * Returns whether the menu link is marked as always expanded. + * + * @return bool + * TRUE for expanded, FALSE otherwise. + */ + public function isExpanded(); + + /** + * Gets the plugin ID of the parent menu link. + * + * @return string + * A plugin ID, or empty string if this link is at the top level. + */ + public function getParentId(); + + /** + * Returns the weight of the menu link content entity. + * + * @return int + * A weight for use when ordering links. + */ + public function getWeight(); + +} 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..718e151 --- /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 access manager. + */ + 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..5781044 --- /dev/null +++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php @@ -0,0 +1,247 @@ + 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']; + // Builds a list of entity IDs to take advantage of the more efficient + // EntityStorageInterface::loadMultiple() in getEntity() at render time. + static::$entityIdsToLoad[$entity_id] = $entity_id; + } + + $this->entityManager = $entity_manager; + $this->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']; + // Make sure the current ID is in the list, which may include multiple + // IDs added earlier in each plugin's constructor. + static::$entityIdsToLoad[$entity_id] = $entity_id; + $entities = $storage->loadMultiple(array_values(static::$entityIdsToLoad)); + $entity = isset($entities[$entity_id]) ? $entities[$entity_id] : NULL; + static::$entityIdsToLoad = array(); + } + 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; + $the_entity = $this->entityManager->getTranslationFromContext($this->entity); + /** @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface $the_entity */ + $this->entity = $the_entity; + $this->entity->setInsidePlugin(); + } + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + // We only need to get the title from the actual entity if it may be a + // translation based on the current language context. This can only happen + // if the site is configured to be multilingual. + if ($this->langaugeManager->isMultilingual()) { + return $this->getEntity()->getTitle(); + } + return $this->pluginDefinition['title']; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + // We only need to get the description from the actual entity if it may be a + // translation based on the current language context. This can only happen + // if the site is configured to be multilingual. + if ($this->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 = 'menu_link_content'; + 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() { + $this->getEntity()->delete(); + } + +} diff --git a/core/modules/migrate/config/schema/migrate.schema.yml b/core/modules/migrate/config/schema/migrate.schema.yml index 9206904..cdca049 100644 --- a/core/modules/migrate/config/schema/migrate.schema.yml +++ b/core/modules/migrate/config/schema/migrate.schema.yml @@ -6,7 +6,7 @@ migrate.migration.*: mapping: id: type: string - label: 'ID' + lable: 'ID' label: type: label label: 'Label' diff --git a/core/modules/search/search.module b/core/modules/search/search.module index 6c041d0..449f3ec 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -83,7 +83,7 @@ function search_help($route_name, RouteMatchInterface $route_match) { $output .= '
    ' . t('Displaying the Search block') . '
    '; $output .= '
    ' . t('The Search module includes a block, which can be enabled and configured on the Block layout page, if you have the Block module enabled; the default block title is Search, and it is the Search form block in the Forms category, if you wish to add another instance. The block is available to users with the Use search permission, and it performs a search using the configured default search page.', array('!blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? \Drupal::url('block.admin_display') : '#', '!search_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-search')))) . '
    '; $output .= '
    ' . t('Searching your site') . '
    '; - $output .= '
    ' . t('Users with Use search permission can use the Search block and Search page. Users with the View published content permission can use configured search pages of type Content to search for content containing exact keywords; in addition, users with Use advanced search permission can use more complex search filtering. Users with the View user information permission can use configured search pages of type Users to search for active users containing the keyword anywhere in the user name, and users with the Administer users permission can search for active and blocked users, by email address or user name keyword.', array('!search' => \Drupal::url('search.view'), '!search_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-search')), '!node_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-node')), '!user_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-user')))) . '
    '; + $output .= '
    ' . t('Users with Use search permission can use the Search block and Search page. Users with the View published content permission can use configured search pages of type Content to search for content containing exact keywords; in addition, users with Use advanced search permission can use more complex search filtering. Users with the View user profiles permission can use configured search pages of type Users to search for active users containing the keyword anywhere in the user name, and users with the Administer users permission can search for active and blocked users, by email address or user name keyword.', array('!search' => \Drupal::url('search.view'), '!search_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-search')), '!node_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-node')), '!user_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-user')))) . '
    '; $output .= '
    ' . t('Extending the Search module') . '
    '; $output .= '
    ' . t('By default, the Search module only supports exact keyword matching in content searches. You can modify this behavior by installing a language-specific stemming module for your language (such as Porter Stemmer for American English), which allows words such as walk, walking, and walked to be matched in the Search module. Another approach is to use a third-party search technology with stemming or partial word matching features built in, such as Apache Solr or Sphinx. There are also contributed modules that provide additional search pages. These and other search-related contributed modules can be downloaded by visiting Drupal.org.', array('!contrib-search' => 'https://drupal.org/project/project_module?f[2]=im_vid_3%3A105', '!porterstemmer_url' => 'https://drupal.org/project/porterstemmer', '!solr_url' => 'https://drupal.org/project/apachesolr', '!sphinx_url' => 'https://drupal.org/project/sphinx')) . '
    '; $output .= ''; diff --git a/core/modules/system/core.api.php b/core/modules/system/core.api.php index 0503464..ecbb006 100644 --- a/core/modules/system/core.api.php +++ b/core/modules/system/core.api.php @@ -533,7 +533,6 @@ * $query \Drupal::entityQueryAggregate('your_entity_type'); * // Or: * $query = $query_service->getAggregate('your_entity_type'); - * @endcode * Also, you should use dependency injection to get this object if * possible; the service you need is entity.query, and its methods getQuery() * or getAggregateQuery() will get the query object. diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php index 4b5f785..f1645a7 100644 --- a/core/modules/system/src/Controller/FormAjaxController.php +++ b/core/modules/system/src/Controller/FormAjaxController.php @@ -70,7 +70,7 @@ public function content(Request $request) { * @throws Symfony\Component\HttpKernel\Exception\HttpExceptionInterface */ protected function getForm(Request $request) { - $form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $form_state = form_state_defaults(); $form_build_id = $request->request->get('form_build_id'); // Get the form from the cache. diff --git a/core/modules/system/src/Tests/Database/SelectTest.php b/core/modules/system/src/Tests/Database/SelectTest.php index 3579f83..996d269 100644 --- a/core/modules/system/src/Tests/Database/SelectTest.php +++ b/core/modules/system/src/Tests/Database/SelectTest.php @@ -20,9 +20,14 @@ class SelectTest extends DatabaseTestBase { */ function testSimpleSelect() { $query = db_select('test'); - $query->addField('test', 'name'); - $query->addField('test', 'age', 'age'); - $num_records = $query->countQuery()->execute()->fetchField(); + $name_field = $query->addField('test', 'name'); + $age_field = $query->addField('test', 'age', 'age'); + $result = $query->execute(); + + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); } @@ -32,16 +37,19 @@ function testSimpleSelect() { */ function testSimpleComment() { $query = db_select('test')->comment('Testing query comments'); - $query->addField('test', 'name'); - $query->addField('test', 'age', 'age'); + $name_field = $query->addField('test', 'name'); + $age_field = $query->addField('test', 'age', 'age'); $result = $query->execute(); - $records = $result->fetchAll(); + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } - $query = (string) $query; + $query = (string)$query; $expected = "/* Testing query comments */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; - $this->assertEqual(count($records), 4, 'Returned the correct number of rows.'); + $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); $this->assertEqual($query, $expected, 'The flattened query contains the comment string.'); } @@ -50,16 +58,19 @@ function testSimpleComment() { */ function testVulnerableComment() { $query = db_select('test')->comment('Testing query comments */ SELECT nid FROM {node}; --'); - $query->addField('test', 'name'); - $query->addField('test', 'age', 'age'); + $name_field = $query->addField('test', 'name'); + $age_field = $query->addField('test', 'age', 'age'); $result = $query->execute(); - $records = $result->fetchAll(); + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } - $query = (string) $query; + $query = (string)$query; $expected = "/* Testing query comments SELECT nid FROM {node}; -- */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; - $this->assertEqual(count($records), 4, 'Returned the correct number of rows.'); + $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); $this->assertEqual($query, $expected, 'The flattened query contains the sanitised comment string.'); } diff --git a/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php b/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php index 30380ac..e6bfdda 100644 --- a/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php +++ b/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php @@ -207,7 +207,7 @@ function testMultipleFalseOptionchecker() { */ private function formSubmitHelper($form, $edit) { $form_id = $this->randomName(); - $form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $form_state = form_state_defaults(); $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); // The form token CSRF protection should not interfere with this test, so we @@ -217,7 +217,7 @@ private function formSubmitHelper($form, $edit) { $form_state['input'] = $edit; $form_state['input']['form_id'] = $form_id; - \Drupal::formBuilder()->prepareForm($form_id, $form, $form_state); + drupal_prepare_form($form_id, $form, $form_state); drupal_process_form($form_id, $form, $form_state); diff --git a/core/modules/system/src/Tests/Form/FormCacheTest.php b/core/modules/system/src/Tests/Form/FormCacheTest.php index b33c749..301e5fc 100644 --- a/core/modules/system/src/Tests/Form/FormCacheTest.php +++ b/core/modules/system/src/Tests/Form/FormCacheTest.php @@ -32,7 +32,7 @@ public function setUp() { $this->form = array( '#property' => $this->randomName(), ); - $this->form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $this->form_state = form_state_defaults(); $this->form_state['example'] = $this->randomName(); } @@ -43,7 +43,7 @@ function testCacheToken() { \Drupal::currentUser()->setAccount(new UserSession(array('uid' => 1))); form_set_cache($this->form_build_id, $this->form, $this->form_state); - $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $cached_form_state = form_state_defaults(); $cached_form = form_get_cache($this->form_build_id, $cached_form_state); $this->assertEqual($this->form['#property'], $cached_form['#property']); $this->assertTrue(!empty($cached_form['#cache_token']), 'Form has a cache token'); @@ -53,14 +53,14 @@ function testCacheToken() { // Change the private key. (We cannot change the session ID because this // will break the parent site test runner batch.) \Drupal::state()->set('system.private_key', 'invalid'); - $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $cached_form_state = form_state_defaults(); $cached_form = form_get_cache($this->form_build_id, $cached_form_state); $this->assertFalse($cached_form, 'No form returned from cache'); $this->assertTrue(empty($cached_form_state['example'])); // Test that loading the cache with a different form_id fails. $wrong_form_build_id = $this->randomName(9); - $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $cached_form_state = form_state_defaults(); $this->assertFalse(form_get_cache($wrong_form_build_id, $cached_form_state), 'No form returned from cache'); $this->assertTrue(empty($cached_form_state['example']), 'Cached form state was not loaded'); } @@ -74,7 +74,7 @@ function testNoCacheToken() { $this->form_state['example'] = $this->randomName(); form_set_cache($this->form_build_id, $this->form, $this->form_state); - $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $cached_form_state = form_state_defaults(); $cached_form = form_get_cache($this->form_build_id, $cached_form_state); $this->assertEqual($this->form['#property'], $cached_form['#property']); $this->assertTrue(empty($cached_form['#cache_token']), 'Form has no cache token'); diff --git a/core/modules/system/src/Tests/Form/FormTest.php b/core/modules/system/src/Tests/Form/FormTest.php index 88acb40..a380bd7 100644 --- a/core/modules/system/src/Tests/Form/FormTest.php +++ b/core/modules/system/src/Tests/Form/FormTest.php @@ -101,7 +101,7 @@ function testRequiredFields() { foreach (array(TRUE, FALSE) as $required) { $form_id = $this->randomName(); $form = array(); - $form_state = \Drupal::formBuilder()->getFormStateDefaults(); + $form_state = form_state_defaults(); $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); $element = $data['element']['#title']; $form[$element] = $data['element']; @@ -112,7 +112,7 @@ function testRequiredFields() { // The form token CSRF protection should not interfere with this test, // so we bypass it by setting the token to FALSE. $form['#token'] = FALSE; - \Drupal::formBuilder()->prepareForm($form_id, $form, $form_state); + drupal_prepare_form($form_id, $form, $form_state); drupal_process_form($form_id, $form, $form_state); $errors = form_get_errors($form_state); // Form elements of type 'radios' throw all sorts of PHP notices 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..0eb6f42 --- /dev/null +++ b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php @@ -0,0 +1,151 @@ + '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 = $this->container->get('menu.link_tree'); + $this->menuLinkManager = $this->container->get('plugin.manager.menu.link'); + } + + /** + * Tests deleting all the links in a menu. + */ + 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->load('menu1', new MenuTreeParameters()); + $this->assertEqual(count($output), 2); + $output = $this->linkTree->load('menu2', new MenuTreeParameters()); + $this->assertEqual(count($output), 1); + + $this->menuLinkManager->deleteLinksInMenu('menu1'); + + $output = $this->linkTree->load('menu1', new MenuTreeParameters()); + $this->assertEqual(count($output), 0); + + $output = $this->linkTree->load('menu2', new MenuTreeParameters()); + $this->assertEqual(count($output), 1); + } + + /** + * Tests creating links with an expected tree structure. + */ + public function testCreateLinksInMenu() { + /** + * This creates a tree with the following structure: + * - 1 + * - 2 + * - 3 + * - 4 + * - 5 + * - 7 + * - 6 + * - 8 + * + * With link 6 being the only external link. + */ + $links = array( + 1 => MenuLinkMock::create(array('id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '')), + 2 => MenuLinkMock::create(array('id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => 'test.example1', 'route_parameters' => array('foo' => 'bar'))), + 3 => MenuLinkMock::create(array('id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'route_parameters' => array('baz' => 'qux'))), + 4 => MenuLinkMock::create(array('id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3')), + 5 => MenuLinkMock::create(array('id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '')), + 6 => MenuLinkMock::create(array('id' => 'test.example6', 'route_name' => '', 'url' => 'https://drupal.org/', 'title' => 'barbar', 'parent' => '')), + 7 => MenuLinkMock::create(array('id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => '')), + 8 => MenuLinkMock::create(array('id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '')), + ); + foreach ($links as $instance) { + $this->menuLinkManager->addDefinition($instance->getPluginId(), $instance->getPluginDefinition()); + } + $parameters = new MenuTreeParameters(); + $tree = $this->linkTree->load('mock', $parameters); + + $count = function(array $tree) { + $sum = function ($carry, MenuLinkTreeElement $item) { + return $carry + $item->count(); + }; + return array_reduce($tree, $sum); + }; + + $this->assertEqual($count($tree), 8); + $parameters = new MenuTreeParameters(); + $parameters->setRoot('test.example2'); + $tree = $this->linkTree->load($instance->getMenuName(), $parameters); + $top_link = reset($tree); + $this->assertEqual(count($top_link->subtree), 1); + $child = reset($top_link->subtree); + $this->assertEqual($child->link->getPluginId(), $links[3]->getPluginId()); + $height = $this->linkTree->getSubtreeHeight('test.example2'); + $this->assertEqual($height, 3); + } + +} 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..8064984 --- /dev/null +++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php @@ -0,0 +1,398 @@ + 'Menu tree storage tests', + 'description' => 'Tests menu tree storage tests', + 'group' => 'Menu', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->treeStorage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), 'menu_tree'); + $this->connection = $this->container->get('database'); + $this->installEntitySchema('menu_link_content'); + } + + /** + * 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('cache.menu'), '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 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. + //