diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php index 2e2ff6b..ef7ca47 100644 --- a/core/lib/Drupal/Core/Routing/CompiledRoute.php +++ b/core/lib/Drupal/Core/Routing/CompiledRoute.php @@ -56,7 +56,8 @@ class CompiledRoute extends SymfonyCompiledRoute { * @param array $tokens * An array of tokens to use to generate URL for this route * @param array $pathVariables - * An array of path variables + * An array of path variable names where the numeric array keys correspond + * to the path part where the named variable is found in the path pattern. * @param string|null $hostRegex * Host regex * @param array $hostTokens @@ -143,6 +144,27 @@ public function getRequirements() { } /** + * Returns the path variables. + * + * The Drupal implementation differs from the parent class in that the numeric + * array keys correspond to a matching path part containing that variable. + * For example, for a route with path + * @code + * /node/{node}/revisions/{node_revision}/view + * @endcode + * this method will return the array + * @code + * [1 => 'node', 3 => 'node_revision'] + * @endcode + * + * @return array + * The path variable names keyed by numeric path part. + */ + public function getPathVariables() { + return parent::getPathVariables(); + } + + /** * {@inheritdoc} */ public function serialize() { diff --git a/core/lib/Drupal/Core/Routing/RouteBuilder.php b/core/lib/Drupal/Core/Routing/RouteBuilder.php index a78cf0c..c88a033 100644 --- a/core/lib/Drupal/Core/Routing/RouteBuilder.php +++ b/core/lib/Drupal/Core/Routing/RouteBuilder.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Routing; use Drupal\Component\Discovery\YamlDiscovery; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Access\CheckProviderInterface; use Drupal\Core\Controller\ControllerResolverInterface; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -174,7 +175,9 @@ public function rebuild() { 'condition' => '', ); - $route = new Route($route_info['path'], $route_info['defaults'], $route_info['requirements'], $route_info['options'], $route_info['host'], $route_info['schemes'], $route_info['methods'], $route_info['condition']); + // Lowercase the path here so that the events get a consistent path, + // even though we force them all to be lowercase later. + $route = new Route(Unicode::strtolower($route_info['path']), $route_info['defaults'], $route_info['requirements'], $route_info['options'], $route_info['host'], $route_info['schemes'], $route_info['methods'], $route_info['condition']); $collection->add($name, $route); } } @@ -182,11 +185,15 @@ public function rebuild() { // DYNAMIC is supposed to be used to add new routes based upon all the // static defined ones. $this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection)); + // Process the whole collection since we cannot tell what was newly added. + $this->lowercaseCollection($collection); // ALTER is the final step to alter all the existing routes. We cannot stop // people from adding new routes here, but we define two separate steps to // make it clear. $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection)); + // Process the whole collection since we cannot tell what was changed. + $this->lowercaseCollection($collection); $this->checkProvider->setChecks($collection); @@ -223,6 +230,20 @@ public function destruct() { } /** + * Lowercases the path for each route in the collection. + * + * @param \Symfony\Component\Routing\RouteCollection $collection + * A route collection. + */ + protected function lowercaseCollection(RouteCollection $collection) { + foreach ($collection->all() as $route) { + // Force each path to be lowercase. + $path = Unicode::strtolower($route->getPath()); + $route->setPath($path); + } + } + + /** * Retrieves all defined routes from .routing.yml files. * * @return array diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index 639feff..ece32e2 100644 --- a/core/lib/Drupal/Core/Routing/RouteCompiler.php +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -55,7 +55,7 @@ public static function compile(Route $route) { $symfony_compiled->getStaticPrefix(), $symfony_compiled->getRegex(), $symfony_compiled->getTokens(), - $symfony_compiled->getPathVariables(), + static::getMappedPathVariables($route->getPath(), $symfony_compiled->getPathVariables()), $symfony_compiled->getHostRegex(), $symfony_compiled->getHostTokens(), $symfony_compiled->getHostVariables(), @@ -64,6 +64,36 @@ public static function compile(Route $route) { } /** + * Returns the mapping of route variables to numeric path part. + * + * Drupal only supports path wildcard (variables) that consist of a complete + * part of the path separated by slashes. + * + * @see \Symfony\Component\Routing\RouteCompiler::compilePattern() + * + * @param string $path + * The path for which we want the mapping. + * @param array $variables + * The names of placeholder variables in the path. + * + * @return array + * The mapping of numeric path part to variable name. + */ + public static function getMappedPathVariables($path, array $variables) { + $map = []; + // Split the path up on the slashes, ignoring multiple slashes in a row + // or leading or trailing slashes. + $parts = preg_split('@/+@', $path, NULL, PREG_SPLIT_NO_EMPTY); + foreach ($variables as $name) { + $index = array_search('{' . $name . '}', $parts, TRUE); + if ($index !== FALSE) { + $map[$index] = $name; + } + } + return $map; + } + + /** * Returns the pattern outline. * * The pattern outline is the path pattern but normalized so that all diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php index 937cff7..783b6ca 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -10,6 +10,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Drupal\Core\State\StateInterface; @@ -147,7 +148,9 @@ public function __construct(Connection $connection, StateInterface $state, Curre */ public function getRouteCollectionForRequest(Request $request) { // Cache both the system path as well as route parameters and matching - // routes. + // routes. We can not yet convert the path to lowercase since wildcard path + // portions may be case sensitive if they contain data like a base64 encoded + // token. $cid = 'route:' . $request->getPathInfo() . ':' . $request->getQueryString(); if ($cached = $this->cache->get($cid)) { $this->currentPath->setPath($cached->data['path'], $request); @@ -155,9 +158,9 @@ public function getRouteCollectionForRequest(Request $request) { return $cached->data['routes']; } else { - // Just trim on the right side. $path = $request->getPathInfo(); - $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); + // Just trim path on the right side. + $path = $path === '/' ? $path : rtrim($path, '/'); $path = $this->pathProcessor->processInbound($path, $request); $this->currentPath->setPath($path, $request); // Incoming path processors may also set query parameters. @@ -314,10 +317,12 @@ public function getRoutesByPattern($pattern) { } /** - * Get all routes which match a certain pattern. + * Get all routes which match a certain path pattern. + * + * The path will be converted to lowercase before performing the match. * * @param string $path - * The route pattern to search for (contains % as placeholders). + * The route path pattern to search for (contains % as placeholders). * * @return \Symfony\Component\Routing\RouteCollection * Returns a route collection of matching routes. @@ -325,7 +330,7 @@ public function getRoutesByPattern($pattern) { protected function getRoutesByPath($path) { // Split the path up on the slashes, ignoring multiple slashes in a row // or leading or trailing slashes. - $parts = preg_split('@/+@', $path, NULL, PREG_SPLIT_NO_EMPTY); + $parts = preg_split('@/+@', Unicode::strtolower($path), NULL, PREG_SPLIT_NO_EMPTY); $collection = new RouteCollection(); diff --git a/core/lib/Drupal/Core/Routing/UrlMatcher.php b/core/lib/Drupal/Core/Routing/UrlMatcher.php index 49bff8f..f802d2f 100644 --- a/core/lib/Drupal/Core/Routing/UrlMatcher.php +++ b/core/lib/Drupal/Core/Routing/UrlMatcher.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Routing; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Path\CurrentPathStack; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RouteCollection; @@ -46,4 +47,83 @@ public function finalMatch(RouteCollection $collection, Request $request) { return $this->match($this->currentPath->getPath($request)); } + /** + * Tries to match a URL with a set of routes. + * + * This version differs from the Symfony parent version in two respects. + * First, the $pathinfo string is converted to lowercase when matching the + * route path regular expression. In addition, we remove the check against any + * static prefix since we would already have matched the static prefix in + * \Drupal\Core\Routing\RouteProvider before arriving here. + * + * @param string $pathinfo + * The path to be parsed. + * @param \Symfony\Component\Routing\RouteCollection $routes + * The set of routes. + * + * @return array + * An array of route parameters. + * + * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException + * If the resource could not be found. + * @throws \Symfony\Component\Routing\Exception\MethodNotAllowedException + * If the resource was found but the request method is not allowed. + */ + protected function matchCollection($pathinfo, RouteCollection $routes) { + foreach ($routes as $name => $route) { + /** @var \Symfony\Component\Routing\Route $route */ + $compiledRoute = $route->compile(); + + // Convert the path to lowercase, so that we match the patterns from + // routes where we forced all paths to be lowercase. + // @see \Drupal\Core\Routing\RouteBuilder::rebuild() + // @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath() + if (!preg_match($compiledRoute->getRegex(), Unicode::strtolower($pathinfo), $matches)) { + continue; + } + // Recover the original value for wildcard (named variable) portions + // of the path, since they may be case-sensitive data like a base64 + // encoded token. We create this positional mapping in the route compiler. + // @see \Drupal\Core\Routing\RouteCompiler::getMappedPathVariables() + $parts = preg_split('@/+@', $pathinfo, NULL, PREG_SPLIT_NO_EMPTY); + foreach ($compiledRoute->getPathVariables() as $position => $variable_name) { + if (isset($matches[$variable_name])) { + $matches[$variable_name] = $parts[$position]; + } + } + + $hostMatches = array(); + if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { + continue; + } + + // Check the HTTP method requirement. + if ($requiredMethods = $route->getMethods()) { + // HEAD and GET are equivalent as per RFC. + $method = $this->context->getMethod(); + if ('HEAD' === $method) { + $method = 'GET'; + } + + if (!in_array($method, $requiredMethods)) { + $this->allow = array_merge($this->allow, $requiredMethods); + + continue; + } + } + + $status = $this->handleRouteRequirements($pathinfo, $name, $route); + + if (self::ROUTE_MATCH === $status[0]) { + return $status[1]; + } + + if (self::REQUIREMENT_MISMATCH === $status[0]) { + continue; + } + + return $this->getAttributes($route, $name, array_replace($matches, $hostMatches)); + } + } + } diff --git a/core/lib/Drupal/Core/Routing/routing.api.php b/core/lib/Drupal/Core/Routing/routing.api.php index 7c5e80a..f5a38e3 100644 --- a/core/lib/Drupal/Core/Routing/routing.api.php +++ b/core/lib/Drupal/Core/Routing/routing.api.php @@ -43,7 +43,7 @@ * by the machine name of the module that defines the route, or the name of * a subsystem. * - The 'path' line gives the URL path of the route (relative to the site's - * base URL). + * base URL). Paths are handled with case-insensitive matching. * - The 'defaults' section tells how to build the main content of the route, * and can also give other information, such as the page title and additional * arguments for the route controller method. There are several possibilities diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php index 13b56d5..387aeaa 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php @@ -8,6 +8,7 @@ namespace Drupal\config_translation\Tests; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\Unicode; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; @@ -96,7 +97,7 @@ public function testMapperListPage() { foreach ($labels as $label) { $test_entity = entity_create('config_test', array( - 'id' => $this->randomMachineName(), + 'id' => Unicode::strtolower($this->randomMachineName()), 'label' => $label, )); $test_entity->save(); diff --git a/core/modules/system/src/Tests/Menu/MenuRouterTest.php b/core/modules/system/src/Tests/Menu/MenuRouterTest.php index 42332e0..68b6657 100644 --- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Menu; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php index 17345fa..06a5119 100644 --- a/core/modules/system/src/Tests/Routing/RouterTest.php +++ b/core/modules/system/src/Tests/Routing/RouterTest.php @@ -211,6 +211,71 @@ public function testRouterMatching() { } /** + * Tests the case insensitivity of route paths and path variable handling. + * + * The un-routed URLs are constructed using base: so that we are sure they are + * used as written without being processed by the routing system. + */ + public function testRoutePathMixedCase() { + $this->drupalGet(Url::fromUri('base:router_test/TEST14/1')); + $this->assertResponse(200); + $this->assertText('User route "entity.user.canonical" was matched.'); + + /** @var \Drupal\Core\Routing\RouteProvider $route_provider */ + $route_provider = $this->container->get('router.route_provider'); + $route2 = $route_provider->getRouteByName('router_test.mixedcase.2'); + // Verify that the route path is stored as lowercase despite being defined + // with mixed case in the YAML file. + $this->assertIdentical('/router_test/mixedcase2', $route2->getPath()); + $this->drupalGet(Url::fromUri('base:router_test/mixEDCASE2')); + $this->assertResponse(200); + $this->assertRaw('test2', 'The expected controller output was found.'); + + $this->drupalGet(Url::fromUri('base:router_TEST/MIXEDcase2')); + $this->assertResponse(200); + $this->assertRaw('test2', 'The expected controller output was found.'); + + $this->drupalGet(Url::fromUri('base:router_test/mixedcase2')); + $this->assertResponse(200); + $this->assertRaw('test2', 'The expected controller output was found.'); + + $route3 = $route_provider->getRouteByName('router_test.mixedcase.3'); + // Verify that the variable name in the route path is stored as lowercase + // despite being defined with mixed case in the YAML file. + $this->assertIdentical('/router_test/mixedcase3/{value}', $route3->getPath()); + + // Verify that data in variable path parts is retained as the original, + // mixed-case value. Matches route path /router_test/mixedcase3/{value} + $this->drupalGet(Url::fromUri('base:router_test/mixedcase3/mixedCASEstring')); + $this->assertResponse(200); + $this->assertRaw('mixedCASEstring', 'The variable string was output with unmodified case.'); + + $this->drupalGet(Url::fromUri('base:router_test/mixedcase3/mixedcasestring')); + $this->assertResponse(200); + $this->assertRaw('mixedcasestring', 'The variable string was output with unmodified case.'); + + $this->drupalGet(Url::fromUri('base:router_test/mixedcase3/MIXEDCASESTRING')); + $this->assertResponse(200); + $this->assertRaw('MIXEDCASESTRING', 'The variable string was output with unmodified case.'); + // Test routes added by \Drupal\router_test\RouteTestSubscriber. + // Check that a dynamically added route has path changed to lower-case. + $route4 = $route_provider->getRouteByName('router_test.mixedcase.4'); + $this->assertIdentical('/router_test/mixedcase4', $route4->getPath()); + $this->drupalGet(Url::fromUri('base:router_TEST/MixedCASE4')); + $this->assertResponse(200); + // Check that a route added dynamically and altered has path lower-cased. + $route5 = $route_provider->getRouteByName('router_test.mixedcase.5'); + $this->assertIdentical('/router_test/mixedcase5/altered', $route5->getPath()); + $this->drupalGet(Url::fromUri('base:router_test/MixedCASE5/altereD')); + $this->assertResponse(200); + // Check that a route added during the alter event has path lower-cased. + $route6 = $route_provider->getRouteByName('router_test.mixedcase.6'); + $this->assertIdentical('/router_test/mixedcase6', $route6->getPath()); + $this->drupalGet(Url::fromUri('base:router_test/MixedCASE6')); + $this->assertResponse(200); + } + + /** * Tests that a PSR-7 response works. */ public function testRouterResponsePsr7() { diff --git a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml index e0c91dd..7bf05b7 100644 --- a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml @@ -189,3 +189,17 @@ router_test.hierarchy_parent_child2: _controller: '\Drupal\router_test\TestControllers::test' requirements: _access: 'TRUE' + +router_test.mixedcase.2: + path: '/router_test/mixedCASE2' + defaults: + _controller: '\Drupal\router_test\TestControllers::test2' + requirements: + _access: 'TRUE' + +router_test.mixedcase.3: + path: '/router_test/mixedcase3/{ValUe}' + defaults: + _controller: '\Drupal\router_test\TestControllers::test3' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php b/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php index 3bc703b..72b824b 100644 --- a/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php +++ b/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php @@ -7,21 +7,82 @@ namespace Drupal\router_test; -use Drupal\Core\Routing\RouteSubscriberBase; -use Symfony\Component\Routing\RouteCollection; +use Drupal\Core\Routing\RouteBuildEvent; +use Drupal\Core\Routing\RoutingEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Routing\Route; /** * Listens to the dynamic route event and add a test route. */ -class RouteTestSubscriber extends RouteSubscriberBase { +class RouteTestSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} */ - protected function alterRoutes(RouteCollection $collection) { + public static function getSubscribedEvents() { + $events[RoutingEvents::DYNAMIC] = 'onDynamicRoutes'; + $events[RoutingEvents::ALTER] = 'onAlterRoutes'; + return $events; + } + + /** + * Adds dynamic routes for a specific collection from an event. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onDynamicRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + $route = new Route( + '/router_test/mixedCASE4', + [ + '_controller' => '\Drupal\router_test\TestControllers::test2', + ], + [ + '_access' => 'TRUE', + ] + ); + $collection->add("router_test.mixedcase.4", $route); + $route = new Route( + '/router_test/tobealtered', + [ + '_controller' => '\Drupal\router_test\TestControllers::test2', + ], + [ + '_access' => 'TRUE', + ] + ); + $collection->add("router_test.mixedcase.5", $route); + } + + /** + * Alters existing routes for a specific collection from an event. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onAlterRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); $route = $collection->get('router_test.6'); // Change controller method from test1 to test5. $route->setDefault('_controller', '\Drupal\router_test\TestControllers::test5'); + // Change a route path from lower case to mixed case. + $route = $collection->get('router_test.mixedcase.5'); + $route->setPath('/router_tesT/MIXedcase5/ALTERED'); + // We can also add routes in the alter, which happens in classes like + // \Drupal\content_translation\Routing\ContentTranslationRouteSubscriber + // that need to add routes based on other routes that are added dynamically. + $route = new Route( + '/router_test/mixedCASE6', + [ + '_controller' => '\Drupal\router_test\TestControllers::test2', + ], + [ + '_access' => 'TRUE', + ] + ); + $collection->add("router_test.mixedcase.6", $route); } } diff --git a/core/modules/views/src/Plugin/views/display/PathPluginBase.php b/core/modules/views/src/Plugin/views/display/PathPluginBase.php index aab4f6d..f2a379b 100644 --- a/core/modules/views/src/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/PathPluginBase.php @@ -243,7 +243,9 @@ public function alterRoutes(RouteCollection $collection) { // Ensure that we don't override a route which is already controlled by // views. if (!$route->hasDefault('view_id') && ('/' . $view_path == $route_path)) { - $parameters = $route->compile()->getPathVariables(); + // The path variables array may have arbitrary keys, so take just the + // values to ensure we have a simple numeric array starting from 0. + $parameters = array_values($route->compile()->getPathVariables()); // @todo Figure out whether we need to merge some settings (like // requirements). diff --git a/core/modules/views/src/Tests/Wizard/BasicTest.php b/core/modules/views/src/Tests/Wizard/BasicTest.php index 8a64df8..4216594 100644 --- a/core/modules/views/src/Tests/Wizard/BasicTest.php +++ b/core/modules/views/src/Tests/Wizard/BasicTest.php @@ -9,6 +9,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Url; use Drupal\views\Views; @@ -66,9 +67,9 @@ function testViewsWizardAndListing() { $view2['description'] = $this->randomMachineName(16); $view2['page[create]'] = 1; $view2['page[title]'] = $this->randomMachineName(16); - $view2['page[path]'] = $this->randomMachineName(16); + $view2['page[path]'] = Unicode::strtolower($this->randomMachineName(16)); $view2['page[feed]'] = 1; - $view2['page[feed_properties][path]'] = $this->randomMachineName(16); + $view2['page[feed_properties][path]'] = Unicode::strtolower($this->randomMachineName(16)); $this->drupalPostForm('admin/structure/views/add', $view2, t('Save and edit')); $this->drupalGet($view2['page[path]']); $this->assertResponse(200); @@ -115,7 +116,7 @@ function testViewsWizardAndListing() { $view3['show[type]'] = 'page'; $view3['page[create]'] = 1; $view3['page[title]'] = $this->randomMachineName(16); - $view3['page[path]'] = $this->randomMachineName(16); + $view3['page[path]'] = Unicode::strtolower($this->randomMachineName(16)); $view3['block[create]'] = 1; $view3['block[title]'] = $this->randomMachineName(16); $this->drupalPostForm('admin/structure/views/add', $view3, t('Save and edit')); diff --git a/core/modules/views/src/Tests/Wizard/MenuTest.php b/core/modules/views/src/Tests/Wizard/MenuTest.php index af6a98d..93c11c5 100644 --- a/core/modules/views/src/Tests/Wizard/MenuTest.php +++ b/core/modules/views/src/Tests/Wizard/MenuTest.php @@ -30,7 +30,7 @@ function testMenus() { $view['description'] = $this->randomMachineName(16); $view['page[create]'] = 1; $view['page[title]'] = $this->randomMachineName(16); - $view['page[path]'] = $this->randomMachineName(16); + $view['page[path]'] = strtolower($this->randomMachineName(16)); $view['page[link]'] = 1; $view['page[link_properties][menu_name]'] = 'main'; $view['page[link_properties][title]'] = $this->randomMachineName(16);