Commit 22a6d8cc authored by catch's avatar catch

Issue #2012382 by damiankloip: Improve efficiency of access checker matching on routes.

parent 8b4fa68c
......@@ -8,37 +8,11 @@
namespace Drupal\Core\Access;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* An access check service determines access rules for particular routes.
*/
interface AccessCheckInterface {
/**
* Grant access.
*
* A checker should return this value to indicate that it grants access to a
* route.
*/
const ALLOW = TRUE;
/**
* Deny access.
*
* A checker should return this value to indicate it does not grant access to
* a route.
*/
const DENY = NULL;
/**
* Block access.
*
* A checker should return this value to indicate that it wants to completely
* block access to this route, regardless of any other access checkers. Most
* checkers should prefer DENY.
*/
const KILL = FALSE;
interface AccessCheckInterface extends AccessInterface {
/**
* Declares whether the access check applies to a specific route or not.
......@@ -46,23 +20,9 @@ interface AccessCheckInterface {
* @param \Symfony\Component\Routing\Route $route
* The route to consider attaching to.
*
* @return bool
* TRUE if the check applies to the passed route, FALSE otherwise.
* @return array
* An array of route requirement keys this access checker applies to.
*/
public function applies(Route $route);
/**
* Checks for access to route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return mixed
* TRUE if access is allowed.
* FALSE if not.
* NULL if no opinion.
*/
public function access(Route $route, Request $request);
}
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessInterface.
*/
namespace Drupal\Core\Access;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* An access check service determines access rules for particular routes.
*/
interface AccessInterface {
/**
* Grant access.
*
* A checker should return this value to indicate that it grants access to a
* route.
*/
const ALLOW = TRUE;
/**
* Deny access.
*
* A checker should return this value to indicate it does not grant access to
* a route.
*/
const DENY = NULL;
/**
* Block access.
*
* A checker should return this value to indicate that it wants to completely
* block access to this route, regardless of any other access checkers. Most
* checkers should prefer DENY.
*/
const KILL = FALSE;
/**
* Checks for access to a route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return mixed
* TRUE if access is allowed.
* FALSE if not.
* NULL if no opinion.
*/
public function access(Route $route, Request $request);
}
<?php
/**
* @file
* Contains Drupal\Core\Access\AccessManager.
......@@ -33,6 +34,20 @@ class AccessManager extends ContainerAware {
*/
protected $checks;
/**
* An array to map static requirement keys to service IDs.
*
* @var array
*/
protected $staticRequirementMap;
/**
* An array to map dynamic requirement keys to service IDs.
*
* @var array
*/
protected $dynamicRequirementMap;
/**
* Registers a new AccessCheck by service ID.
*
......@@ -50,9 +65,9 @@ public function addCheckService($service_id) {
* A collection of routes to apply checks to.
*/
public function setChecks(RouteCollection $routes) {
$this->loadAccessRequirementMap();
foreach ($routes as $route) {
$checks = $this->applies($route);
if (!empty($checks)) {
if ($checks = $this->applies($route)) {
$route->setOption('_access_checks', $checks);
}
}
......@@ -71,13 +86,21 @@ public function setChecks(RouteCollection $routes) {
protected function applies(Route $route) {
$checks = array();
foreach ($this->checkIds as $service_id) {
if (empty($this->checks[$service_id])) {
$this->loadCheck($service_id);
// Iterate through map requirements from appliesTo() on access checkers.
// Only iterate through all checkIds if this is not used.
foreach ($route->getRequirements() as $key => $value) {
if (isset($this->staticRequirementMap[$key])) {
foreach ($this->staticRequirementMap[$key] as $service_id) {
$checks[] = $service_id;
}
}
if ($this->checks[$service_id]->applies($route)) {
$checks[] = $service_id;
// This means appliesTo() method was empty. Iterate through all checkers.
else {
foreach ($this->dynamicRequirementMap as $service_id) {
if ($this->checks[$service_id]->applies($route)) {
$checks[] = $service_id;
}
}
}
}
......@@ -135,7 +158,7 @@ protected function checkAll(array $checks, Route $route, Request $request) {
}
$service_access = $this->checks[$service_id]->access($route, $request);
if ($service_access === AccessCheckInterface::ALLOW) {
if ($service_access === AccessInterface::ALLOW) {
$access = TRUE;
}
else {
......@@ -171,10 +194,10 @@ protected function checkAny(array $checks, $route, $request) {
}
$service_access = $this->checks[$service_id]->access($route, $request);
if ($service_access === AccessCheckinterface::ALLOW) {
if ($service_access === AccessInterface::ALLOW) {
$access = TRUE;
}
if ($service_access === AccessCheckInterface::KILL) {
if ($service_access === AccessInterface::KILL) {
return FALSE;
}
}
......@@ -196,4 +219,34 @@ protected function loadCheck($service_id) {
$this->checks[$service_id] = $this->container->get($service_id);
}
/**
* Compiles a mapping of requirement keys to access checker service IDs.
*/
public function loadAccessRequirementMap() {
if (isset($this->staticRequirementMap, $this->dynamicRequirementMap)) {
return;
}
// Set them here, so we can use the isset() check above.
$this->staticRequirementMap = array();
$this->dynamicRequirementMap = array();
foreach ($this->checkIds as $service_id) {
if (empty($this->checks[$service_id])) {
$this->loadCheck($service_id);
}
// Empty arrays will not register anything.
if (is_subclass_of($this->checks[$service_id], 'Drupal\Core\Access\StaticAccessCheckInterface')) {
foreach ((array) $this->checks[$service_id]->appliesTo() as $key) {
$this->staticRequirementMap[$key][] = $service_id;
}
}
// Add the service ID to a the regular that will be iterated over.
else {
$this->dynamicRequirementMap[] = $service_id;
}
}
}
}
......@@ -13,13 +13,13 @@
/**
* Allows access to routes to be controlled by an '_access' boolean parameter.
*/
class DefaultAccessCheck implements AccessCheckInterface {
class DefaultAccessCheck implements StaticAccessCheckInterface {
/**
* Implements AccessCheckInterface::applies().
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_access', $route->getRequirements());
public function appliesTo() {
return array('_access');
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Access\StaticAccessCheckInterface.
*/
namespace Drupal\Core\Access;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* An access check service determines access rules for particular routes.
*
* This interface is specifically for routes that know exactly which requirement
* keys they should react to for a route.
*/
interface StaticAccessCheckInterface extends AccessInterface {
/**
* Declares the route requirement keys this access checker applies to.
*
* This should be used when the requirement matching for a route is static,
* and does not require any further information. For example, '_access' will
* provide TRUE, or FALSE. We do not need any more information other than the
* route provides this requirement key.
*
* @return array
* An array of route requirement keys this access checker applies to. An
* empty array will check all routes using the apply method.
*/
public function appliesTo();
}
......@@ -10,18 +10,18 @@
use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
/**
* Provides a generic access checker for entities.
*/
class EntityAccessCheck implements AccessCheckInterface {
class EntityAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_entity_access', $route->getRequirements());
public function appliesTo() {
return array('_entity_access');
}
/**
......
......@@ -7,14 +7,14 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Defines an access checker for entity creation.
*/
class EntityCreateAccessCheck implements AccessCheckInterface {
class EntityCreateAccessCheck implements StaticAccessCheckInterface {
/**
* The entity manager.
......@@ -43,8 +43,8 @@ public function __construct(EntityManager $entity_manager) {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists($this->requirementsKey, $route->getRequirements());
public function appliesTo() {
return array($this->requirementsKey);
}
/**
......
......@@ -7,7 +7,7 @@
namespace Drupal\aggregator\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Drupal\Core\Database\Connection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
......@@ -15,7 +15,7 @@
/**
* Provides an access check for aggregator categories routes.
*/
class CategoriesAccessCheck implements AccessCheckInterface {
class CategoriesAccessCheck implements StaticAccessCheckInterface {
/**
* The database connection.
......@@ -37,8 +37,8 @@ public function __construct(Connection $database) {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_access_aggregator_categories', $route->getRequirements());
public function appliesTo() {
return array('_access_aggregator_categories');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\block\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Checks access for displaying block page.
*/
class BlockThemeAccessCheck implements AccessCheckInterface {
class BlockThemeAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_block_themes_access', $route->getRequirements());
public function appliesTo() {
return array('_block_themes_access');
}
/**
......
......@@ -7,7 +7,7 @@
namespace Drupal\edit\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
......@@ -16,14 +16,14 @@
/**
* Access check for editing entities.
*/
class EditEntityAccessCheck implements AccessCheckInterface, EditEntityAccessCheckInterface {
class EditEntityAccessCheck implements StaticAccessCheckInterface, EditEntityAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
public function appliesTo() {
// @see edit.routing.yml
return array_key_exists('_access_edit_entity', $route->getRequirements());
return array('_access_edit_entity');
}
/**
......
......@@ -7,7 +7,7 @@
namespace Drupal\edit\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
......@@ -16,14 +16,13 @@
/**
* Access check for editing entity fields.
*/
class EditEntityFieldAccessCheck implements AccessCheckInterface, EditEntityFieldAccessCheckInterface {
class EditEntityFieldAccessCheck implements StaticAccessCheckInterface, EditEntityFieldAccessCheckInterface {
/**
* Implements AccessCheckInterface::applies().
* {@inheritdoc}
*/
public function applies(Route $route) {
// @see edit.routing.yml
return array_key_exists('_access_edit_entity_field', $route->getRequirements());
public function appliesTo() {
return array('_access_edit_entity_field');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\field_ui\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Allows access to routes to be controlled by an '_access' boolean parameter.
*/
class FormModeAccessCheck implements AccessCheckInterface {
class FormModeAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_field_ui_form_mode_access', $route->getRequirements());
public function appliesTo() {
return array('_field_ui_form_mode_access');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\field_ui\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Allows access to routes to be controlled by an '_access' boolean parameter.
*/
class ViewModeAccessCheck implements AccessCheckInterface {
class ViewModeAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_field_ui_view_mode_access', $route->getRequirements());
public function appliesTo() {
return array('_field_ui_view_mode_access');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\filter\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Checks access for text formats.
*/
class FilterAccessCheck implements AccessCheckInterface {
class FilterAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_filter_access', $route->getRequirements());
public function appliesTo() {
return array('_filter_access');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\filter\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Checks access for disabling text formats.
*/
class FormatDisableCheck implements AccessCheckInterface {
class FormatDisableCheck implements StaticAccessCheckInterface {
/**
* Implements \Drupal\Core\Access\AccessCheckInterface::applies().
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_filter_disable_format_access', $route->getRequirements());
public function appliesTo() {
return array('_filter_disable_format_access');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\menu\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Access check for menu link delete routes.
*/
class DeleteLinkAccessCheck implements AccessCheckInterface {
class DeleteLinkAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_access_menu_delete_link', $route->getRequirements());
public function appliesTo() {
return array('_access_menu_delete_link');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\menu\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Access check for menu delete routes.
*/
class DeleteMenuAccessCheck implements AccessCheckInterface {
class DeleteMenuAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_access_menu_delete_menu', $route->getRequirements());
public function appliesTo() {
return array('_access_menu_delete_menu');
}
/**
......
......@@ -21,6 +21,7 @@ class CSRFAccessCheck implements AccessCheckInterface {
*/
public function applies(Route $route) {
$requirements = $route->getRequirements();
if (array_key_exists('_access_rest_csrf', $requirements)) {
if (isset($requirements['_method'])) {
// There could be more than one method requirement separated with '|'.
......@@ -36,7 +37,6 @@ public function applies(Route $route) {
// safe side.
return TRUE;
}
return FALSE;
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\shortcut\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an access check for shortcut link delete routes.
*/
class LinkDeleteAccessCheck implements AccessCheckInterface {
class LinkDeleteAccessCheck implements StaticAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_access_shortcut_link_delete', $route->getRequirements());
public function appliesTo() {
return array('_access_shortcut_link_delete');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\system\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Access check for cron routes.
*/
class CronAccessCheck implements AccessCheckInterface {
class CronAccessCheck implements StaticAccessCheckInterface {
/**
* Implements AccessCheckInterface::applies().
* {@inheritdoc}
*/
public function applies(Route $route) {
return array_key_exists('_access_system_cron', $route->getRequirements());
public function appliesTo() {
return array('_access_system_cron');
}
/**
......
......@@ -7,20 +7,20 @@
namespace Drupal\toolbar\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Defines a special access checker for the toolbar subtree route.
*/
class SubtreeAccess implements AccessCheckInterface