monster_menus-9.0.x-dev/src/Routing/RouteSubscriber.php
src/Routing/RouteSubscriber.php
<?php
/**
* @file
* Contains \Drupal\monster_menus\Routing\RouteSubscriber.
*/
namespace Drupal\monster_menus\Routing;
use Drupal\Core\Database\Database;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\monster_menus\Constants;
use Symfony\Component\Routing\RouteCollection;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\Core\Url;
use Drupal\Component\Serialization\Yaml;
/**
* Listens to dynamic route events. Alters existing menu routes to include
* special handing for MM.
*/
class RouteSubscriber extends RouteSubscriberBase {
use MessengerTrait;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -9999]; // negative value means "late"
return $events;
}
/**
* {@inheritdoc}
*/
public function alterRoutes(RouteCollection $collection) {
mm_module_invoke_all_array('mm_routing_alter', [&$collection]);
// Alter routes based on transformations found in ::alterations().
$alterations = Yaml::decode($this->alterations());
foreach ($alterations as $name => $changes) {
if ($route = $collection->get($name)) {
// Read current options and hold for future alterations.
$options = $route->getOptions();
if (isset($changes['options'])) {
foreach (array_keys($options) as $key) {
if ($key[0] != '_') {
unset($options[$key]);
}
}
}
// Alter defaults.
if (isset($changes['defaults'])) {
$route->setDefaults($changes['defaults']);
}
// Alter requirements.
$bare_path = $route->getPath();
if (isset($changes['requirements'])) {
if (str_contains($bare_path, '{node}')) {
$changes['requirements']['node'] = '\d+';
}
$route->setRequirements($changes['requirements']);
$route->setMethods(['GET', 'POST']);
}
// Alter options.
if (isset($changes['options'])) {
$options = $changes['options'] + $options;
}
// Alter path, adding MM's prefix.
$route->setPath('/mm/{mm_tree}' . $bare_path);
// Add parameter converters, as needed.
$params = $options['parameters'] ?? [];
$params['mm_tree'] = ['type' => 'entity:mm_tree', 'converter' => 'paramconverter.entity'];
if (str_contains($bare_path, '{node}') && !isset($params['node'])) {
$params['node'] = ['type' => 'entity:node', 'converter' => 'paramconverter.entity'];
$route->setRequirement('node', '\d+');
}
if (str_contains($bare_path, '{node_revision}') && !isset($params['node_revision'])) {
$params['node_revision'] = ['type' => 'entity_revision:node', 'converter' => 'paramconverter.entity_revision'];
$route->setRequirement('node_revision', '\d+');
}
$options['parameters'] = $params;
// Set final options.
$route->setOptions($options);
// Set the parameter requirement for {mm_tree}.
$route->setRequirement('mm_tree', '-?\d+');
}
}
// Alter remaining routes which contain {node}, prefixing them with
// "/mm/{mm_tree}".
foreach ($collection->all() as $name => $route) {
// Make sure this isn't already one of our routes.
// Skip the path if the requirement "_not_mm_path" is set.
if (strncmp($name, 'monster_menus.', 14) && !$route->getRequirement('_not_mm_path')) {
$route_path = $route->getPath();
if (!isset($alterations[$name]) && str_contains($route_path, '{node}') && !str_starts_with($route_path, '/mm/{mm_tree}') && !$this->noMMPrefix($route_path)) {
$route->setPath('/mm/{mm_tree}' . $route_path);
$route->setOption('_admin_route', FALSE);
}
}
}
// Modify some built-in routes to point to MM's equivalent.
$aliases = [
'node.add_page' => 'monster_menus.add_node',
'node.add' => 'monster_menus.add_node_with_type',
];
foreach ($aliases as $from => $to) {
if ($collection->get($from) && ($to_route = $collection->get($to))) {
$collection->remove($from);
$collection->add($from, clone $to_route);
}
}
// Generate the list of keywords that are not allowed in URL aliases, and
// give an error message if there already is something in mm_tree using one
// of the menu keywords.
$checked = $reserved_alias = $top_level_reserved = [];
foreach ($collection->all() as $name => $route) {
// Remove leading or trailing slashes, then squish any multiple slashes in
// a row.
$path = $route->getPath();
$elems = explode('/', preg_replace('{//+}', '/', trim($path, '/')));
if ($elems[0] && $elems[0][0] != '{' && $elems[0][0] != '<') {
$top_level_reserved[$elems[0]] = TRUE;
}
if (count($elems) >= 3 && $elems[0] == 'mm' && $elems[1] == '{mm_tree}') {
$failed_elems = [];
for ($i = 2; $i < count($elems); $i++) {
// Only reserve the first non-token after mm/{mm_tree}
if ($elems[$i][0] != '{') {
if (empty($reserved_alias[$elems[$i]])) {
if (!isset($checked[$elems[$i]])) {
$checked[$elems[$i]] = mm_content_get(['alias' => $elems[$i]], [], 10);
}
if (!empty($checked[$elems[$i]])) {
$failed_elems[] = $elems[$i];
}
$reserved_alias[$elems[$i]] = TRUE;
}
break;
}
}
foreach ($failed_elems as $elem) {
[$error, $list] = $this->addErrors($checked[$elem], 'The menu entry %entry contains the element %element. This conflicts with the URL names that are already assigned to these MM pages:<br />');
$error .= '<br />The menu entry has been disabled. You must change the URL name(s) and rebuild the menus.';
$err_arr = array_merge([
'%entry' => $path,
'%element' => $elem,
], $list);
if (\Drupal::currentUser()->hasPermission('administer all menus')) {
$this->messenger()->addError(t($error, $err_arr));
}
\Drupal::logger('mm')->error($error, $err_arr);
$collection->remove($name);
}
}
}
\Drupal::state()->set('monster_menus.reserved_alias', array_merge(array_keys($reserved_alias), mm_content_reserved_aliases_base()));
\Drupal::state()->set('monster_menus.top_level_reserved', array_keys($top_level_reserved));
// Emit an error message if there already is something in mm_tree that would
// match one of the system menu entries.
$db = Database::getConnection();
$found = [];
// Skip /mm/{mm_tree}...
$not_found = ['/mm/{mm_tree}' => 1];
foreach ($collection->all() as $name => $route) {
// Remove leading or trailing slashes, then squish any multiple slashes in
// a row.
$path = $route->getPath();
if ($path[0] == '<') {
continue;
}
$elems = explode('/', preg_replace('{//+}', '/', trim($path, '/')));
$test_path = '';
$query = $where = '';
$args = [':par0' => mm_home_mmtid()];
foreach ($elems as $depth => $elem) {
$test_path .= '/' . $elem;
if (!empty($not_found[$test_path]) || !$elem) {
break;
}
$prev = $depth - 1;
if ($elem[0] == '{') {
$query .= " INNER JOIN {mm_tree} t$depth ON t$depth.parent = t$prev.mmtid";
}
else {
$args[":a$depth"] = $elem;
if ($query) {
$query .= " INNER JOIN {mm_tree} t$depth ON t$depth.parent = t$prev.mmtid";
$where .= " AND t$depth.alias = :a$depth";
}
else {
$query = "FROM {mm_tree} t$depth";
$where = "WHERE t$depth.parent = :par0 AND t$depth.alias = :a$depth";
}
}
$is_last = $depth == count($elems) - 1;
if (empty($found[$test_path]) || $is_last) {
$result = $db->queryRange("SELECT t$depth.mmtid $query $where", 0, 10, $args)->fetchCol();
if ($result) {
if ($is_last) {
[$error, $list] = $this->addErrors($result, 'The menu entry %entry conflicts with these MM pages:<br />');
$error .= '<br />The menu entry has been disabled. Change either the URL name(s) or the menu path and rebuild the menus.';
$err_arr = array_merge(['%entry' => $path], $list);
if (\Drupal::currentUser()->hasPermission('administer all menus')) {
$this->messenger()->addError(t($error, $err_arr));
}
\Drupal::logger('mm')->error($error, $err_arr);
$collection->remove($name);
}
$found[$test_path] = 1;
}
else {
$not_found[$test_path] = 1;
break;
}
}
}
}
// Regenerate the list of MM tree entry names to hide from non-admin users
$hidden_names = [Constants::MM_ENTRY_NAME_DEFAULT_USER, Constants::MM_ENTRY_NAME_DISABLED_USER];
$hidden_names = array_merge($hidden_names, mm_module_invoke_all('mm_hidden_user_names'));
\Drupal::state()->set('monster_menus.hidden_user_names', $hidden_names);
// Find the path of the contextual.render route, for processing in
// mm_active_menu_item().
$path = '';
if ($route = $collection->get('contextual.render')) {
$path = $route->getPath();
}
\Drupal::state()->set('monster_menus.contextual_render_path', $path);
// Regenerate the custom page display list
_mm_showpage_router(TRUE);
}
private function addErrors($pages, $error) {
$list = [];
foreach ($pages as $index => $tree) {
$error .= '<a href=":link' . $index . '">@title' . $index . '</a>' . ($index > 0 ? '<br />' : '');
$list['@title' . $index] = ' ' . mm_content_get_name($tree);
$list[':link' . $index] = Url::fromRoute('monster_menus.handle_page_settings', ['mm_tree' => is_object($tree) ? $tree->mmtid : $tree])->toString();
}
return [$error, $list];
}
private function noMMPrefix($path) {
$skips = ['/history/' => 9];
foreach ($skips as $prefix => $len) {
if (!strncmp($path, $prefix, $len)) {
return TRUE;
}
}
return FALSE;
}
private function alterations() {
return <<<YAML
entity.node.edit_form:
defaults:
_title_callback: '\Drupal\monster_menus\Controller\DefaultController::editNodeGetTitle'
_controller: '\Drupal\monster_menus\Controller\DefaultController::editNode'
options:
_admin_route: FALSE
entity.node.delete_form:
defaults:
_title_callback: '\Drupal\monster_menus\Form\DeleteNodeConfirmForm::getMenuTitle'
_form: \Drupal\monster_menus\Form\DeleteNodeConfirmForm
requirements:
_custom_access: '\Drupal\monster_menus\Form\DeleteNodeConfirmForm::access'
options:
_admin_route: FALSE
entity.node.preview: {}
entity.node.version_history:
defaults:
_title: Revisions
_controller: '\Drupal\monster_menus\Controller\NodeRevisionsController::revisionOverview'
requirements:
_custom_access: '\Drupal\monster_menus\Controller\NodeRevisionsController::menuAccessNodeRevisions'
options:
_admin_route: FALSE
node.revision_revert_confirm:
defaults:
_title: 'Revert to earlier revision'
_controller: '\Drupal\monster_menus\Controller\NodeRevisionsController::revisionRevertConfirm'
requirements:
_custom_access: '\Drupal\monster_menus\Controller\NodeRevisionsController::menuAccessNodeRevisions'
options:
_admin_route: FALSE
node.revision_delete_confirm:
defaults:
op: delete
_title: 'Delete earlier revision'
_controller: '\Drupal\monster_menus\Controller\NodeRevisionsController::revisionDeleteConfirm'
requirements:
_custom_access: '\Drupal\monster_menus\Controller\NodeRevisionsController::menuAccessNodeRevisions'
options:
_admin_route: FALSE
entity.node.revision:
defaults:
_title_callback: '\Drupal\\node\Controller\NodeController::revisionPageTitle'
_controller: '\Drupal\monster_menus\Controller\NodeRevisionsController::revisionShow'
requirements:
_custom_access: '\Drupal\monster_menus\Controller\NodeRevisionsController::menuAccessNodeRevisions'
diff.revisions_diff:
defaults:
op: compare
_title: 'Compare revisions'
_controller: '\Drupal\monster_menus\Controller\NodeRevisionsController::compareRevisions'
requirements:
_custom_access: '\Drupal\monster_menus\Controller\NodeRevisionsController::menuAccessNodeRevisions'
entity.comment.canonical: {}
entity.comment.delete_form:
defaults:
_title: 'Delete comment'
_entity_form: 'comment.delete'
entity.comment.edit_form:
defaults:
_title: 'Edit comment'
_entity_form: 'comment.default'
comment.reply:
defaults:
_title: 'Reply to comment'
_controller: '\Drupal\comment\Controller\CommentController::getReplyForm'
_entity_form: 'comment.default'
pid: null
YAML;
}
}
