diff --git a/core/modules/menu/lib/Drupal/menu/Controller/MenuController.php b/core/modules/menu/lib/Drupal/menu/Controller/MenuController.php new file mode 100644 index 0000000..f707dfb --- /dev/null +++ b/core/modules/menu/lib/Drupal/menu/Controller/MenuController.php @@ -0,0 +1,101 @@ +menuLinkStorage = $menu_link_storage; + $this->entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.entity')->getStorageController('menu_link'), + $container->get('plugin.manager.entity') + ); + } + + /** + * Gets all the available menus and menu items as a JavaScript array. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request of the page. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * The available menu and menu items. + */ + public function getParentOptions(Request $request) { + $available_menus = array(); + if ($menus = $request->request->get('menus')) { + foreach ($menus as $menu) { + $available_menus[$menu] = $menu; + } + } + $options = _menu_get_options(menu_get_menus(), $available_menus, array('mlid' => 0)); + + return new JsonResponse($options); + } + + /** + * Provides the menu link submission form. + * + * @param \Drupal\system\MenuInterface $menu + * An entity representing a custom menu. + * + * @return array + * Returns the menu link submission form. + */ + public function addLink(MenuInterface $menu) { + // @todo Remove this when https://drupal.org/node/1981644 is in. + drupal_set_title(t('Add menu link')); + $menu_link = $this->menuLinkStorage->create(array( + 'mlid' => 0, + 'plid' => 0, + 'menu_name' => $menu->id(), + )); + return $this->entityManager->getForm($menu_link); + } + +} diff --git a/core/modules/menu/lib/Drupal/menu/MenuFormController.php b/core/modules/menu/lib/Drupal/menu/MenuFormController.php index a709d2e..de05e1a 100644 --- a/core/modules/menu/lib/Drupal/menu/MenuFormController.php +++ b/core/modules/menu/lib/Drupal/menu/MenuFormController.php @@ -2,14 +2,18 @@ /** * @file - * Contains Drupal\menu\MenuFormController. + * Contains \Drupal\menu\MenuFormController. */ namespace Drupal\menu; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityControllerInterface; use Drupal\Core\Entity\EntityFormController; +use Drupal\Core\Entity\Query\QueryFactory; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\Language; +use Drupal\menu_link\MenuLinkStorageControllerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -18,12 +22,71 @@ class MenuFormController extends EntityFormController implements EntityControllerInterface { /** + * The module handler to invoke hooks on. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The factory for entity queries. + * + * @var \Drupal\Core\Entity\Query\QueryFactory + */ + protected $entityQueryFactory; + + /** + * The menu link storage controller. + * + * @var \Drupal\menu_link\MenuLinkStorageControllerInterface + */ + protected $storageController; + + /** + * The overview tree form. + * + * @var array + */ + protected $overviewTreeForm = array('#tree' => TRUE); + + /** + * Constructs a MenuFormController object. + * + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke hooks on. + * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query_factory + * The factory for entity queries. + * @param \Drupal\menu_link\MenuLinkStorageControllerInterface $storage_controller + * The menu link storage controller. + */ + public function __construct(ModuleHandlerInterface $module_handler, QueryFactory $entity_query_factory, MenuLinkStorageControllerInterface $storage_controller) { + $this->moduleHandler = $module_handler; + $this->entityQueryFactory = $entity_query_factory; + $this->storageController = $storage_controller; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { + return new static( + $container->get('module_handler'), + $container->get('entity.query'), + $container->get('plugin.manager.entity')->getStorageController('menu_link') + ); + } + + /** * Overrides Drupal\Core\Entity\EntityFormController::form(). */ public function form(array $form, array &$form_state) { $menu = $this->entity; + + if ($this->operation == 'edit') { + drupal_set_title(t('Edit menu %label', array('%label' => $menu->label())), PASS_THROUGH); + } + $system_menus = menu_list_system_menus(); - $form_state['menu'] = &$menu; $form['label'] = array( '#type' => 'textfield', @@ -39,7 +102,7 @@ public function form(array $form, array &$form_state) { '#description' => t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'), '#field_prefix' => $menu->isNew() ? 'menu-' : '', '#machine_name' => array( - 'exists' => 'menu_edit_menu_name_exists', + 'exists' => array($this, 'menuNameExists'), 'source' => array('label'), 'replace_pattern' => '[^a-z0-9-]+', 'replace' => '-', @@ -83,17 +146,36 @@ public function form(array $form, array &$form_state) { // within forms, but does not allow to handle the form section's submission // equally separated yet. Therefore, we use a $form_state key to point to // the parents of the form section. - // @see menu_overview_form_submit() + // @see self::submitOverviewForm() $form_state['menu_overview_form_parents'] = array('links'); $form['links'] = array(); - $form['links'] = menu_overview_form($form['links'], $form_state); + $form['links'] = $this->buildOverviewForm($form['links'], $form_state); } return parent::form($form, $form_state); } /** - * Overrides Drupal\Core\Entity\EntityFormController::actions(). + * Returns whether a menu name already exists. + * + * @param string $value + * The name of the menu. + * + * @return bool + * Returns TRUE if the menu already exists. + * + * @see form_validate_machine_name() + */ + public function menuNameExists($value) { + $custom_exists = $this->entityQueryFactory->get('menu')->condition('id', $value)->range(0, 1)->count()->execute(); + // 'menu-' is added to the menu name to avoid name-space conflicts. + $link_exists = $this->entityQueryFactory->get('menu_link')->condition('menu_name', 'menu-' . $value)->range(0, 1)->count()->execute(); + + return $custom_exists || $link_exists; + } + + /** + * {@inheritdoc} */ protected function actions(array $form, array &$form_state) { $actions = parent::actions($form, $form_state); @@ -142,14 +224,14 @@ public function languageConfigurationSubmit(array &$form, array &$form_state) { } /** - * Overrides Drupal\Core\Entity\EntityFormController::save(). + * {@inheritdoc} */ public function save(array $form, array &$form_state) { $menu = $this->entity; $system_menus = menu_list_system_menus(); if (!$menu->isNew() || isset($system_menus[$menu->id()])) { - menu_overview_form_submit($form, $form_state); + $this->submitOverviewForm($form, $form_state); } $status = $menu->save(); @@ -168,11 +250,201 @@ public function save(array $form, array &$form_state) { } /** - * Overrides Drupal\Core\Entity\EntityFormController::delete(). + * {@inheritdoc} */ public function delete(array $form, array &$form_state) { - $menu = $this->entity; - $form_state['redirect'] = 'admin/structure/menu/manage/' . $menu->id() . '/delete'; + $form_state['redirect'] = 'admin/structure/menu/manage/' . $this->entity->id() . '/delete'; + } + + /** + * Form constructor to edit an entire menu tree at once. + * + * Shows for one menu the menu links accessible to the current user and + * relevant operations. + * + * This form constructor can be integrated as a section into another form. It + * relies on the following keys in $form_state: + * - menu: A loaded menu definition, as returned by menu_load(). + * - menu_overview_form_parents: An array containing the parent keys to this + * form. + * Forms integrating this section should call menu_overview_form_submit() from + * their form submit handler. + */ + protected function buildOverviewForm(array &$form, array &$form_state) { + global $menu_admin; + + // Ensure that menu_overview_form_submit() knows the parents of this form + // section. + $form['#tree'] = TRUE; + $form['#theme'] = 'menu_overview_form'; + $form_state += array('menu_overview_form_parents' => array()); + + $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css'); + + $links = array(); + $query = $this->entityQueryFactory->get('menu_link') + ->condition('menu_name', $this->entity->id()); + for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { + $query->sort('p' . $i, 'ASC'); + } + $result = $query->execute(); + + if (!empty($result)) { + $links = $this->storageController->load($result); + } + + $delta = max(count($links), 50); + $tree = menu_tree_data($links); + $node_links = array(); + menu_tree_collect_node_links($tree, $node_links); + // We indicate that a menu administrator is running the menu access check. + $menu_admin = TRUE; + menu_tree_check_access($tree, $node_links); + $menu_admin = FALSE; + + $form = array_merge($form, $this->buildOverviewTreeForm($tree, $delta)); + $form['#empty_text'] = t('There are no menu links yet. Add link.', array('@link' => url('admin/structure/menu/manage/' . $this->entity->id() .'/add'))); + + return $form; + } + + /** + * Recursive helper function for buildOverviewForm(). + * + * @param $tree + * The menu_tree retrieved by menu_tree_data. + * @param $delta + * The default number of menu items used in the menu weight selector is 50. + * + * @return array + * The overview tree form. + */ + protected function buildOverviewTreeForm($tree, $delta) { + $form = &$this->overviewTreeForm; + foreach ($tree as $data) { + $item = $data['link']; + // Don't show callbacks; these have $item['hidden'] < 0. + if ($item && $item['hidden'] >= 0) { + $mlid = 'mlid:' . $item['mlid']; + $form[$mlid]['#item'] = $item; + $form[$mlid]['#attributes'] = $item['hidden'] ? array('class' => array('menu-disabled')) : array('class' => array('menu-enabled')); + $form[$mlid]['title']['#markup'] = l($item['title'], $item['href'], $item['localized_options']); + if ($item['hidden']) { + $form[$mlid]['title']['#markup'] .= ' (' . t('disabled') . ')'; + } + elseif ($item['link_path'] == 'user' && $item['module'] == 'system') { + $form[$mlid]['title']['#markup'] .= ' (' . t('logged in users only') . ')'; + } + + $form[$mlid]['hidden'] = array( + '#type' => 'checkbox', + '#title' => t('Enable @title menu link', array('@title' => $item['title'])), + '#title_display' => 'invisible', + '#default_value' => !$item['hidden'], + ); + $form[$mlid]['weight'] = array( + '#type' => 'weight', + '#delta' => $delta, + '#default_value' => $item['weight'], + '#title_display' => 'invisible', + '#title' => t('Weight for @title', array('@title' => $item['title'])), + ); + $form[$mlid]['mlid'] = array( + '#type' => 'hidden', + '#value' => $item['mlid'], + ); + $form[$mlid]['plid'] = array( + '#type' => 'hidden', + '#default_value' => $item['plid'], + ); + // Build a list of operations. + $operations = array(); + $links = array(); + $links['edit'] = array( + 'title' => t('Edit'), + 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/edit', + ); + $operations['edit'] = array('#type' => 'link', '#title' => t('Edit'), '#href' => 'admin/structure/menu/item/' . $item['mlid'] . '/edit'); + // Only items created by the menu module can be deleted. + if ($item['module'] == 'menu' || $item['updated'] == 1) { + $links['delete'] = array( + 'title' => t('Delete'), + 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/delete', + ); + $operations['delete'] = array('#type' => 'link', '#title' => t('Delete'), '#href' => 'admin/structure/menu/item/' . $item['mlid'] . '/delete'); + } + // Set the reset column. + elseif ($item['module'] == 'system' && $item['customized']) { + $links['reset'] = array( + 'title' => t('Reset'), + 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/reset', + ); + $operations['reset'] = array('#type' => 'link', '#title' => t('Reset'), '#href' => 'admin/structure/menu/item/' . $item['mlid'] . '/reset'); + } + $form[$mlid]['operations'] = array( + '#type' => 'operations', + '#links' => $links, + ); + } + + if ($data['below']) { + $this->buildOverviewTreeForm($data['below'], $delta); + } + } + return $form; + } + + /** + * Submit handler for the menu overview form. + * + * This function takes great care in saving parent items first, then items + * underneath them. Saving items in the incorrect order can break the menu tree. + */ + protected function submitOverviewForm(array $complete_form, array &$form_state) { + // Form API supports constructing and validating self-contained sections + // within forms, but does not allow to handle the form section's submission + // equally separated yet. Therefore, we use a $form_state key to point to + // the parents of the form section. + $parents = $form_state['menu_overview_form_parents']; + $input = NestedArray::getValue($form_state['input'], $parents); + $form = &NestedArray::getValue($complete_form, $parents); + + // When dealing with saving menu items, the order in which these items are + // saved is critical. If a changed child item is saved before its parent, + // the child item could be saved with an invalid path past its immediate + // parent. To prevent this, save items in the form in the same order they + // are sent, ensuring parents are saved first, then their children. + // See http://drupal.org/node/181126#comment-632270 + $order = is_array($input) ? array_flip(array_keys($input)) : array(); + // Update our original form with the new order. + $form = array_intersect_key(array_merge($order, $form), $form); + + $updated_items = array(); + $fields = array('weight', 'plid'); + foreach (element_children($form) as $mlid) { + if (isset($form[$mlid]['#item'])) { + $element = $form[$mlid]; + // Update any fields that have changed in this menu item. + foreach ($fields as $field) { + if ($element[$field]['#value'] != $element[$field]['#default_value']) { + $element['#item'][$field] = $element[$field]['#value']; + $updated_items[$mlid] = $element['#item']; + } + } + // Hidden is a special case, the value needs to be reversed. + if ($element['hidden']['#value'] != $element['hidden']['#default_value']) { + // Convert to integer rather than boolean due to PDO cast to string. + $element['#item']['hidden'] = $element['hidden']['#value'] ? 0 : 1; + $updated_items[$mlid] = $element['#item']; + } + } + } + + // Save all our changed items to the database. + foreach ($updated_items as $item) { + $item['customized'] = 1; + $item->save(); + } } } diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php index b914837..48e57eb 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php @@ -188,7 +188,7 @@ function deleteCustomMenu($menu) { $this->assertFalse($result, 'All menu links associated to the custom menu were deleted.'); // Make sure there's no delete button on system menus. - $this->drupalGet('admin/structure/menu/manage/main/edit'); + $this->drupalGet('admin/structure/menu/manage/main'); $this->assertNoRaw('edit-delete', 'The delete button was not found'); // Try to delete the main menu. @@ -375,7 +375,7 @@ function testSystemMenuRename() { $edit = array( 'label' => $this->randomName(16), ); - $this->drupalPost('admin/structure/menu/manage/main/edit', $edit, t('Save')); + $this->drupalPost('admin/structure/menu/manage/main', $edit, t('Save')); // Make sure menu shows up with new name in block addition. $default_theme = variable_get('theme_default', 'stark'); diff --git a/core/modules/menu/menu.admin.inc b/core/modules/menu/menu.admin.inc index 7346712..2fe9455 100644 --- a/core/modules/menu/menu.admin.inc +++ b/core/modules/menu/menu.admin.inc @@ -5,254 +5,6 @@ * Administrative page callbacks for menu module. */ -use Drupal\menu_link\Plugin\Core\Entity\MenuLink; -use Drupal\system\Plugin\Core\Entity\Menu; -use Drupal\Component\Utility\NestedArray; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Menu callback which shows an overview page of all the custom menus and their descriptions. - */ -function menu_overview_page() { - return Drupal::entityManager() - ->getListController('menu') - ->render(); -} - -/** - * Page callback: Presents the menu creation form. - * - * @return array - * A form array as expected by drupal_render(). - * - * @see menu_menu() - */ -function menu_menu_add() { - $menu = entity_create('menu', array()); - return Drupal::entityManager()->getForm($menu); -} - -/** - * Page callback: Presents the menu edit form. - * - * @param \Drupal\system\Plugin\Core\Entity\Menu $menu - * The menu to edit. - * - * @return array - * A form array as expected by drupal_render(). - * - * @see menu_menu() - */ -function menu_menu_edit(Menu $menu) { - drupal_set_title(t('Edit menu %label', array('%label' => $menu->label())), PASS_THROUGH); - return Drupal::entityManager()->getForm($menu); -} - -/** - * Form constructor to edit an entire menu tree at once. - * - * Shows for one menu the menu links accessible to the current user and - * relevant operations. - * - * This form constructor can be integrated as a section into another form. It - * relies on the following keys in $form_state: - * - menu: A loaded menu definition, as returned by menu_load(). - * - menu_overview_form_parents: An array containing the parent keys to this - * form. - * Forms integrating this section should call menu_overview_form_submit() from - * their form submit handler. - */ -function menu_overview_form($form, &$form_state) { - global $menu_admin; - - // Ensure that menu_overview_form_submit() knows the parents of this form - // section. - $form['#tree'] = TRUE; - $form['#theme'] = 'menu_overview_form'; - $form_state += array('menu_overview_form_parents' => array()); - - $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css'); - - $links = array(); - $query = Drupal::entityQuery('menu_link') - ->condition('menu_name', $form_state['menu']->id()); - for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { - $query->sort('p' . $i, 'ASC'); - } - $result = $query->execute(); - - if (!empty($result)) { - $links = menu_link_load_multiple($result); - } - - $delta = max(count($links), 50); - $tree = menu_tree_data($links); - $node_links = array(); - menu_tree_collect_node_links($tree, $node_links); - // We indicate that a menu administrator is running the menu access check. - $menu_admin = TRUE; - menu_tree_check_access($tree, $node_links); - $menu_admin = FALSE; - - // Inline the "Add link" action so it displays right above the table of - // links. No access check needed, since this form has the same access - // restriction as adding menu items to the menu. - $form['inline_actions'] = array( - '#prefix' => '