Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions
web/core/lib/Drupal/Core
Access
Action
Ajax
Annotation
AppRootFactory.php
Archiver
Asset
Authentication

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Component\Utility\ArgumentsResolver;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Resolves the arguments to pass to an access check callable.
*/
class AccessArgumentsResolverFactory implements AccessArgumentsResolverFactoryInterface {
/**
* {@inheritdoc}
*/
public function getArgumentsResolver(RouteMatchInterface $route_match, AccountInterface $account, Request $request = NULL) {
$route = $route_match->getRouteObject();
// Defaults for the parameters defined on the route object need to be added
// to the raw arguments.
$raw_route_arguments = $route_match->getRawParameters()->all() + $route->getDefaults();
$upcasted_route_arguments = $route_match->getParameters()->all();
// Parameters which are not defined on the route object, but still are
// essential for access checking are passed as wildcards to the argument
// resolver. An access-check method with a parameter of type Route,
// RouteMatchInterface, AccountInterface or Request will receive those
// arguments regardless of the parameter name.
$wildcard_arguments = [$route, $route_match, $account];
if (isset($request)) {
$wildcard_arguments[] = $request;
}
return new ArgumentsResolver($raw_route_arguments, $upcasted_route_arguments, $wildcard_arguments);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Constructs the arguments resolver instance to use when running access checks.
*/
interface AccessArgumentsResolverFactoryInterface {
/**
* Returns the arguments resolver to use when running access checks.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object to be checked.
* @param \Drupal\Core\Session\AccountInterface $account
* The account being checked.
* @param \Symfony\Component\HttpFoundation\Request $request
* Optional, the request object.
*
* @return \Drupal\Component\Utility\ArgumentsResolverInterface
* The parametrized arguments resolver instance.
*/
public function getArgumentsResolver(RouteMatchInterface $route_match, AccountInterface $account, Request $request = NULL);
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Access;
use Symfony\Component\Routing\Route;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
/**
* An access check service determines access rules for particular routes.
*/
interface AccessCheckInterface extends RoutingAccessInterface {
/**
* Declares whether the access check applies to a specific route or not.
*
* @param \Symfony\Component\Routing\Route $route
* The route to consider attaching to.
*
* @return array
* An array of route requirement keys this access checker applies to.
*/
public function applies(Route $route);
}

View file

@ -0,0 +1,12 @@
<?php
namespace Drupal\Core\Access;
/**
* An exception thrown for access errors.
*
* Examples could be invalid access callback return values, or invalid access
* objects being used.
*/
class AccessException extends \RuntimeException {
}

View file

@ -0,0 +1,172 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\ParamConverter\ParamConverterManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Utility\ArgumentsResolverInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
/**
* Attaches access check services to routes and runs them on request.
*
* @see \Drupal\Tests\Core\Access\AccessManagerTest
*/
class AccessManager implements AccessManagerInterface {
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The paramconverter manager.
*
* @var \Drupal\Core\ParamConverter\ParamConverterManagerInterface
*/
protected $paramConverterManager;
/**
* The access arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface
*/
protected $argumentsResolverFactory;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The check provider.
*
* @var \Drupal\Core\Access\CheckProviderInterface
*/
protected $checkProvider;
/**
* Constructs a AccessManager instance.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\ParamConverter\ParamConverterManagerInterface $paramconverter_manager
* The param converter manager.
* @param \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface $arguments_resolver_factory
* The access arguments resolver.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param CheckProviderInterface $check_provider
*/
public function __construct(RouteProviderInterface $route_provider, ParamConverterManagerInterface $paramconverter_manager, AccessArgumentsResolverFactoryInterface $arguments_resolver_factory, AccountInterface $current_user, CheckProviderInterface $check_provider) {
$this->routeProvider = $route_provider;
$this->paramConverterManager = $paramconverter_manager;
$this->argumentsResolverFactory = $arguments_resolver_factory;
$this->currentUser = $current_user;
$this->checkProvider = $check_provider;
}
/**
* {@inheritdoc}
*/
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account = NULL, $return_as_object = FALSE) {
try {
$route = $this->routeProvider->getRouteByName($route_name, $parameters);
// ParamConverterManager relies on the route name and object being
// available from the parameters array.
$parameters[RouteObjectInterface::ROUTE_NAME] = $route_name;
$parameters[RouteObjectInterface::ROUTE_OBJECT] = $route;
$upcasted_parameters = $this->paramConverterManager->convert($parameters + $route->getDefaults());
$route_match = new RouteMatch($route_name, $route, $upcasted_parameters, $parameters);
return $this->check($route_match, $account, NULL, $return_as_object);
}
catch (RouteNotFoundException $e) {
// Cacheable until extensions change.
$result = AccessResult::forbidden()->addCacheTags(['config:core.extension']);
return $return_as_object ? $result : $result->isAllowed();
}
catch (ParamNotConvertedException $e) {
// Uncacheable because conversion of the parameter may not have been
// possible due to dynamic circumstances.
$result = AccessResult::forbidden()->setCacheMaxAge(0);
return $return_as_object ? $result : $result->isAllowed();
}
}
/**
* {@inheritdoc}
*/
public function checkRequest(Request $request, AccountInterface $account = NULL, $return_as_object = FALSE) {
$route_match = RouteMatch::createFromRequest($request);
return $this->check($route_match, $account, $request, $return_as_object);
}
/**
* {@inheritdoc}
*/
public function check(RouteMatchInterface $route_match, AccountInterface $account = NULL, Request $request = NULL, $return_as_object = FALSE) {
if (!isset($account)) {
$account = $this->currentUser;
}
$route = $route_match->getRouteObject();
$checks = $route->getOption('_access_checks') ?: array();
// Filter out checks which require the incoming request.
if (!isset($request)) {
$checks = array_diff($checks, $this->checkProvider->getChecksNeedRequest());
}
$result = AccessResult::neutral();
if (!empty($checks)) {
$arguments_resolver = $this->argumentsResolverFactory->getArgumentsResolver($route_match, $account, $request);
if (!$checks) {
return AccessResult::neutral();
}
$result = AccessResult::allowed();
foreach ($checks as $service_id) {
$result = $result->andIf($this->performCheck($service_id, $arguments_resolver));
}
}
return $return_as_object ? $result : $result->isAllowed();
}
/**
* Performs the specified access check.
*
* @param string $service_id
* The access check service ID to use.
* @param \Drupal\Component\Utility\ArgumentsResolverInterface $arguments_resolver
* The parametrized arguments resolver instance.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*
* @throws \Drupal\Core\Access\AccessException
* Thrown when the access check returns an invalid value.
*/
protected function performCheck($service_id, ArgumentsResolverInterface $arguments_resolver) {
$callable = $this->checkProvider->loadCheck($service_id);
$arguments = $arguments_resolver->getArguments($callable);
/** @var \Drupal\Core\Access\AccessResultInterface $service_access **/
$service_access = call_user_func_array($callable, $arguments);
if (!$service_access instanceof AccessResultInterface) {
throw new AccessException("Access error in $service_id. Access services must return an object that implements AccessResultInterface.");
}
return $service_access;
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Drupal\Core\Access;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Provides an interface for attaching and running access check services.
*/
interface AccessManagerInterface {
/**
* Checks a named route with parameters against applicable access check services.
*
* Determines whether the route is accessible or not.
*
* @param string $route_name
* The route to check access to.
* @param array $parameters
* Optional array of values to substitute into the route path pattern.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account = NULL, $return_as_object = FALSE);
/**
* Execute access checks against the incoming request.
*
* @param Request $request
* The incoming request.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function checkRequest(Request $request, AccountInterface $account = NULL, $return_as_object = FALSE);
/**
* Checks a route against applicable access check services.
*
* Determines whether the route is accessible or not.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
* @param \Symfony\Component\HttpFoundation\Request $request
* Optional, a request. Only supply this parameter when checking the
* incoming request, do not specify when checking routes on output.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function check(RouteMatchInterface $route_match, AccountInterface $account = NULL, Request $request = NULL, $return_as_object = FALSE);
}

View file

@ -0,0 +1,404 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Config\ConfigBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Value object for passing an access result with cacheability metadata.
*
* The access result itself excluding the cacheability metadata  is
* immutable. There are subclasses for each of the three possible access results
* themselves:
*
* @see \Drupal\Core\Access\AccessResultAllowed
* @see \Drupal\Core\Access\AccessResultForbidden
* @see \Drupal\Core\Access\AccessResultNeutral
*
* When using ::orIf() and ::andIf(), cacheability metadata will be merged
* accordingly as well.
*/
abstract class AccessResult implements AccessResultInterface, RefinableCacheableDependencyInterface {
use RefinableCacheableDependencyTrait;
/**
* Creates an AccessResultInterface object with isNeutral() === TRUE.
*
* @return \Drupal\Core\Access\AccessResult
* isNeutral() will be TRUE.
*/
public static function neutral() {
return new AccessResultNeutral();
}
/**
* Creates an AccessResultInterface object with isAllowed() === TRUE.
*
* @return \Drupal\Core\Access\AccessResult
* isAllowed() will be TRUE.
*/
public static function allowed() {
return new AccessResultAllowed();
}
/**
* Creates an AccessResultInterface object with isForbidden() === TRUE.
*
* @param string|null $reason
* (optional) The reason why access is forbidden. Intended for developers,
* hence not translatable.
*
* @return \Drupal\Core\Access\AccessResult
* isForbidden() will be TRUE.
*/
public static function forbidden($reason = NULL) {
assert('is_string($reason) || is_null($reason)');
return new AccessResultForbidden($reason);
}
/**
* Creates an allowed or neutral access result.
*
* @param bool $condition
* The condition to evaluate.
*
* @return \Drupal\Core\Access\AccessResult
* If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
* will be TRUE.
*/
public static function allowedIf($condition) {
return $condition ? static::allowed() : static::neutral();
}
/**
* Creates a forbidden or neutral access result.
*
* @param bool $condition
* The condition to evaluate.
*
* @return \Drupal\Core\Access\AccessResult
* If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
* will be TRUE.
*/
public static function forbiddenIf($condition) {
return $condition ? static::forbidden() : static::neutral();
}
/**
* Creates an allowed access result if the permission is present, neutral otherwise.
*
* Checks the permission and adds a 'user.permissions' cache context.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The account for which to check a permission.
* @param string $permission
* The permission to check for.
*
* @return \Drupal\Core\Access\AccessResult
* If the account has the permission, isAllowed() will be TRUE, otherwise
* isNeutral() will be TRUE.
*/
public static function allowedIfHasPermission(AccountInterface $account, $permission) {
return static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
}
/**
* Creates an allowed access result if the permissions are present, neutral otherwise.
*
* Checks the permission and adds a 'user.permissions' cache contexts.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The account for which to check permissions.
* @param array $permissions
* The permissions to check.
* @param string $conjunction
* (optional) 'AND' if all permissions are required, 'OR' in case just one.
* Defaults to 'AND'
*
* @return \Drupal\Core\Access\AccessResult
* If the account has the permissions, isAllowed() will be TRUE, otherwise
* isNeutral() will be TRUE.
*/
public static function allowedIfHasPermissions(AccountInterface $account, array $permissions, $conjunction = 'AND') {
$access = FALSE;
if ($conjunction == 'AND' && !empty($permissions)) {
$access = TRUE;
foreach ($permissions as $permission) {
if (!$permission_access = $account->hasPermission($permission)) {
$access = FALSE;
break;
}
}
}
else {
foreach ($permissions as $permission) {
if ($permission_access = $account->hasPermission($permission)) {
$access = TRUE;
break;
}
}
}
return static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
}
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Access\AccessResultAllowed
*/
public function isAllowed() {
return FALSE;
}
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Access\AccessResultForbidden
*/
public function isForbidden() {
return FALSE;
}
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Access\AccessResultNeutral
*/
public function isNeutral() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->cacheContexts;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->cacheTags;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->cacheMaxAge;
}
/**
* Resets cache contexts (to the empty array).
*
* @return $this
*/
public function resetCacheContexts() {
$this->cacheContexts = [];
return $this;
}
/**
* Resets cache tags (to the empty array).
*
* @return $this
*/
public function resetCacheTags() {
$this->cacheTags = [];
return $this;
}
/**
* Sets the maximum age for which this access result may be cached.
*
* @param int $max_age
* The maximum time in seconds that this access result may be cached.
*
* @return $this
*/
public function setCacheMaxAge($max_age) {
$this->cacheMaxAge = $max_age;
return $this;
}
/**
* Convenience method, adds the "user.permissions" cache context.
*
* @return $this
*/
public function cachePerPermissions() {
$this->addCacheContexts(array('user.permissions'));
return $this;
}
/**
* Convenience method, adds the "user" cache context.
*
* @return $this
*/
public function cachePerUser() {
$this->addCacheContexts(array('user'));
return $this;
}
/**
* Convenience method, adds the entity's cache tag.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose cache tag to set on the access result.
*
* @return $this
*
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
* ::addCacheableDependency() instead.
*/
public function cacheUntilEntityChanges(EntityInterface $entity) {
return $this->addCacheableDependency($entity);
}
/**
* Convenience method, adds the configuration object's cache tag.
*
* @param \Drupal\Core\Config\ConfigBase $configuration
* The configuration object whose cache tag to set on the access result.
*
* @return $this
*
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
* ::addCacheableDependency() instead.
*/
public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
return $this->addCacheableDependency($configuration);
}
/**
* {@inheritdoc}
*/
public function orIf(AccessResultInterface $other) {
$merge_other = FALSE;
// $other's cacheability metadata is merged if $merge_other gets set to TRUE
// and this happens in three cases:
// 1. $other's access result is the one that determines the combined access
// result.
// 2. This access result is not cacheable and $other's access result is the
// same. i.e. attempt to return a cacheable access result.
// 3. Neither access result is 'forbidden' and both are cacheable: inherit
// the other's cacheability metadata because it may turn into a
// 'forbidden' for another value of the cache contexts in the
// cacheability metadata. In other words: this is necessary to respect
// the contagious nature of the 'forbidden' access result.
// e.g. we have two access results A and B. Neither is forbidden. A is
// globally cacheable (no cache contexts). B is cacheable per role. If we
// don't have merging case 3, then A->orIf(B) will be globally cacheable,
// which means that even if a user of a different role logs in, the
// cached access result will be used, even though for that other role, B
// is forbidden!
if ($this->isForbidden() || $other->isForbidden()) {
$result = static::forbidden();
if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
$merge_other = TRUE;
}
}
elseif ($this->isAllowed() || $other->isAllowed()) {
$result = static::allowed();
if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
$merge_other = TRUE;
}
}
else {
$result = static::neutral();
if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
$merge_other = TRUE;
}
}
$result->inheritCacheability($this);
if ($merge_other) {
$result->inheritCacheability($other);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function andIf(AccessResultInterface $other) {
// The other access result's cacheability metadata is merged if $merge_other
// gets set to TRUE. It gets set to TRUE in one case: if the other access
// result is used.
$merge_other = FALSE;
if ($this->isForbidden() || $other->isForbidden()) {
$result = static::forbidden();
if (!$this->isForbidden()) {
if ($other instanceof AccessResultReasonInterface) {
$result->setReason($other->getReason());
}
$merge_other = TRUE;
}
else {
if ($this instanceof AccessResultReasonInterface) {
$result->setReason($this->getReason());
}
}
}
elseif ($this->isAllowed() && $other->isAllowed()) {
$result = static::allowed();
$merge_other = TRUE;
}
else {
$result = static::neutral();
if (!$this->isNeutral()) {
$merge_other = TRUE;
}
}
$result->inheritCacheability($this);
if ($merge_other) {
$result->inheritCacheability($other);
// If this access result is not cacheable, then an AND with another access
// result must also not be cacheable, except if the other access result
// has isForbidden() === TRUE. isForbidden() access results are contagious
// in that they propagate regardless of the other value.
if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
$result->setCacheMaxAge(0);
}
}
return $result;
}
/**
* Inherits the cacheability of the other access result, if any.
*
* inheritCacheability() differs from addCacheableDependency() in how it
* handles max-age, because it is designed to inherit the cacheability of the
* second operand in the andIf() and orIf() operations. There, the situation
* "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
* as the end result.
*
* @param \Drupal\Core\Access\AccessResultInterface $other
* The other access result, whose cacheability (if any) to inherit.
*
* @return $this
*/
public function inheritCacheability(AccessResultInterface $other) {
$this->addCacheableDependency($other);
if ($other instanceof CacheableDependencyInterface) {
if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
}
else {
$this->setCacheMaxAge($other->getCacheMaxAge());
}
}
return $this;
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\Core\Access;
/**
* Value object indicating an allowed access result, with cacheability metadata.
*/
class AccessResultAllowed extends AccessResult {
/**
* {@inheritdoc}
*/
public function isAllowed() {
return TRUE;
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\Core\Access;
/**
* Value object indicating a forbidden access result, with cacheability metadata.
*/
class AccessResultForbidden extends AccessResult implements AccessResultReasonInterface {
/**
* The reason why access is forbidden. For use in error messages.
*
* @var string|null
*/
protected $reason;
/**
* Constructs a new AccessResultForbidden instance.
*
* @param null|string $reason
* (optional) a message to provide details about this access result
*/
public function __construct($reason = NULL) {
$this->reason = $reason;
}
/**
* {@inheritdoc}
*/
public function isForbidden() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getReason() {
return $this->reason;
}
/**
* {@inheritdoc}
*/
public function setReason($reason) {
$this->reason = $reason;
return $this;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Drupal\Core\Access;
/**
* Interface for access result value objects.
*
* IMPORTANT NOTE: You have to call isAllowed() when you want to know whether
* someone has access. Just using
* @code
* if ($access_result) {
* // The user has access!
* }
* else {
* // The user doesn't have access!
* }
* @endcode
* would never enter the else-statement and hence introduce a critical security
* issue.
*/
interface AccessResultInterface {
/**
* Checks whether this access result indicates access is explicitly allowed.
*
* @return bool
* When TRUE then isForbidden() and isNeutral() are FALSE.
*/
public function isAllowed();
/**
* Checks whether this access result indicates access is explicitly forbidden.
*
* This is a kill switch both orIf() and andIf() will result in
* isForbidden() if either results are isForbidden().
*
* @return bool
* When TRUE then isAllowed() and isNeutral() are FALSE.
*/
public function isForbidden();
/**
* Checks whether this access result indicates access is not yet determined.
*
* @return bool
* When TRUE then isAllowed() and isForbidden() are FALSE.
*/
public function isNeutral();
/**
* Combine this access result with another using OR.
*
* When OR-ing two access results, the result is:
* - isForbidden() in either isForbidden()
* - otherwise if isAllowed() in either isAllowed()
* - otherwise both must be isNeutral() isNeutral()
*
* Truth table:
* @code
* |A N F
* --+-----
* A |A A F
* N |A N F
* F |F F F
* @endcode
*
* @param \Drupal\Core\Access\AccessResultInterface $other
* The other access result to OR this one with.
*
* @return static
*/
public function orIf(AccessResultInterface $other);
/**
* Combine this access result with another using AND.
*
* When AND-ing two access results, the result is:
* - isForbidden() in either isForbidden()
* - otherwise, if isAllowed() in both isAllowed()
* - otherwise, one of them is isNeutral() isNeutral()
*
* Truth table:
* @code
* |A N F
* --+-----
* A |A N F
* N |N N F
* F |F F F
* @endcode
*
* @param \Drupal\Core\Access\AccessResultInterface $other
* The other access result to AND this one with.
*
* @return static
*/
public function andIf(AccessResultInterface $other);
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\Core\Access;
/**
* Value object indicating a neutral access result, with cacheability metadata.
*/
class AccessResultNeutral extends AccessResult {
/**
* {@inheritdoc}
*/
public function isNeutral() {
return TRUE;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\Core\Access;
/**
* Interface for access result value objects with stored reason for developers.
*
* For example, a developer can specify the reason for forbidden access:
* @code
* new AccessResultForbidden('You are not authorized to hack core');
* @endcode
*
* @see \Drupal\Core\Access\AccessResultInterface
*/
interface AccessResultReasonInterface extends AccessResultInterface {
/**
* Gets the reason for this access result.
*
* @return string|null
* The reason of this access result or NULL if no reason is provided.
*/
public function getReason();
/**
* Sets the reason for this access result.
*
* @param $reason string|null
* The reason of this access result or NULL if no reason is provided.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result instance.
*/
public function setReason($reason);
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Session\AccountInterface;
/**
* Interface for checking access.
*
* @ingroup entity_api
*/
interface AccessibleInterface {
/**
* Checks data value access.
*
* @param string $operation
* The operation to be performed.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE);
}

View file

@ -0,0 +1,168 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Routing\Access\AccessInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Loads access checkers from the container.
*/
class CheckProvider implements CheckProviderInterface, ContainerAwareInterface {
use ContainerAwareTrait;
/**
* Array of registered access check service ids.
*
* @var array
*/
protected $checkIds = array();
/**
* Array of access check objects keyed by service id.
*
* @var \Drupal\Core\Routing\Access\AccessInterface[]
*/
protected $checks;
/**
* Array of access check method names keyed by service ID.
*
* @var array
*/
protected $checkMethods = array();
/**
* Array of access checks which only will be run on the incoming request.
*/
protected $checksNeedsRequest = array();
/**
* 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;
/**
* {@inheritdoc}
*/
public function addCheckService($service_id, $service_method, array $applies_checks = array(), $needs_incoming_request = FALSE) {
$this->checkIds[] = $service_id;
$this->checkMethods[$service_id] = $service_method;
if ($needs_incoming_request) {
$this->checksNeedsRequest[$service_id] = $service_id;
}
foreach ($applies_checks as $applies_check) {
$this->staticRequirementMap[$applies_check][] = $service_id;
}
}
/**
* {@inheritdoc}
*/
public function getChecksNeedRequest() {
return $this->checksNeedsRequest;
}
/**
* {@inheritdoc}
*/
public function setChecks(RouteCollection $routes) {
$this->loadDynamicRequirementMap();
foreach ($routes as $route) {
if ($checks = $this->applies($route)) {
$route->setOption('_access_checks', $checks);
}
}
}
/**
* {@inheritdoc}
*/
public function loadCheck($service_id) {
if (empty($this->checks[$service_id])) {
if (!in_array($service_id, $this->checkIds)) {
throw new \InvalidArgumentException(sprintf('No check has been registered for %s', $service_id));
}
$check = $this->container->get($service_id);
if (!($check instanceof AccessInterface)) {
throw new AccessException('All access checks must implement AccessInterface.');
}
if (!is_callable(array($check, $this->checkMethods[$service_id]))) {
throw new AccessException(sprintf('Access check method %s in service %s must be callable.', $this->checkMethods[$service_id], $service_id));
}
$this->checks[$service_id] = $check;
}
return [$this->checks[$service_id], $this->checkMethods[$service_id]];
}
/**
* Determine which registered access checks apply to a route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to get list of access checks for.
*
* @return array
* An array of service ids for the access checks that apply to passed
* route.
*/
protected function applies(Route $route) {
$checks = array();
// 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;
}
}
}
// Finally, see if any dynamic access checkers apply.
foreach ($this->dynamicRequirementMap as $service_id) {
if ($this->checks[$service_id]->applies($route)) {
$checks[] = $service_id;
}
}
return $checks;
}
/**
* Compiles a mapping of requirement keys to access checker service IDs.
*/
protected function loadDynamicRequirementMap() {
if (isset($this->dynamicRequirementMap)) {
return;
}
// Set them here, so we can use the isset() check above.
$this->dynamicRequirementMap = array();
foreach ($this->checkIds as $service_id) {
if (empty($this->checks[$service_id])) {
$this->loadCheck($service_id);
}
// Add the service ID to an array that will be iterated over.
if ($this->checks[$service_id] instanceof AccessCheckInterface) {
$this->dynamicRequirementMap[] = $service_id;
}
}
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\Core\Access;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides the available access checkers by service IDs.
*
* Access checker services are added by ::addCheckService calls and are loaded
* by ::loadCheck.
*
* The checker provider service and the actual checking is separated in order
* to not require the full access manager on route build time.
*/
interface CheckProviderInterface {
/**
* For each route, saves a list of applicable access checks to the route.
*
* @param \Symfony\Component\Routing\RouteCollection $routes
* A collection of routes to apply checks to.
*/
public function setChecks(RouteCollection $routes);
/**
* Registers a new AccessCheck by service ID.
*
* @param string $service_id
* The ID of the service in the Container that provides a check.
* @param string $service_method
* The method to invoke on the service object for performing the check.
* @param array $applies_checks
* (optional) An array of route requirement keys the checker service applies
* to.
* @param bool $needs_incoming_request
* (optional) True if access-check method only acts on an incoming request.
*/
public function addCheckService($service_id, $service_method, array $applies_checks = array(), $needs_incoming_request = FALSE);
/**
* Lazy-loads access check services.
*
* @param string $service_id
* The service id of the access check service to load.
*
* @return callable
* A callable access check.
*
* @throws \InvalidArgumentException
* Thrown when the service hasn't been registered in addCheckService().
* @throws \Drupal\Core\Access\AccessException
* Thrown when the service doesn't implement the required interface.
*/
public function loadCheck($service_id);
/**
* A list of checks that needs the request.
*
* @return array
*/
public function getChecksNeedRequest();
}

View file

@ -0,0 +1,67 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Allows access to routes to be controlled by a '_csrf_token' parameter.
*
* To use this check, add a "token" GET parameter to URLs of which the value is
* a token generated by \Drupal::csrfToken()->get() using the same value as the
* "_csrf_token" parameter in the route.
*/
class CsrfAccessCheck implements RoutingAccessInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* Constructs a CsrfAccessCheck object.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
*/
public function __construct(CsrfTokenGenerator $csrf_token) {
$this->csrfToken = $csrf_token;
}
/**
* Checks access based on a CSRF token for the request.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, Request $request, RouteMatchInterface $route_match) {
$parameters = $route_match->getRawParameters();
$path = ltrim($route->getPath(), '/');
// Replace the path parameters with values from the parameters array.
foreach ($parameters as $param => $value) {
$path = str_replace("{{$param}}", $value, $path);
}
if ($this->csrfToken->validate($request->query->get('token', ''), $path)) {
$result = AccessResult::allowed();
}
else {
$result = AccessResult::forbidden();
}
// Not cacheable because the CSRF token is highly dynamic.
return $result->setCacheMaxAge(0);
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionConfigurationInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Access protection against CSRF attacks.
*/
class CsrfRequestHeaderAccessCheck implements AccessCheckInterface {
/**
* A string key that will used to designate the token used by this class.
*/
const TOKEN_KEY = 'X-CSRF-Token request header';
/**
* The session configuration.
*
* @var \Drupal\Core\Session\SessionConfigurationInterface
*/
protected $sessionConfiguration;
/**
* The token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* Constructs a new rest CSRF access check.
*
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
* The session configuration.
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The token generator.
*/
public function __construct(SessionConfigurationInterface $session_configuration, CsrfTokenGenerator $csrf_token) {
$this->sessionConfiguration = $session_configuration;
$this->csrfToken = $csrf_token;
}
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
$requirements = $route->getRequirements();
// Check for current requirement _csrf_request_header_token and deprecated
// REST requirement.
$applicable_requirements = [
'_csrf_request_header_token',
// @todo Remove _access_rest_csrf in Drupal 9.0.0.
'_access_rest_csrf',
];
$requirement_keys = array_keys($requirements);
if (array_intersect($applicable_requirements, $requirement_keys)) {
if (isset($requirements['_method'])) {
// There could be more than one method requirement separated with '|'.
$methods = explode('|', $requirements['_method']);
// CSRF protection only applies to write operations, so we can filter
// out any routes that require reading methods only.
$write_methods = array_diff($methods, array('GET', 'HEAD', 'OPTIONS', 'TRACE'));
if (empty($write_methods)) {
return FALSE;
}
}
// No method requirement given, so we run this access check to be on the
// safe side.
return TRUE;
}
}
/**
* Checks access.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Request $request, AccountInterface $account) {
$method = $request->getMethod();
// This check only applies if
// 1. this is a write operation
// 2. the user was successfully authenticated and
// 3. the request comes with a session cookie.
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))
&& $account->isAuthenticated()
&& $this->sessionConfiguration->hasSession($request)
) {
if (!$request->headers->has('X-CSRF-Token')) {
return AccessResult::forbidden()->setReason('X-CSRF-Token request header is missing')->setCacheMaxAge(0);
}
$csrf_token = $request->headers->get('X-CSRF-Token');
// @todo Remove validate call using 'rest' in 8.3.
// Kept here for sessions active during update.
if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY)
&& !$this->csrfToken->validate($csrf_token, 'rest')) {
return AccessResult::forbidden()->setReason('X-CSRF-Token request header is invalid')->setCacheMaxAge(0);
}
}
// Let other access checkers decide if the request is legit.
return AccessResult::allowed()->setCacheMaxAge(0);
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\PrivateKey;
use Drupal\Core\Session\MetadataBag;
use Drupal\Core\Site\Settings;
/**
* Generates and validates CSRF tokens.
*
* @see \Drupal\Tests\Core\Access\CsrfTokenGeneratorTest
*/
class CsrfTokenGenerator {
/**
* The private key service.
*
* @var \Drupal\Core\PrivateKey
*/
protected $privateKey;
/**
* The session metadata bag.
*
* @var \Drupal\Core\Session\MetadataBag
*/
protected $sessionMetadata;
/**
* Constructs the token generator.
*
* @param \Drupal\Core\PrivateKey $private_key
* The private key service.
* @param \Drupal\Core\Session\MetadataBag $session_metadata
* The session metadata bag.
*/
public function __construct(PrivateKey $private_key, MetadataBag $session_metadata) {
$this->privateKey = $private_key;
$this->sessionMetadata = $session_metadata;
}
/**
* Generates a token based on $value, the user session, and the private key.
*
* The generated token is based on the session of the current user. Normally,
* anonymous users do not have a session, so the generated token will be
* different on every page request. To generate a token for users without a
* session, manually start a session prior to calling this function.
*
* @param string $value
* (optional) An additional value to base the token on.
*
* @return string
* A 43-character URL-safe token for validation, based on the token seed,
* the hash salt provided by Settings::getHashSalt(), and the
* 'drupal_private_key' configuration variable.
*
* @see \Drupal\Core\Site\Settings::getHashSalt()
* @see \Symfony\Component\HttpFoundation\Session\SessionInterface::start()
*/
public function get($value = '') {
$seed = $this->sessionMetadata->getCsrfTokenSeed();
if (empty($seed)) {
$seed = Crypt::randomBytesBase64();
$this->sessionMetadata->setCsrfTokenSeed($seed);
}
return $this->computeToken($seed, $value);
}
/**
* Validates a token based on $value, the user session, and the private key.
*
* @param string $token
* The token to be validated.
* @param string $value
* (optional) An additional value to base the token on.
*
* @return bool
* TRUE for a valid token, FALSE for an invalid token.
*/
public function validate($token, $value = '') {
$seed = $this->sessionMetadata->getCsrfTokenSeed();
if (empty($seed)) {
return FALSE;
}
return Crypt::hashEquals($this->computeToken($seed, $value), $token);
}
/**
* Generates a token based on $value, the token seed, and the private key.
*
* @param string $seed
* The per-session token seed.
* @param string $value
* (optional) An additional value to base the token on.
*
* @return string
* A 43-character URL-safe token for validation, based on the token seed,
* the hash salt provided by Settings::getHashSalt(), and the
* 'drupal_private_key' configuration variable.
*
* @see \Drupal\Core\Site\Settings::getHashSalt()
*/
protected function computeToken($seed, $value = '') {
return Crypt::hmacBase64($value, $seed . $this->privateKey->get() . Settings::getHashSalt());
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Defines an access checker that allows specifying a custom method for access.
*
* You should only use it when you are sure that the access callback will not be
* reused. Good examples in core are Edit or Toolbar module.
*
* The method is called on another instance of the controller class, so you
* cannot reuse any stored property of your actual controller instance used
* to generate the output.
*/
class CustomAccessCheck implements RoutingAccessInterface {
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface
*/
protected $argumentsResolverFactory;
/**
* Constructs a CustomAccessCheck instance.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface $arguments_resolver_factory
* The arguments resolver factory.
*/
public function __construct(ControllerResolverInterface $controller_resolver, AccessArgumentsResolverFactoryInterface $arguments_resolver_factory) {
$this->controllerResolver = $controller_resolver;
$this->argumentsResolverFactory = $arguments_resolver_factory;
}
/**
* Checks access for the account and route using the custom access checker.
*
* @param \Symfony\Component\Routing\Route $route
* The route.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object to be checked.
* @param \Drupal\Core\Session\AccountInterface $account
* The account being checked.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
$callable = $this->controllerResolver->getControllerFromDefinition($route->getRequirement('_custom_access'));
$arguments_resolver = $this->argumentsResolverFactory->getArgumentsResolver($route_match, $account);
$arguments = $arguments_resolver->getArguments($callable);
return call_user_func_array($callable, $arguments);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Symfony\Component\Routing\Route;
/**
* Allows access to routes to be controlled by an '_access' boolean parameter.
*/
class DefaultAccessCheck implements RoutingAccessInterface {
/**
* Checks access to the route based on the _access parameter.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route) {
if ($route->getRequirement('_access') === 'TRUE') {
return AccessResult::allowed();
}
elseif ($route->getRequirement('_access') === 'FALSE') {
return AccessResult::forbidden();
}
else {
return AccessResult::neutral();
}
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
use Symfony\Component\Routing\Route;
/**
* Processes the outbound route to handle the CSRF token.
*/
class RouteProcessorCsrf implements OutboundRouteProcessorInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* Constructs a RouteProcessorCsrf object.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
*/
function __construct(CsrfTokenGenerator $csrf_token) {
$this->csrfToken = $csrf_token;
}
/**
* {@inheritdoc}
*/
public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
if ($route->hasRequirement('_csrf_token')) {
$path = ltrim($route->getPath(), '/');
// Replace the path parameters with values from the parameters array.
foreach ($parameters as $param => $value) {
$path = str_replace("{{$param}}", $value, $path);
}
// Adding this to the parameters means it will get merged into the query
// string when the route is compiled.
if (!$bubbleable_metadata) {
$parameters['token'] = $this->csrfToken->get($path);
}
else {
// Generate a placeholder and a render array to replace it.
$placeholder = hash('sha1', $path);
$placeholder_render_array = [
'#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
];
// Instead of setting an actual CSRF token as the query string, we set
// the placeholder, which will be replaced at the very last moment. This
// ensures links with CSRF tokens don't break cacheability.
$parameters['token'] = $placeholder;
$bubbleable_metadata->addAttachments(['placeholders' => [$placeholder => $placeholder_render_array]]);
}
}
}
/**
* #lazy_builder callback; gets a CSRF token for the given path.
*
* @param string $path
* The path to get a CSRF token for.
*
* @return array
* A renderable array representing the CSRF token.
*/
public function renderPlaceholderCsrfToken($path) {
return [
'#markup' => $this->csrfToken->get($path),
// Tokens are per session.
'#cache' => [
'contexts' => [
'session',
],
],
];
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\Core\Action;
use Drupal\Core\Plugin\PluginBase;
/**
* Provides a base implementation for an Action plugin.
*
* @see \Drupal\Core\Annotation\Action
* @see \Drupal\Core\Action\ActionManager
* @see \Drupal\Core\Action\ActionInterface
* @see plugin_api
*/
abstract class ActionBase extends PluginBase implements ActionInterface {
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
foreach ($entities as $entity) {
$this->execute($entity);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\Core\Action;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Executable\ExecutableInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an interface for an Action plugin.
*
* @todo WARNING: The action API is going to receive some additions before
* release. The following additions are likely to happen:
* - The way configuration is handled and configuration forms are built is
* likely to change in order for the plugin to be of use for Rules.
* - Actions are going to become context-aware in
* https://www.drupal.org/node/2011038, what will deprecated the 'type'
* annotation.
* - Instead of action implementations saving entities, support for marking
* required context as to be saved by the execution manager will be added as
* part of https://www.drupal.org/node/2347017.
* - Actions will receive a data processing API that allows for token
* replacements to happen outside of the action plugin implementations,
* see https://www.drupal.org/node/2347023.
*
* @see \Drupal\Core\Annotation\Action
* @see \Drupal\Core\Action\ActionManager
* @see \Drupal\Core\Action\ActionBase
* @see plugin_api
*/
interface ActionInterface extends ExecutableInterface, PluginInspectionInterface {
/**
* Executes the plugin for an array of objects.
*
* @param array $objects
* An array of entities.
*/
public function executeMultiple(array $objects);
/**
* Checks object access.
*
* @param mixed $object
* The object to execute the action on.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE);
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\Core\Action;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Provides an Action plugin manager.
*
* @see \Drupal\Core\Annotation\Action
* @see \Drupal\Core\Action\ActionInterface
* @see \Drupal\Core\Action\ActionBase
* @see plugin_api
*/
class ActionManager extends DefaultPluginManager implements CategorizingPluginManagerInterface {
use CategorizingPluginManagerTrait;
/**
* Constructs a new class instance.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Action', $namespaces, $module_handler, 'Drupal\Core\Action\ActionInterface', 'Drupal\Core\Annotation\Action');
$this->alterInfo('action_info');
$this->setCacheBackend($cache_backend, 'action_info');
}
/**
* Gets the plugin definitions for this entity type.
*
* @param string $type
* The entity type name.
*
* @return array
* An array of plugin definitions for this entity type.
*/
public function getDefinitionsByType($type) {
return array_filter($this->getDefinitions(), function ($definition) use ($type) {
return $definition['type'] === $type;
});
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\Core\Action;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
/**
* Provides a container for lazily loading Action plugins.
*/
class ActionPluginCollection extends DefaultSingleLazyPluginCollection {
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Action\ActionInterface
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Drupal\Core\Action;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Provides a base implementation for a configurable Action plugin.
*/
abstract class ConfigurableActionBase extends ActionBase implements ConfigurablePluginInterface, PluginFormInterface {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configuration += $this->defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array();
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return array();
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Drupal\Core\Ajax;
/**
* An AJAX command for adding css to the page via ajax.
*
* This command is implemented by Drupal.AjaxCommands.prototype.add_css()
* defined in misc/ajax.js.
*
* @see misc/ajax.js
*
* @ingroup ajax
*/
class AddCssCommand implements CommandInterface {
/**
* A string that contains the styles to be added to the page.
*
* It should include the wrapping style tag.
*
* @var string
*/
protected $styles;
/**
* Constructs an AddCssCommand.
*
* @param string $styles
* A string that contains the styles to be added to the page, including the
* wrapping <style> tag.
*/
public function __construct($styles) {
$this->styles = $styles;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'add_css',
'data' => $this->styles,
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Core\Ajax;
/**
* An AJAX command for calling the jQuery after() method.
*
* The 'insert/after' command instructs the client to use jQuery's after()
* method to insert the given HTML content after each element matched by the
* given selector.
*
* This command is implemented by Drupal.AjaxCommands.prototype.insert()
* defined in misc/ajax.js.
*
* @see http://docs.jquery.com/Manipulation/after#content
*
* @ingroup ajax
*/
class AfterCommand extends InsertCommand {
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'insert',
'method' => 'after',
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Drupal\Core\Ajax;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsTrait;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* JSON response object for AJAX requests.
*
* @ingroup ajax
*/
class AjaxResponse extends JsonResponse implements AttachmentsInterface {
use AttachmentsTrait;
/**
* The array of ajax commands.
*
* @var array
*/
protected $commands = array();
/**
* Add an AJAX command to the response.
*
* @param \Drupal\Core\Ajax\CommandInterface $command
* An AJAX command object implementing CommandInterface.
* @param bool $prepend
* A boolean which determines whether the new command should be executed
* before previously added commands. Defaults to FALSE.
*
* @return AjaxResponse
* The current AjaxResponse.
*/
public function addCommand(CommandInterface $command, $prepend = FALSE) {
if ($prepend) {
array_unshift($this->commands, $command->render());
}
else {
$this->commands[] = $command->render();
}
if ($command instanceof CommandWithAttachedAssetsInterface) {
$assets = $command->getAttachedAssets();
$attachments = [
'library' => $assets->getLibraries(),
'drupalSettings' => $assets->getSettings(),
];
$attachments = BubbleableMetadata::mergeAttachments($this->getAttachments(), $attachments);
$this->setAttachments($attachments);
}
return $this;
}
/**
* Gets all AJAX commands.
*
* @return \Drupal\Core\Ajax\CommandInterface[]
* Returns all previously added AJAX commands.
*/
public function &getCommands() {
return $this->commands;
}
}

View file

@ -0,0 +1,205 @@
<?php
namespace Drupal\Core\Ajax;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Processes attachments of AJAX responses.
*
* @see \Drupal\Core\Ajax\AjaxResponse
* @see \Drupal\Core\Render\MainContent\AjaxRenderer
*/
class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface {
/**
* The asset resolver service.
*
* @var \Drupal\Core\Asset\AssetResolverInterface
*/
protected $assetResolver;
/**
* A config object for the system performance configuration.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The CSS asset collection renderer service.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $cssCollectionRenderer;
/**
* The JS asset collection renderer service.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $jsCollectionRenderer;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a AjaxResponseAttachmentsProcessor object.
*
* @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
* An asset resolver.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A config factory for retrieving required config objects.
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
* The CSS asset collection renderer.
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer
* The JS asset collection renderer.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
$this->jsCollectionRenderer = $js_collection_renderer;
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function processAttachments(AttachmentsInterface $response) {
// @todo Convert to assertion once https://www.drupal.org/node/2408013 lands
if (!$response instanceof AjaxResponse) {
throw new \InvalidArgumentException('\Drupal\Core\Ajax\AjaxResponse instance expected.');
}
$request = $this->requestStack->getCurrentRequest();
if ($response->getContent() == '{}') {
$response->setData($this->buildAttachmentsCommands($response, $request));
}
return $response;
}
/**
* Prepares the AJAX commands to attach assets.
*
* @param \Drupal\Core\Ajax\AjaxResponse $response
* The AJAX response to update.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object that the AJAX is responding to.
*
* @return array
* An array of commands ready to be returned as JSON.
*/
protected function buildAttachmentsCommands(AjaxResponse $response, Request $request) {
$ajax_page_state = $request->request->get('ajax_page_state');
// Aggregate CSS/JS if necessary, but only during normal site operation.
$optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess');
$optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess');
$attachments = $response->getAttachments();
// Resolve the attached libraries into asset collections.
$assets = new AttachedAssets();
$assets->setLibraries(isset($attachments['library']) ? $attachments['library'] : [])
->setAlreadyLoadedLibraries(isset($ajax_page_state['libraries']) ? explode(',', $ajax_page_state['libraries']) : [])
->setSettings(isset($attachments['drupalSettings']) ? $attachments['drupalSettings'] : []);
$css_assets = $this->assetResolver->getCssAssets($assets, $optimize_css);
list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js);
// First, AttachedAssets::setLibraries() ensures duplicate libraries are
// removed: it converts it to a set of libraries if necessary. Second,
// AssetResolver::getJsSettings() ensures $assets contains the final set of
// JavaScript settings. AttachmentsResponseProcessorInterface also mandates
// that the response it processes contains the final attachment values, so
// update both the 'library' and 'drupalSettings' attachments accordingly.
$attachments['library'] = $assets->getLibraries();
$attachments['drupalSettings'] = $assets->getSettings();
$response->setAttachments($attachments);
// Render the HTML to load these files, and add AJAX commands to insert this
// HTML in the page. Settings are handled separately, afterwards.
$settings = [];
if (isset($js_assets_header['drupalSettings'])) {
$settings = $js_assets_header['drupalSettings']['data'];
unset($js_assets_header['drupalSettings']);
}
if (isset($js_assets_footer['drupalSettings'])) {
$settings = $js_assets_footer['drupalSettings']['data'];
unset($js_assets_footer['drupalSettings']);
}
// Prepend commands to add the assets, preserving their relative order.
$resource_commands = array();
if ($css_assets) {
$css_render_array = $this->cssCollectionRenderer->render($css_assets);
$resource_commands[] = new AddCssCommand($this->renderer->renderPlain($css_render_array));
}
if ($js_assets_header) {
$js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
$resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array));
}
if ($js_assets_footer) {
$js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
$resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array));
}
foreach (array_reverse($resource_commands) as $resource_command) {
$response->addCommand($resource_command, TRUE);
}
// Prepend a command to merge changes and additions to drupalSettings.
if (!empty($settings)) {
// During Ajax requests basic path-specific settings are excluded from
// new drupalSettings values. The original page where this request comes
// from already has the right values. An Ajax request would update them
// with values for the Ajax request and incorrectly override the page's
// values.
// @see system_js_settings_alter()
unset($settings['path']);
$response->addCommand(new SettingsCommand($settings, TRUE), TRUE);
}
$commands = $response->getCommands();
$this->moduleHandler->alter('ajax_render', $commands);
return $commands;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for a javascript alert box.
*
* @ingroup ajax
*/
class AlertCommand implements CommandInterface {
/**
* The text to be displayed in the alert box.
*
* @var string
*/
protected $text;
/**
* Constructs an AlertCommand object.
*
* @param string $text
* The text to be displayed in the alert box.
*/
public function __construct($text) {
$this->text = $text;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'alert',
'text' => $this->text,
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Core\Ajax;
/**
* An AJAX command for calling the jQuery append() method.
*
* The 'insert/append' command instructs the client to use jQuery's append()
* method to append the given HTML content to the inside of each element matched
* by the given selector.
*
* This command is implemented by Drupal.AjaxCommands.prototype.insert()
* defined in misc/ajax.js.
*
* @see http://docs.jquery.com/Manipulation/append#content
*
* @ingroup ajax
*/
class AppendCommand extends InsertCommand {
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'insert',
'method' => 'append',
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Base command that only exists to simplify AJAX commands.
*/
class BaseCommand implements CommandInterface {
/**
* The name of the command.
*
* @var string
*/
protected $command;
/**
* The data to pass on to the client side.
*
* @var string
*/
protected $data;
/**
* Constructs a BaseCommand object.
*
* @param string $command
* The name of the command.
* @param string $data
* The data to pass on to the client side.
*/
public function __construct($command, $data) {
$this->command = $command;
$this->data = $data;
}
/**
* {@inheritdoc}
*/
public function render() {
return array(
'command' => $this->command,
'data' => $this->data,
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Core\Ajax;
/**
* An AJAX command for calling the jQuery before() method.
*
* The 'insert/before' command instructs the client to use jQuery's before()
* method to insert the given HTML content before each of elements matched by
* the given selector.
*
* This command is implemented by Drupal.AjaxCommands.prototype.insert()
* defined in misc/ajax.js.
*
* @see http://docs.jquery.com/Manipulation/before#content
*
* @ingroup ajax
*/
class BeforeCommand extends InsertCommand {
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'insert',
'method' => 'before',
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\Core\Ajax;
/**
* An AJAX command for marking HTML elements as changed.
*
* This command instructs the client to mark each of the elements matched by the
* given selector as 'ajax-changed'.
*
* This command is implemented by Drupal.AjaxCommands.prototype.changed()
* defined in misc/ajax.js.
*
* @ingroup ajax
*/
class ChangedCommand implements CommandInterface {
/**
* A CSS selector string.
*
* If the command is a response to a request from an #ajax form element then
* this value can be NULL.
*
* @var string
*/
protected $selector;
/**
* An optional CSS selector for elements to which asterisks will be appended.
*
* @var string
*/
protected $asterisk;
/**
* Constructs a ChangedCommand object.
*
* @param string $selector
* CSS selector for elements to be marked as changed.
* @param string $asterisk
* CSS selector for elements to which an asterisk will be appended.
*/
public function __construct($selector, $asterisk = '') {
$this->selector = $selector;
$this->asterisk = $asterisk;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'changed',
'selector' => $this->selector,
'asterisk' => $this->asterisk,
);
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Defines an AJAX command that closes the current active dialog.
*
* @ingroup ajax
*/
class CloseDialogCommand implements CommandInterface {
/**
* A CSS selector string of the dialog to close.
*
* @var string
*/
protected $selector;
/**
* Whether to persist the dialog in the DOM or not.
*
* @var bool
*/
protected $persist;
/**
* Constructs a CloseDialogCommand object.
*
* @param string $selector
* A CSS selector string of the dialog to close.
* @param bool $persist
* (optional) Whether to persist the dialog in the DOM or not.
*/
public function __construct($selector = NULL, $persist = FALSE) {
$this->selector = $selector ? $selector : '#drupal-modal';
$this->persist = $persist;
}
/**
* {@inheritdoc}
*/
public function render() {
return array(
'command' => 'closeDialog',
'selector' => $this->selector,
'persist' => $this->persist,
);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Defines an AJAX command that closes the currently visible modal dialog.
*
* @ingroup ajax
*/
class CloseModalDialogCommand extends CloseDialogCommand {
/**
* Constructs a CloseModalDialogCommand object.
*
* @param bool $persist
* (optional) Whether to persist the dialog in the DOM or not.
*/
public function __construct($persist = FALSE) {
$this->selector = '#drupal-modal';
$this->persist = $persist;
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command interface.
*
* All AJAX commands passed to AjaxResponse objects should implement these
* methods.
*
* @ingroup ajax
*/
interface CommandInterface {
/**
* Return an array to be run through json_encode and sent to the client.
*/
public function render();
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Interface for Ajax commands that render content and attach assets.
*
* All Ajax commands that render HTML should implement these methods
* to be able to return attached assets to the calling AjaxResponse object.
*
* @ingroup ajax
*/
interface CommandWithAttachedAssetsInterface {
/**
* Gets the attached assets.
*
* @return \Drupal\Core\Asset\AttachedAssets|null
* The attached assets for this command.
*/
public function getAttachedAssets();
}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\Core\Ajax;
use Drupal\Core\Asset\AttachedAssets;
/**
* Trait for Ajax commands that render content and attach assets.
*
* @ingroup ajax
*/
trait CommandWithAttachedAssetsTrait {
/**
* The attached assets for this Ajax command.
*
* @var \Drupal\Core\Asset\AttachedAssets
*/
protected $attachedAssets;
/**
* Processes the content for output.
*
* If content is a render array, it may contain attached assets to be
* processed.
*
* @return string|\Drupal\Component\Render\MarkupInterface
* HTML rendered content.
*/
protected function getRenderedContent() {
$this->attachedAssets = new AttachedAssets();
if (is_array($this->content)) {
$html = \Drupal::service('renderer')->renderRoot($this->content);
$this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
return $html;
}
else {
return $this->content;
}
}
/**
* Gets the attached assets.
*
* @return \Drupal\Core\Asset\AttachedAssets|null
* The attached assets for this command.
*/
public function getAttachedAssets() {
return $this->attachedAssets;
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\Core\Ajax;
/**
* An AJAX command for calling the jQuery css() method.
*
* The 'css' command will instruct the client to use the jQuery css() method to
* apply the CSS arguments to elements matched by the given selector.
*
* This command is implemented by Drupal.AjaxCommands.prototype.css() defined
* in misc/ajax.js.
*
* @see http://docs.jquery.com/CSS/css#properties
*
* @ingroup ajax
*/
class CssCommand implements CommandInterface {
/**
* A CSS selector string.
*
* If the command is a response to a request from an #ajax form element then
* this value can be NULL.
*
* @var string
*/
protected $selector;
/**
* An array of property/value pairs to set in the CSS for the selector.
*
* @var array
*/
protected $css = array();
/**
* Constructs a CssCommand object.
*
* @param string $selector
* A CSS selector for elements to which the CSS will be applied.
* @param array $css
* An array of CSS property/value pairs to set.
*/
public function __construct($selector, array $css = array()) {
$this->selector = $selector;
$this->css = $css;
}
/**
* Adds a property/value pair to the CSS to be added to this element.
*
* @param $property
* The CSS property to be changed.
* @param $value
* The new value of the CSS property.
*
* @return $this
*/
public function setProperty($property, $value) {
$this->css[$property] = $value;
return $this;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'css',
'selector' => $this->selector,
'argument' => $this->css,
);
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Drupal\Core\Ajax;
/**
* An AJAX command for implementing jQuery's data() method.
*
* This instructs the client to attach the name=value pair of data to the
* selector via jQuery's data cache.
*
* This command is implemented by Drupal.AjaxCommands.prototype.data() defined
* in misc/ajax.js.
*
* @ingroup ajax
*/
class DataCommand implements CommandInterface {
/**
* A CSS selector string for elements to which data will be attached.
*
* If the command is a response to a request from an #ajax form element then
* this value can be NULL.
*
* @var string
*/
protected $selector;
/**
* The key of the data attached to elements matched by the selector.
*
* @var string
*/
protected $name;
/**
* The value of the data to be attached to elements matched by the selector.
*
* The data is not limited to strings; it can be any format.
*
* @var mixed
*/
protected $value;
/**
* Constructs a DataCommand object.
*
* @param string $selector
* A CSS selector for the elements to which the data will be attached.
* @param string $name
* The key of the data to be attached to elements matched by the selector.
* @param mixed $value
* The value of the data to be attached to elements matched by the selector.
*/
public function __construct($selector, $name, $value) {
$this->selector = $selector;
$this->name = $name;
$this->value = $value;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'data',
'selector' => $this->selector,
'name' => $this->name,
'value' => $this->value,
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for calling the jQuery html() method.
*
* The 'insert/html' command instructs the client to use jQuery's html() method
* to set the HTML content of each element matched by the given selector while
* leaving the outer tags intact.
*
* This command is implemented by Drupal.AjaxCommands.prototype.insert()
* defined in misc/ajax.js.
*
* @see http://docs.jquery.com/Attributes/html#val
*
* @ingroup ajax
*/
class HtmlCommand extends InsertCommand {
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'insert',
'method' => 'html',
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Generic AJAX command for inserting content.
*
* This command instructs the client to insert the given HTML using whichever
* jQuery DOM manipulation method has been specified in the #ajax['method']
* variable of the element that triggered the request.
*
* This command is implemented by Drupal.AjaxCommands.prototype.insert()
* defined in misc/ajax.js.
*
* @ingroup ajax
*/
class InsertCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
use CommandWithAttachedAssetsTrait;
/**
* A CSS selector string.
*
* If the command is a response to a request from an #ajax form element then
* this value can be NULL.
*
* @var string
*/
protected $selector;
/**
* The content for the matched element(s).
*
* Either a render array or an HTML string.
*
* @var string|array
*/
protected $content;
/**
* A settings array to be passed to any attached JavaScript behavior.
*
* @var array
*/
protected $settings;
/**
* Constructs an InsertCommand object.
*
* @param string $selector
* A CSS selector.
* @param string|array $content
* The content that will be inserted in the matched element(s), either a
* render array or an HTML string.
* @param array $settings
* An array of JavaScript settings to be passed to any attached behaviors.
*/
public function __construct($selector, $content, array $settings = NULL) {
$this->selector = $selector;
$this->content = $content;
$this->settings = $settings;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'insert',
'method' => NULL,
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for invoking an arbitrary jQuery method.
*
* The 'invoke' command will instruct the client to invoke the given jQuery
* method with the supplied arguments on the elements matched by the given
* selector. Intended for simple jQuery commands, such as attr(), addClass(),
* removeClass(), toggleClass(), etc.
*
* This command is implemented by Drupal.AjaxCommands.prototype.invoke()
* defined in misc/ajax.js.
*
* @ingroup ajax
*/
class InvokeCommand implements CommandInterface {
/**
* A CSS selector string.
*
* If the command is a response to a request from an #ajax form element then
* this value can be NULL.
*
* @var string
*/
protected $selector;
/**
* A jQuery method to invoke.
*
* @var string
*/
protected $method;
/**
* An optional list of arguments to pass to the method.
*
* @var array
*/
protected $arguments;
/**
* Constructs an InvokeCommand object.
*
* @param string $selector
* A jQuery selector.
* @param string $method
* The name of a jQuery method to invoke.
* @param array $arguments
* An optional array of arguments to pass to the method.
*/
public function __construct($selector, $method, array $arguments = array()) {
$this->selector = $selector;
$this->method = $method;
$this->arguments = $arguments;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'invoke',
'selector' => $this->selector,
'method' => $this->method,
'args' => $this->arguments,
);
}
}

View file

@ -0,0 +1,140 @@
<?php
namespace Drupal\Core\Ajax;
use Drupal\Component\Render\PlainTextOutput;
/**
* Defines an AJAX command to open certain content in a dialog.
*
* @ingroup ajax
*/
class OpenDialogCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
use CommandWithAttachedAssetsTrait;
/**
* The selector of the dialog.
*
* @var string
*/
protected $selector;
/**
* The title of the dialog.
*
* @var string
*/
protected $title;
/**
* The content for the dialog.
*
* Either a render array or an HTML string.
*
* @var string|array
*/
protected $content;
/**
* Stores dialog-specific options passed directly to jQuery UI dialogs. Any
* jQuery UI option can be used. See http://api.jqueryui.com/dialog.
*
* @var array
*/
protected $dialogOptions;
/**
* Custom settings that will be passed to the Drupal behaviors on the content
* of the dialog.
*
* @var array
*/
protected $settings;
/**
* Constructs an OpenDialogCommand object.
*
* @param string $selector
* The selector of the dialog.
* @param string $title
* The title of the dialog.
* @param string|array $content
* The content that will be placed in the dialog, either a render array
* or an HTML string.
* @param array $dialog_options
* (optional) Options to be passed to the dialog implementation. Any
* jQuery UI option can be used. See http://api.jqueryui.com/dialog.
* @param array|null $settings
* (optional) Custom settings that will be passed to the Drupal behaviors
* on the content of the dialog. If left empty, the settings will be
* populated automatically from the current request.
*/
public function __construct($selector, $title, $content, array $dialog_options = array(), $settings = NULL) {
$title = PlainTextOutput::renderFromHtml($title);
$dialog_options += array('title' => $title);
$this->selector = $selector;
$this->content = $content;
$this->dialogOptions = $dialog_options;
$this->settings = $settings;
}
/**
* Returns the dialog options.
*
* @return array
*/
public function getDialogOptions() {
return $this->dialogOptions;
}
/**
* Sets the dialog options array.
*
* @param array $dialog_options
* Options to be passed to the dialog implementation. Any jQuery UI option
* can be used. See http://api.jqueryui.com/dialog.
*/
public function setDialogOptions($dialog_options) {
$this->dialogOptions = $dialog_options;
}
/**
* Sets a single dialog option value.
*
* @param string $key
* Key of the dialog option. Any jQuery UI option can be used.
* See http://api.jqueryui.com/dialog.
* @param mixed $value
* Option to be passed to the dialog implementation.
*/
public function setDialogOption($key, $value) {
$this->dialogOptions[$key] = $value;
}
/**
* Sets the dialog title (an alias of setDialogOptions).
*
* @param string $title
* The new title of the dialog.
*/
public function setDialogTitle($title) {
$this->setDialogOptions('title', $title);
}
/**
* Implements \Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
// For consistency ensure the modal option is set to TRUE or FALSE.
$this->dialogOptions['modal'] = isset($this->dialogOptions['modal']) && $this->dialogOptions['modal'];
return array(
'command' => 'openDialog',
'selector' => $this->selector,
'settings' => $this->settings,
'data' => $this->getRenderedContent(),
'dialogOptions' => $this->dialogOptions,
);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Defines an AJAX command to open certain content in a dialog in a modal dialog.
*
* @ingroup ajax
*/
class OpenModalDialogCommand extends OpenDialogCommand {
/**
* Constructs an OpenModalDialog object.
*
* The modal dialog differs from the normal modal provided by
* OpenDialogCommand in that a modal prevents other interactions on the page
* until the modal has been completed. Drupal provides a built-in modal for
* this purpose, so no selector needs to be provided.
*
* @param string $title
* The title of the dialog.
* @param string|array $content
* The content that will be placed in the dialog, either a render array
* or an HTML string.
* @param array $dialog_options
* (optional) Settings to be passed to the dialog implementation. Any
* jQuery UI option can be used. See http://api.jqueryui.com/dialog.
* @param array|null $settings
* (optional) Custom settings that will be passed to the Drupal behaviors
* on the content of the dialog. If left empty, the settings will be
* populated automatically from the current request.
*/
public function __construct($title, $content, array $dialog_options = array(), $settings = NULL) {
$dialog_options['modal'] = TRUE;
parent::__construct('#drupal-modal', $title, $content, $dialog_options, $settings);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for calling the jQuery insert() method.
*
* The 'insert/prepend' command instructs the client to use jQuery's prepend()
* method to prepend the given HTML content to the inside each element matched
* by the given selector.
*
* This command is implemented by Drupal.AjaxCommands.prototype.insert()
* defined in misc/ajax.js.
*
* @see http://docs.jquery.com/Manipulation/prepend#content
*
* @ingroup ajax
*/
class PrependCommand extends InsertCommand {
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'insert',
'method' => 'prepend',
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Defines an AJAX command to set the window.location, loading that URL.
*
* @ingroup ajax
*/
class RedirectCommand implements CommandInterface {
/**
* The URL that will be loaded into window.location.
*
* @var string
*/
protected $url;
/**
* Constructs an RedirectCommand object.
*
* @param string $url
* The URL that will be loaded into window.location. This should be a full
* URL.
*/
public function __construct($url) {
$this->url = $url;
}
/**
* Implements \Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'redirect',
'url' => $this->url,
);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for calling the jQuery remove() method.
*
* The 'remove' command instructs the client to use jQuery's remove() method
* to remove each of elements matched by the given selector, and everything
* within them.
*
* This command is implemented by Drupal.AjaxCommands.prototype.remove()
* defined in misc/ajax.js.
*
* @see http://docs.jquery.com/Manipulation/remove#expr
*
* @ingroup ajax
*/
class RemoveCommand implements CommandInterface {
/**
* The CSS selector for the element(s) to be removed.
*
* @var string
*/
protected $selector;
/**
* Constructs a RemoveCommand object.
*
* @param string $selector
*/
public function __construct($selector) {
$this->selector = $selector;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'remove',
'selector' => $this->selector,
);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for calling the jQuery replace() method.
*
* The 'insert/replaceWith' command instructs the client to use jQuery's
* replaceWith() method to replace each element matched by the given selector
* with the given HTML.
*
* This command is implemented by Drupal.AjaxCommands.prototype.insert()
* defined in misc/ajax.js.
*
* See
* @link http://docs.jquery.com/Manipulation/replaceWith#content jQuery replaceWith command @endlink
*
* @ingroup ajax
*/
class ReplaceCommand extends InsertCommand {
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'insert',
'method' => 'replaceWith',
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for resetting the striping on a table.
*
* The 'restripe' command instructs the client to restripe a table. This is
* usually used after a table has been modified by a replace or append command.
*
* This command is implemented by Drupal.AjaxCommands.prototype.restripe()
* defined in misc/ajax.js.
*
* @ingroup ajax
*/
class RestripeCommand implements CommandInterface {
/**
* A CSS selector string.
*
* If the command is a response to a request from an #ajax form element then
* this value can be NULL.
*
* @var string
*/
protected $selector;
/**
* Constructs a RestripeCommand object.
*
* @param string $selector
* A CSS selector for the table to be restriped.
*/
public function __construct($selector) {
$this->selector = $selector;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'restripe',
'selector' => $this->selector,
);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Defines an AJAX command that sets jQuery UI dialog properties.
*
* @ingroup ajax
*/
class SetDialogOptionCommand implements CommandInterface {
/**
* A CSS selector string.
*
* @var string
*/
protected $selector;
/**
* A jQuery UI dialog option name.
*
* @var string
*/
protected $optionName;
/**
* A jQuery UI dialog option value.
*
* @var mixed
*/
protected $optionValue;
/**
* Constructs a SetDialogOptionCommand object.
*
* @param string $selector
* The selector of the dialog whose title will be set. If set to an empty
* value, the default modal dialog will be selected.
* @param string $option_name
* The name of the option to set. May be any jQuery UI dialog option.
* See http://api.jqueryui.com/dialog.
* @param mixed $option_value
* The value of the option to be passed to the dialog.
*/
public function __construct($selector, $option_name, $option_value) {
$this->selector = $selector ? $selector : '#drupal-modal';
$this->optionName = $option_name;
$this->optionValue = $option_value;
}
/**
* {@inheritdoc}
*/
public function render() {
return array(
'command' => 'setDialogOption',
'selector' => $this->selector,
'optionName' => $this->optionName,
'optionValue' => $this->optionValue,
);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\Core\Ajax;
/**
* Defines an AJAX command that sets jQuery UI dialog properties.
*
* @ingroup ajax
*/
class SetDialogTitleCommand extends SetDialogOptionCommand {
/**
* Constructs a SetDialogTitleCommand object.
*
* @param string $selector
* The selector of the dialog whose title will be set. If set to an empty
* value, the default modal dialog will be selected.
* @param string $title
* The title that will be set on the dialog.
*/
public function __construct($selector, $title) {
$this->selector = $selector ? $selector : '#drupal-modal';
$this->optionName = 'title';
$this->optionValue = $title;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for adjusting Drupal's JavaScript settings.
*
* The 'settings' command instructs the client either to use the given array as
* the settings for ajax-loaded content or to extend drupalSettings with the
* given array, depending on the value of the $merge parameter.
*
* This command is implemented by Drupal.AjaxCommands.prototype.settings()
* defined in misc/ajax.js.
*
* @ingroup ajax
*/
class SettingsCommand implements CommandInterface {
/**
* An array of key/value pairs of JavaScript settings.
*
* This will be used for all commands after this if they do not include their
* own settings array.
*
* @var array
*/
protected $settings;
/**
* Whether the settings should be merged into the global drupalSettings.
*
* By default (FALSE), the settings that are passed to Drupal.attachBehaviors
* will not include the global drupalSettings.
*
* @var bool
*/
protected $merge;
/**
* Constructs a SettingsCommand object.
*
* @param array $settings
* An array of key/value pairs of JavaScript settings.
* @param bool $merge
* Whether the settings should be merged into the global drupalSettings.
*/
public function __construct(array $settings, $merge = FALSE) {
$this->settings = $settings;
$this->merge = $merge;
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return array(
'command' => 'settings',
'settings' => $this->settings,
'merge' => $this->merge,
);
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\Core\Ajax;
/**
* AJAX command for updating the value of a hidden form_build_id input element
* on a form. It requires the form passed in to have keys for both the old build
* ID in #build_id_old and the new build ID in #build_id.
*
* The primary use case for this Ajax command is to serve a new build ID to a
* form served from the cache to an anonymous user, preventing one anonymous
* user from accessing the form state of another anonymous user on Ajax enabled
* forms.
*
* This command is implemented by
* Drupal.AjaxCommands.prototype.update_build_id() defined in misc/ajax.js.
*O
* @ingroup ajax
*/
class UpdateBuildIdCommand implements CommandInterface {
/**
* Old build id.
*
* @var string
*/
protected $old;
/**
* New build id.
*
* @var string
*/
protected $new;
/**
* Constructs a UpdateBuildIdCommand object.
*
* @param string $old
* The old build_id.
* @param string $new
* The new build_id.
*/
public function __construct($old, $new) {
$this->old = $old;
$this->new = $new;
}
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'update_build_id',
'old' => $this->old,
'new' => $this->new,
];
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an Action annotation object.
*
* Plugin Namespace: Plugin\Action
*
* For a working example, see \Drupal\node\Plugin\Action\UnpublishNode
*
* @see \Drupal\Core\Action\ActionInterface
* @see \Drupal\Core\Action\ActionManager
* @see \Drupal\Core\Action\ActionBase
* @see plugin_api
*
* @Annotation
*/
class Action extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the action plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The route name for a confirmation form for this action.
*
* @todo Provide a more generic way to allow an action to be confirmed first.
*
* @var string (optional)
*/
public $confirm_form_route_name = '';
/**
* The entity type the action can apply to.
*
* @todo Replace with \Drupal\Core\Plugin\Context\Context.
*
* @var string
*/
public $type = '';
/**
* The category under which the action should be listed in the UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $category;
}

View file

@ -0,0 +1,134 @@
<?php
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* @defgroup plugin_context Annotation for context definition
* @{
* Describes how to use ContextDefinition annotation.
*
* When providing plugin annotations, contexts can be defined to support UI
* interactions through providing limits, and mapping contexts to appropriate
* plugins. Context definitions can be provided as such:
* @code
* context = {
* "node" = @ContextDefinition("entity:node")
* }
* @endcode
* Remove spaces after @ in your actual plugin - these are put into this sample
* code so that it is not recognized as an annotation.
*
* To add a label to a context definition use the "label" key:
* @code
* context = {
* "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
* }
* @endcode
*
* Contexts are required unless otherwise specified. To make an optional
* context use the "required" key:
* @code
* context = {
* "node" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Node"))
* }
* @endcode
*
* To define multiple contexts, simply provide different key names in the
* context array:
* @code
* context = {
* "artist" = @ContextDefinition("entity:node", label = @Translation("Artist")),
* "album" = @ContextDefinition("entity:node", label = @Translation("Album"))
* }
* @endcode
*
* Specifying a default value for the context definition:
* @code
* context = {
* "message" = @ContextDefinition("string",
* label = @Translation("Message"),
* default_value = @Translation("Checkout complete! Thank you for your purchase.")
* )
* }
* @endcode
*
* @see annotation
*
* @}
*/
/**
* Defines a context definition annotation object.
*
* Some plugins require various data contexts in order to function. This class
* supports that need by allowing the contexts to be easily defined within an
* annotation and return a ContextDefinitionInterface implementing class.
*
* @Annotation
*
* @ingroup plugin_context
*/
class ContextDefinition extends Plugin {
/**
* The ContextDefinitionInterface object.
*
* @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface
*/
protected $definition;
/**
* Constructs a new context definition object.
*
* @param array $values
* An associative array with the following keys:
* - value: The required data type.
* - label: (optional) The UI label of this context definition.
* - required: (optional) Whether the context definition is required.
* - multiple: (optional) Whether the context definition is multivalue.
* - description: (optional) The UI description of this context definition.
* - default_value: (optional) The default value in case the underlying
* value is not set.
* - class: (optional) A custom ContextDefinitionInterface class.
*
* @throws \Exception
* Thrown when the class key is specified with a non
* ContextDefinitionInterface implementing class.
*/
public function __construct(array $values) {
$values += array(
'required' => TRUE,
'multiple' => FALSE,
'default_value' => NULL,
);
// Annotation classes extract data from passed annotation classes directly
// used in the classes they pass to.
foreach (['label', 'description'] as $key) {
// @todo Remove this workaround in https://www.drupal.org/node/2362727.
if (isset($values[$key]) && $values[$key] instanceof TranslatableMarkup) {
$values[$key] = (string) $values[$key]->get();
}
else {
$values[$key] = NULL;
}
}
if (isset($values['class']) && !in_array('Drupal\Core\Plugin\Context\ContextDefinitionInterface', class_implements($values['class']))) {
throw new \Exception('ContextDefinition class must implement \Drupal\Core\Plugin\Context\ContextDefinitionInterface.');
}
$class = isset($values['class']) ? $values['class'] : 'Drupal\Core\Plugin\Context\ContextDefinition';
$this->definition = new $class($values['value'], $values['label'], $values['required'], $values['multiple'], $values['description'], $values['default_value']);
}
/**
* Returns the value of an annotation.
*
* @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface
*/
public function get() {
return $this->definition;
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Mail annotation object.
*
* Plugin Namespace: Plugin\Mail
*
* For a working example, see \Drupal\Core\Mail\Plugin\Mail\PhpMail
*
* @see \Drupal\Core\Mail\MailInterface
* @see \Drupal\Core\Mail\MailManager
* @see plugin_api
*
* @Annotation
*/
class Mail extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the mail plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* A short description of the mail plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
}

View file

@ -0,0 +1,105 @@
<?php
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\AnnotationBase;
/**
* Defines an annotation object for strings that require plural forms.
*
* Note that the return values for both 'singular' and 'plural' keys needs to be
* passed to
* \Drupal\Core\StringTranslation\TranslationInterface::formatPlural().
*
* For example, the annotation can look like this:
* @code
* label_count = @ PluralTranslation(
* singular = "@count item",
* plural = "@count items",
* context = "cart_items",
* ),
* @endcode
* Remove spaces after @ in your actual plugin - these are put into this sample
* code so that it is not recognized as annotation.
*
* Code samples that make use of this annotation class and the definition sample
* above:
* @code
* // Returns: 1 item
* $entity_type->getCountLabel(1);
*
* // Returns: 5 items
* $entity_type->getCountLabel(5);
* @endcode
*
* @see \Drupal\Core\Entity\EntityType::getSingularLabel()
* @see \Drupal\Core\Entity\EntityType::getPluralLabel()
* @see \Drupal\Core\Entity\EntityType::getCountLabel()
*
* @ingroup plugin_translatable
*
* @Annotation
*/
class PluralTranslation extends AnnotationBase {
/**
* The string for the singular case.
*
* @var string
*/
protected $singular;
/**
* The string for the plural case.
*
* @var string
*/
protected $plural;
/**
* The context the source strings belong to.
*
* @var string
*/
protected $context;
/**
* Constructs a new class instance.
*
* @param array $values
* An associative array with the following keys:
* - singular: The string for the singular case.
* - plural: The string for the plural case.
* - context: The context the source strings belong to.
*
* @throws \InvalidArgumentException
* Thrown when the keys 'singular' or 'plural' are missing from the $values
* array.
*/
public function __construct(array $values) {
if (!isset($values['singular'])) {
throw new \InvalidArgumentException('Missing "singular" value in the PluralTranslation annotation');
}
if (!isset($values['plural'])) {
throw new \InvalidArgumentException('Missing "plural" value in the PluralTranslation annotation');
}
$this->singular = $values['singular'];
$this->plural = $values['plural'];
if (isset($values['context'])) {
$this->context = $values['context'];
}
}
/**
* {@inheritdoc}
*/
public function get() {
return [
'singular' => $this->singular,
'plural' => $this->plural,
'context' => $this->context,
];
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Declare a worker class for processing a queue item.
*
* Worker plugins are used by some queues for processing the individual items
* in the queue. In that case, the ID of the worker plugin needs to match the
* machine name of a queue, so that you can retrieve the queue back end by
* calling \Drupal\Core\Queue\QueueFactory::get($plugin_id).
*
* \Drupal\Core\Cron::processQueues() processes queues that use workers; they
* can also be processed outside of the cron process.
*
* Some queues do not use worker plugins: you can create queues, add items to
* them, claim them, etc. without using a QueueWorker plugin. However, you will
* need to take care of processing the items in the queue in that case. You can
* look at \Drupal\Core\Cron::processQueues() for an example of how to process
* a queue that uses workers, and adapt it to your queue.
*
* Plugin Namespace: Plugin\QueueWorker
*
* For a working example, see
* \Drupal\aggregator\Plugin\QueueWorker\AggregatorRefresh.
*
* @see \Drupal\Core\Queue\QueueWorkerInterface
* @see \Drupal\Core\Queue\QueueWorkerBase
* @see \Drupal\Core\Queue\QueueWorkerManager
* @see plugin_api
*
* @Annotation
*/
class QueueWorker extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable title of the plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $title;
/**
* An associative array containing the optional key:
* - time: (optional) How much time Drupal cron should spend on calling
* this worker in seconds. Defaults to 15.
*
* @var array (optional)
*/
public $cron;
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\AnnotationBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* @defgroup plugin_translatable Annotation for translatable text
* @{
* Describes how to put translatable UI text into annotations.
*
* When providing plugin annotation, properties whose values are displayed in
* the user interface should be made translatable. Much the same as how user
* interface text elsewhere is wrapped in t() to make it translatable, in plugin
* annotation, wrap translatable strings in the @ Translation() annotation.
* For example:
* @code
* title = @ Translation("Title of the plugin"),
* @endcode
* Remove spaces after @ in your actual plugin - these are put into this sample
* code so that it is not recognized as annotation.
*
* To provide replacement values for placeholders, use the "arguments" array:
* @code
* title = @ Translation("Bundle !title", arguments = {"!title" = "Foo"}),
* @endcode
*
* It is also possible to provide a context with the text, similar to t():
* @code
* title = @ Translation("Bundle", context = "Validation"),
* @endcode
* Other t() arguments like language code are not valid to pass in. Only
* context is supported.
*
* @see i18n
* @see annotation
* @}
*/
/**
* Defines a translatable annotation object.
*
* Some metadata within an annotation needs to be translatable. This class
* supports that need by allowing both the translatable string and, if
* specified, a context for that string. The string (with optional context)
* is passed into t().
*
* @ingroup plugin_translatable
*
* @Annotation
*/
class Translation extends AnnotationBase {
/**
* The string translation object.
*
* @var \Drupal\Core\StringTranslation\TranslatableMarkup
*/
protected $translation;
/**
* Constructs a new class instance.
*
* Parses values passed into this class through the t() function in Drupal and
* handles an optional context for the string.
*
* @param array $values
* Possible array keys:
* - value (required): the string that is to be translated.
* - arguments (optional): an array with placeholder replacements, keyed by
* placeholder.
* - context (optional): a string that describes the context of "value";
*/
public function __construct(array $values) {
$string = $values['value'];
$arguments = isset($values['arguments']) ? $values['arguments'] : array();
$options = array();
if (!empty($values['context'])) {
$options = array(
'context' => $values['context'],
);
}
$this->translation = new TranslatableMarkup($string, $arguments, $options);
}
/**
* {@inheritdoc}
*/
public function get() {
return $this->translation;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\Core;
/**
* Gets the app root from the kernel.
*/
class AppRootFactory {
/**
* The Drupal kernel.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $drupalKernel;
/**
* Constructs an AppRootFactory instance.
*
* @param \Drupal\Core\DrupalKernelInterface $drupal_kernel
* The Drupal kernel.
*/
public function __construct(DrupalKernelInterface $drupal_kernel) {
$this->drupalKernel = $drupal_kernel;
}
/**
* Gets the app root.
*
* @return string
*/
public function get() {
return $this->drupalKernel->getAppRoot();
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\Core\Archiver\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an archiver annotation object.
*
* Plugin Namespace: Plugin\Archiver
*
* For a working example, see \Drupal\system\Plugin\Archiver\Zip
*
* @see \Drupal\Core\Archiver\ArchiverManager
* @see \Drupal\Core\Archiver\ArchiverInterface
* @see plugin_api
* @see hook_archiver_info_alter()
*
* @Annotation
*/
class Archiver extends Plugin {
/**
* The archiver plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the archiver plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $title;
/**
* The description of the archiver plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
/**
* An array of valid extensions for this archiver.
*
* @var array
*/
public $extensions;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
<?php
namespace Drupal\Core\Archiver;
/**
* Defines an exception class for Drupal\Core\Archiver\ArchiverInterface.
*/
class ArchiverException extends \Exception {}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\Core\Archiver;
/**
* Defines the common interface for all Archiver classes.
*
* @see \Drupal\Core\Archiver\ArchiverManager
* @see \Drupal\Core\Archiver\Annotation\Archiver
* @see plugin_api
*/
interface ArchiverInterface {
/**
* Adds the specified file or directory to the archive.
*
* @param string $file_path
* The full system path of the file or directory to add. Only local files
* and directories are supported.
*
* @return \Drupal\Core\Archiver\ArchiverInterface
* The called object.
*/
public function add($file_path);
/**
* Removes the specified file from the archive.
*
* @param string $path
* The file name relative to the root of the archive to remove.
*
* @return \Drupal\Core\Archiver\ArchiverInterface
* The called object.
*/
public function remove($path);
/**
* Extracts multiple files in the archive to the specified path.
*
* @param string $path
* A full system path of the directory to which to extract files.
* @param array $files
* Optionally specify a list of files to be extracted. Files are
* relative to the root of the archive. If not specified, all files
* in the archive will be extracted.
*
* @return \Drupal\Core\Archiver\ArchiverInterface
* The called object.
*/
public function extract($path, array $files = array());
/**
* Lists all files in the archive.
*
* @return array
* An array of file names relative to the root of the archive.
*/
public function listContents();
}

View file

@ -0,0 +1,64 @@
<?php
namespace Drupal\Core\Archiver;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Provides an Archiver plugin manager.
*
* @see \Drupal\Core\Archiver\Annotation\Archiver
* @see \Drupal\Core\Archiver\ArchiverInterface
* @see plugin_api
*/
class ArchiverManager extends DefaultPluginManager {
/**
* Constructs a ArchiverManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Archiver', $namespaces, $module_handler, 'Drupal\Core\Archiver\ArchiverInterface', 'Drupal\Core\Archiver\Annotation\Archiver');
$this->alterInfo('archiver_info');
$this->setCacheBackend($cache_backend, 'archiver_info_plugins');
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = array()) {
$plugin_definition = $this->getDefinition($plugin_id);
$plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition, 'Drupal\Core\Archiver\ArchiverInterface');
return new $plugin_class($configuration['filepath']);
}
/**
* {@inheritdoc}
*/
public function getInstance(array $options) {
$filepath = $options['filepath'];
foreach ($this->getDefinitions() as $plugin_id => $definition) {
foreach ($definition['extensions'] as $extension) {
// Because extensions may be multi-part, such as .tar.gz,
// we cannot use simpler approaches like substr() or pathinfo().
// This method isn't quite as clean but gets the job done.
// Also note that the file may not yet exist, so we cannot rely
// on fileinfo() or other disk-level utilities.
if (strrpos($filepath, '.' . $extension) === strlen($filepath) - strlen('.' . $extension)) {
return $this->createInstance($plugin_id, $options);
}
}
}
}
}

View file

@ -0,0 +1,91 @@
<?php
namespace Drupal\Core\Archiver;
/**
* Defines a archiver implementation for .tar files.
*/
class Tar implements ArchiverInterface {
/**
* The underlying ArchiveTar instance that does the heavy lifting.
*
* @var \Drupal\Core\Archiver\ArchiveTar
*/
protected $tar;
/**
* Constructs a Tar object.
*
* @param string $file_path
* The full system path of the archive to manipulate. Only local files
* are supported. If the file does not yet exist, it will be created if
* appropriate.
*
* @throws \Drupal\Core\Archiver\ArchiverException
*/
public function __construct($file_path) {
$this->tar = new ArchiveTar($file_path);
}
/**
* {@inheritdoc}
*/
public function add($file_path) {
$this->tar->add($file_path);
return $this;
}
/**
* {@inheritdoc}
*/
public function remove($file_path) {
// @todo Archive_Tar doesn't have a remove operation
// so we'll have to simulate it somehow, probably by
// creating a new archive with everything but the removed
// file.
return $this;
}
/**
* {@inheritdoc}
*/
public function extract($path, array $files = array()) {
if ($files) {
$this->tar->extractList($files, $path);
}
else {
$this->tar->extract($path);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function listContents() {
$files = array();
foreach ($this->tar->listContent() as $file_data) {
$files[] = $file_data['filename'];
}
return $files;
}
/**
* Retrieves the tar engine itself.
*
* In some cases it may be necessary to directly access the underlying
* Archive_Tar object for implementation-specific logic. This is for advanced
* use only as it is not shared by other implementations of ArchiveInterface.
*
* @return Archive_Tar
* The Archive_Tar object used by this object.
*/
public function getArchive() {
return $this->tar;
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace Drupal\Core\Archiver;
/**
* Defines a archiver implementation for .zip files.
*
* @link http://php.net/zip
*/
class Zip implements ArchiverInterface {
/**
* The underlying ZipArchive instance that does the heavy lifting.
*
* @var \ZipArchive
*/
protected $zip;
/**
* Constructs a Zip object.
*
* @param string $file_path
* The full system path of the archive to manipulate. Only local files
* are supported. If the file does not yet exist, it will be created if
* appropriate.
*
* @throws \Drupal\Core\Archiver\ArchiverException
*/
public function __construct($file_path) {
$this->zip = new \ZipArchive();
if ($this->zip->open($file_path) !== TRUE) {
throw new ArchiverException(t('Cannot open %file_path', array('%file_path' => $file_path)));
}
}
/**
* {@inheritdoc}
*/
public function add($file_path) {
$this->zip->addFile($file_path);
return $this;
}
/**
* {@inheritdoc}
*/
public function remove($file_path) {
$this->zip->deleteName($file_path);
return $this;
}
/**
* {@inheritdoc}
*/
public function extract($path, array $files = array()) {
if ($files) {
$this->zip->extractTo($path, $files);
}
else {
$this->zip->extractTo($path);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function listContents() {
$files = array();
for ($i = 0; $i < $this->zip->numFiles; $i++) {
$files[] = $this->zip->getNameIndex($i);
}
return $files;
}
/**
* Retrieves the zip engine itself.
*
* In some cases it may be necessary to directly access the underlying
* ZipArchive object for implementation-specific logic. This is for advanced
* use only as it is not shared by other implementations of ArchiveInterface.
*
* @return \ZipArchive
* The ZipArchive object used by this object.
*/
public function getArchive() {
return $this->zip;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\Core\Asset;
/**
* Interface defining a service that logically groups a collection of assets.
*/
interface AssetCollectionGrouperInterface {
/**
* Groups a collection of assets into logical groups of asset collections.
*
* @param array $assets
* An asset collection.
*
* @return array
* A sorted array of asset groups.
*/
public function group(array $assets);
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Core\Asset;
/**
* Interface defining a service that optimizes a collection of assets.
*/
interface AssetCollectionOptimizerInterface {
/**
* Optimizes a collection of assets.
*
* @param array $assets
* An asset collection.
*
* @return array
* An optimized asset collection.
*/
public function optimize(array $assets);
/**
* Returns all optimized asset collections assets.
*
* @return string[]
* URIs for all optimized asset collection assets.
*/
public function getAll();
/**
* Deletes all optimized asset collections assets.
*/
public function deleteAll();
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\Core\Asset;
/**
* Interface defining a service that generates a render array to render assets.
*/
interface AssetCollectionRendererInterface {
/**
* Renders an asset collection.
*
* @param array $assets
* An asset collection.
*
* @return array
* A render array to render the asset collection.
*/
public function render(array $assets);
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Crypt;
/**
* Dumps a CSS or JavaScript asset.
*/
class AssetDumper implements AssetDumperInterface {
/**
* {@inheritdoc}
*
* The file name for the CSS or JS cache file is generated from the hash of
* the aggregated contents of the files in $data. This forces proxies and
* browsers to download new CSS when the CSS changes.
*/
public function dump($data, $file_extension) {
// Prefix filename to prevent blocking by firewalls which reject files
// starting with "ad*".
$filename = $file_extension . '_' . Crypt::hashBase64($data) . '.' . $file_extension;
// Create the css/ or js/ path within the files folder.
$path = 'public://' . $file_extension;
$uri = $path . '/' . $filename;
// Create the CSS or JS file.
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) {
return FALSE;
}
// If CSS/JS gzip compression is enabled and the zlib extension is available
// then create a gzipped version of this file. This file is served
// conditionally to browsers that accept gzip using .htaccess rules.
// It's possible that the rewrite rules in .htaccess aren't working on this
// server, but there's no harm (other than the time spent generating the
// file) in generating the file anyway. Sites on servers where rewrite rules
// aren't working can set css.gzip to FALSE in order to skip
// generating a file that won't be used.
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get($file_extension . '.gzip')) {
if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) {
return FALSE;
}
}
return $uri;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\Core\Asset;
/**
* Interface defining a service that dumps an (optimized) asset.
*/
interface AssetDumperInterface {
/**
* Dumps an (optimized) asset to persistent storage.
*
* @param string $data
* An (optimized) asset's contents.
* @param string $file_extension
* The file extension of this asset.
*
* @return string
* An URI to access the dumped asset.
*/
public function dump($data, $file_extension);
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\Core\Asset;
/**
* Interface defining a service that optimizes an asset.
*/
interface AssetOptimizerInterface {
/**
* Optimizes an asset.
*
* @param array $asset
* An asset.
*
* @return string
* The optimized asset's contents.
*/
public function optimize(array $asset);
/**
* Removes unwanted content from an asset.
*
* @param string $content
* The content of an asset.
*
* @return string
* The cleaned asset's contents.
*/
public function clean($content);
}

View file

@ -0,0 +1,393 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* The default asset resolver.
*/
class AssetResolver implements AssetResolverInterface {
/**
* The library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
*/
protected $libraryDiscovery;
/**
* The library dependency resolver.
*
* @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
*/
protected $libraryDependencyResolver;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface $language_manager
*/
protected $languageManager;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Constructs a new AssetResolver instance.
*
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
* The library discovery service.
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver
* The library dependency resolver.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
*/
public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
$this->libraryDiscovery = $library_discovery;
$this->libraryDependencyResolver = $library_dependency_resolver;
$this->moduleHandler = $module_handler;
$this->themeManager = $theme_manager;
$this->languageManager = $language_manager;
$this->cache = $cache;
}
/**
* Returns the libraries that need to be loaded.
*
* For example, with core/a depending on core/c and core/b on core/d:
* @code
* $assets = new AttachedAssets();
* $assets->setLibraries(['core/a', 'core/b', 'core/c']);
* $assets->setAlreadyLoadedLibraries(['core/c']);
* $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d']
* @endcode
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
*
* @return string[]
* A list of libraries and their dependencies, in the order they should be
* loaded, excluding any libraries that have already been loaded.
*/
protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
return array_diff(
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()),
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())
);
}
/**
* {@inheritdoc}
*/
public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_library_info_alter().
$libraries_to_load = $this->getLibrariesToLoad($assets);
$cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
return $cached->data;
}
$css = [];
$default_options = [
'type' => 'file',
'group' => CSS_AGGREGATE_DEFAULT,
'weight' => 0,
'media' => 'all',
'preprocess' => TRUE,
'browsers' => [],
];
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['css'])) {
foreach ($definition['css'] as $options) {
$options += $default_options;
$options['browsers'] += [
'IE' => TRUE,
'!IE' => TRUE,
];
// Files with a query string cannot be preprocessed.
if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
$options['preprocess'] = FALSE;
}
// Always add a tiny value to the weight, to conserve the insertion
// order.
$options['weight'] += count($css) / 1000;
// CSS files are being keyed by the full path.
$css[$options['data']] = $options;
}
}
}
// Allow modules and themes to alter the CSS assets.
$this->moduleHandler->alter('css', $css, $assets);
$this->themeManager->alter('css', $css, $assets);
// Sort CSS items, so that they appear in the correct order.
uasort($css, 'static::sort');
// Allow themes to remove CSS files by CSS files full path and file name.
// @todo Remove in Drupal 9.0.x.
if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) {
foreach ($css as $key => $options) {
if (isset($stylesheet_remove[$key])) {
unset($css[$key]);
}
}
}
if ($optimize) {
$css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
}
$this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
return $css;
}
/**
* Returns the JavaScript settings assets for this response's libraries.
*
* Gathers all drupalSettings from all libraries in the attached assets
* collection and merges them.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
* @return array
* A (possibly optimized) collection of JavaScript assets.
*/
protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
$settings = [];
foreach ($this->getLibrariesToLoad($assets) as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['drupalSettings'])) {
$settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
}
}
return $settings;
}
/**
* {@inheritdoc}
*/
public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_library_info_alter(). Additionally add the current language to
// support translation of JavaScript files via hook_js_alter().
$libraries_to_load = $this->getLibrariesToLoad($assets);
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
}
else {
$javascript = [];
$default_options = [
'type' => 'file',
'group' => JS_DEFAULT,
'weight' => 0,
'cache' => TRUE,
'preprocess' => TRUE,
'attributes' => [],
'version' => NULL,
'browsers' => [],
];
// Collect all libraries that contain JS assets and are in the header.
$header_js_libraries = [];
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js']) && !empty($definition['header'])) {
$header_js_libraries[] = $library;
}
}
// The current list of header JS libraries are only those libraries that
// are in the header, but their dependencies must also be loaded for them
// to function correctly, so update the list with those.
$header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js'])) {
foreach ($definition['js'] as $options) {
$options += $default_options;
// 'scope' is a calculated option, based on which libraries are
// marked to be loaded from the header (see above).
$options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
// Preprocess can only be set if caching is enabled and no
// attributes are set.
$options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
// Always add a tiny value to the weight, to conserve the insertion
// order.
$options['weight'] += count($javascript) / 1000;
// Local and external files must keep their name as the associative
// key so the same JavaScript file is not added twice.
$javascript[$options['data']] = $options;
}
}
}
// Allow modules and themes to alter the JavaScript assets.
$this->moduleHandler->alter('js', $javascript, $assets);
$this->themeManager->alter('js', $javascript, $assets);
// Sort JavaScript assets, so that they appear in the correct order.
uasort($javascript, 'static::sort');
// Prepare the return value: filter JavaScript assets per scope.
$js_assets_header = [];
$js_assets_footer = [];
foreach ($javascript as $key => $item) {
if ($item['scope'] == 'header') {
$js_assets_header[$key] = $item;
}
elseif ($item['scope'] == 'footer') {
$js_assets_footer[$key] = $item;
}
}
if ($optimize) {
$collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
$js_assets_header = $collection_optimizer->optimize($js_assets_header);
$js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
}
// If the core/drupalSettings library is being loaded or is already
// loaded, get the JavaScript settings assets, and convert them into a
// single "regular" JavaScript asset.
$libraries_to_load = $this->getLibrariesToLoad($assets);
$settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
$settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
// Initialize settings to FALSE since they are not needed by default. This
// distinguishes between an empty array which must still allow
// hook_js_settings_alter() to be run.
$settings = FALSE;
if ($settings_required && $settings_have_changed) {
$settings = $this->getJsSettingsAssets($assets);
// Allow modules to add cached JavaScript settings.
foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
$function = $module . '_' . 'js_settings_build';
$function($settings, $assets);
}
}
$settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
$this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
}
if ($settings !== FALSE) {
// Attached settings override both library definitions and
// hook_js_settings_build().
$settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
// Allow modules and themes to alter the JavaScript settings.
$this->moduleHandler->alter('js_settings', $settings, $assets);
$this->themeManager->alter('js_settings', $settings, $assets);
// Update the $assets object accordingly, so that it reflects the final
// settings.
$assets->setSettings($settings);
$settings_as_inline_javascript = [
'type' => 'setting',
'group' => JS_SETTING,
'weight' => 0,
'browsers' => [],
'data' => $settings,
];
$settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
// Prepend to the list of JS assets, to render it first. Preferably in
// the footer, but in the header if necessary.
if ($settings_in_header) {
$js_assets_header = $settings_js_asset + $js_assets_header;
}
else {
$js_assets_footer = $settings_js_asset + $js_assets_footer;
}
}
return [
$js_assets_header,
$js_assets_footer,
];
}
/**
* Sorts CSS and JavaScript resources.
*
* This sort order helps optimize front-end performance while providing
* modules and themes with the necessary control for ordering the CSS and
* JavaScript appearing on a page.
*
* @param $a
* First item for comparison. The compared items should be associative
* arrays of member items.
* @param $b
* Second item for comparison.
*
* @return int
*/
public static function sort($a, $b) {
// First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
// group appear before items in the CSS_AGGREGATE_THEME group. Modules may
// create additional groups by defining their own constants.
if ($a['group'] < $b['group']) {
return -1;
}
elseif ($a['group'] > $b['group']) {
return 1;
}
// Finally, order by weight.
elseif ($a['weight'] < $b['weight']) {
return -1;
}
elseif ($a['weight'] > $b['weight']) {
return 1;
}
else {
return 0;
}
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Drupal\Core\Asset;
/**
* Resolves asset libraries into concrete CSS and JavaScript assets.
*
* Given an attached assets collection (to be loaded for the current response),
* the asset resolver can resolve those asset libraries into a list of concrete
* CSS and JavaScript assets.
*
* In other words: this allows developers to translate Drupal's asset
* abstraction (asset libraries) into concrete assets.
*
* @see \Drupal\Core\Asset\AttachedAssetsInterface
* @see \Drupal\Core\Asset\LibraryDependencyResolverInterface
*/
interface AssetResolverInterface {
/**
* Returns the CSS assets for the current response's libraries.
*
* It returns the CSS assets in order, according to the SMACSS categories
* specified in the assets' weights:
* - CSS_BASE
* - CSS_LAYOUT
* - CSS_COMPONENT
* - CSS_STATE
* - CSS_THEME
* @see https://www.drupal.org/node/1887918#separate-concerns
* This ensures proper cascading of styles so themes can easily override
* module styles through CSS selectors.
*
* Themes may replace module-defined CSS files by adding a stylesheet with the
* same filename. For example, themes/bartik/system-menus.css would replace
* modules/system/system-menus.css. This allows themes to override complete
* CSS files, rather than specific selectors, when necessary.
*
* Also invokes hook_css_alter(), to allow CSS assets to be altered.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
* @param bool $optimize
* Whether to apply the CSS asset collection optimizer, to return an
* optimized CSS asset collection rather than an unoptimized one.
*
* @return array
* A (possibly optimized) collection of CSS assets.
*/
public function getCssAssets(AttachedAssetsInterface $assets, $optimize);
/**
* Returns the JavaScript assets for the current response's libraries.
*
* References to JavaScript files are placed in a certain order: first, all
* 'core' files, then all 'module' and finally all 'theme' JavaScript files
* are added to the page. Then, all settings are output, followed by 'inline'
* JavaScript code. If running update.php, all preprocessing is disabled.
*
* Note that hook_js_alter(&$javascript) is called during this function call
* to allow alterations of the JavaScript during its presentation. The correct
* way to add JavaScript during hook_js_alter() is to add another element to
* the $javascript array, deriving from drupal_js_defaults(). See
* locale_js_alter() for an example of this.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
* Note that this object is modified to reflect the final JavaScript
* settings assets.
* @param bool $optimize
* Whether to apply the JavaScript asset collection optimizer, to return
* optimized JavaScript asset collections rather than an unoptimized ones.
*
* @return array
* A nested array containing 2 values:
* - at index zero: the (possibly optimized) collection of JavaScript assets
* for the top of the page
* - at index one: the (possibly optimized) collection of JavaScript assets
* for the bottom of the page
*/
public function getJsAssets(AttachedAssetsInterface $assets, $optimize);
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\Core\Asset;
/**
* The default attached assets collection.
*/
class AttachedAssets implements AttachedAssetsInterface {
/**
* The (ordered) list of asset libraries attached to the current response.
*
* @var string[]
*/
public $libraries = [];
/**
* The JavaScript settings attached to the current response.
*
* @var array
*/
public $settings = [];
/**
* The set of asset libraries that the client has already loaded.
*
* @var string[]
*/
protected $alreadyLoadedLibraries = [];
/**
* {@inheritdoc}
*/
public static function createFromRenderArray(array $render_array) {
if (!isset($render_array['#attached'])) {
throw new \LogicException('The render array has not yet been rendered, hence not all attachments have been collected yet.');
}
$assets = new static();
if (isset($render_array['#attached']['library'])) {
$assets->setLibraries($render_array['#attached']['library']);
}
if (isset($render_array['#attached']['drupalSettings'])) {
$assets->setSettings($render_array['#attached']['drupalSettings']);
}
return $assets;
}
/**
* {@inheritdoc}
*/
public function setLibraries(array $libraries) {
$this->libraries = array_unique($libraries);
return $this;
}
/**
* {@inheritdoc}
*/
public function getLibraries() {
return $this->libraries;
}
/**
* {@inheritdoc}
*/
public function setSettings(array $settings) {
$this->settings = $settings;
return $this;
}
/**
* {@inheritdoc}
*/
public function getSettings() {
return $this->settings;
}
/**
* {@inheritdoc}
*/
public function getAlreadyLoadedLibraries() {
return $this->alreadyLoadedLibraries;
}
/**
* {@inheritdoc}
*/
public function setAlreadyLoadedLibraries(array $libraries) {
$this->alreadyLoadedLibraries = $libraries;
return $this;
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Drupal\Core\Asset;
/**
* The attached assets collection for the current response.
*
* Allows for storage of:
* - an ordered list of asset libraries (to be loaded for the current response)
* - attached JavaScript settings (to be loaded for the current response)
* - a set of asset libraries that the client already has loaded (as indicated
* in the request, to *not* be loaded for the current response)
*
* @see \Drupal\Core\Asset\AssetResolverInterface
*/
interface AttachedAssetsInterface {
/**
* Creates an AttachedAssetsInterface object from a render array.
*
* @param array $render_array
* A render array.
*
* @return \Drupal\Core\Asset\AttachedAssetsInterface
*
* @throws \LogicException
*/
public static function createFromRenderArray(array $render_array);
/**
* Sets the asset libraries attached to the current response.
*
* @param string[] $libraries
* A list of libraries, in the order they should be loaded.
*
* @return $this
*/
public function setLibraries(array $libraries);
/**
* Returns the asset libraries attached to the current response.
*
* @return string[]
*/
public function getLibraries();
/**
* Sets the JavaScript settings that are attached to the current response.
*
* @param array $settings
* The needed JavaScript settings.
*
* @return $this
*/
public function setSettings(array $settings);
/**
* Returns the settings attached to the current response.
*
* @return array
*/
public function getSettings();
/**
* Sets the asset libraries that the current request marked as already loaded.
*
* @param string[] $libraries
* The set of already loaded libraries.
*
* @return $this
*/
public function setAlreadyLoadedLibraries(array $libraries);
/**
* Returns the set of already loaded asset libraries.
*
* @return string[]
*/
public function getAlreadyLoadedLibraries();
}

View file

@ -0,0 +1,85 @@
<?php
namespace Drupal\Core\Asset;
/**
* Groups CSS assets.
*/
class CssCollectionGrouper implements AssetCollectionGrouperInterface {
/**
* {@inheritdoc}
*
* Puts multiple items into the same group if they are groupable and if they
* are for the same 'media' and 'browsers'. Items of the 'file' type are
* groupable if their 'preprocess' flag is TRUE, and items of the 'external'
* type are never groupable.
*
* Also ensures that the process of grouping items does not change their
* relative order. This requirement may result in multiple groups for the same
* type, media, and browsers, if needed to accommodate other items in between.
*/
public function group(array $css_assets) {
$groups = array();
// If a group can contain multiple items, we track the information that must
// be the same for each item in the group, so that when we iterate the next
// item, we can determine if it can be put into the current group, or if a
// new group needs to be made for it.
$current_group_keys = NULL;
// When creating a new group, we pre-increment $i, so by initializing it to
// -1, the first group will have index 0.
$i = -1;
foreach ($css_assets as $item) {
// The browsers for which the CSS item needs to be loaded is part of the
// information that determines when a new group is needed, but the order
// of keys in the array doesn't matter, and we don't want a new group if
// all that's different is that order.
ksort($item['browsers']);
// If the item can be grouped with other items, set $group_keys to an
// array of information that must be the same for all items in its group.
// If the item can't be grouped with other items, set $group_keys to
// FALSE. We put items into a group that can be aggregated together:
// whether they will be aggregated is up to the _drupal_css_aggregate()
// function or an
// override of that function specified in hook_css_alter(), but regardless
// of the details of that function, a group represents items that can be
// aggregated. Since a group may be rendered with a single HTML tag, all
// items in the group must share the same information that would need to
// be part of that HTML tag.
switch ($item['type']) {
case 'file':
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['media'], $item['browsers']) : FALSE;
break;
case 'external':
// Do not group external items.
$group_keys = FALSE;
break;
}
// If the group keys don't match the most recent group we're working with,
// then a new group must be made.
if ($group_keys !== $current_group_keys) {
$i++;
// Initialize the new group with the same properties as the first item
// being placed into it. The item's 'data', 'weight' and 'basename'
// properties are unique to the item and should not be carried over to
// the group.
$groups[$i] = $item;
unset($groups[$i]['data'], $groups[$i]['weight'], $groups[$i]['basename']);
$groups[$i]['items'] = array();
$current_group_keys = $group_keys ? $group_keys : NULL;
}
// Add the item to the current group.
$groups[$i]['items'][] = $item;
}
return $groups;
}
}

View file

@ -0,0 +1,187 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Core\State\StateInterface;
/**
* Optimizes CSS assets.
*/
class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
/**
* A CSS asset grouper.
*
* @var \Drupal\Core\Asset\CssCollectionGrouper
*/
protected $grouper;
/**
* A CSS asset optimizer.
*
* @var \Drupal\Core\Asset\CssOptimizer
*/
protected $optimizer;
/**
* An asset dumper.
*
* @var \Drupal\Core\Asset\AssetDumper
*/
protected $dumper;
/**
* The state key/value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a CssCollectionOptimizer.
*
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
* The grouper for CSS assets.
* @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
* The optimizer for a single CSS asset.
* @param \Drupal\Core\Asset\AssetDumperInterface $dumper
* The dumper for optimized CSS assets.
* @param \Drupal\Core\State\StateInterface $state
* The state key/value store.
*/
public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) {
$this->grouper = $grouper;
$this->optimizer = $optimizer;
$this->dumper = $dumper;
$this->state = $state;
}
/**
* {@inheritdoc}
*
* The cache file name is retrieved on a page load via a lookup variable that
* contains an associative array. The array key is the hash of the file names
* in $css while the value is the cache file name. The cache file is generated
* in two cases. First, if there is no file name value for the key, which will
* happen if a new file name has been added to $css or after the lookup
* variable is emptied to force a rebuild of the cache. Second, the cache file
* is generated if it is missing on disk. Old cache files are not deleted
* immediately when the lookup variable is emptied, but are deleted after a
* configurable period (@code system.performance.stale_file_threshold @endcode)
* to ensure that files referenced by a cached page will still be available.
*/
public function optimize(array $css_assets) {
// Group the assets.
$css_groups = $this->grouper->group($css_assets);
// Now optimize (concatenate + minify) and dump each asset group, unless
// that was already done, in which case it should appear in
// drupal_css_cache_files.
// Drupal contrib can override this default CSS aggregator to keep the same
// grouping, optimizing and dumping, but change the strategy that is used to
// determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …).
$map = $this->state->get('drupal_css_cache_files') ?: array();
$css_assets = array();
foreach ($css_groups as $order => $css_group) {
// We have to return a single asset, not a group of assets. It is now up
// to one of the pieces of code in the switch statement below to set the
// 'data' property to the appropriate value.
$css_assets[$order] = $css_group;
unset($css_assets[$order]['items']);
switch ($css_group['type']) {
case 'file':
// No preprocessing, single CSS asset: just use the existing URI.
if (!$css_group['preprocess']) {
$uri = $css_group['items'][0]['data'];
$css_assets[$order]['data'] = $uri;
}
// Preprocess (aggregate), unless the aggregate file already exists.
else {
$key = $this->generateHash($css_group);
$uri = '';
if (isset($map[$key])) {
$uri = $map[$key];
}
if (empty($uri) || !file_exists($uri)) {
// Optimize each asset within the group.
$data = '';
foreach ($css_group['items'] as $css_asset) {
$data .= $this->optimizer->optimize($css_asset);
}
// Per the W3C specification at
// http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import
// rules must precede any other style, so we move those to the
// top.
$regexp = '/@import[^;]+;/i';
preg_match_all($regexp, $data, $matches);
$data = preg_replace($regexp, '', $data);
$data = implode('', $matches[0]) . $data;
// Dump the optimized CSS for this group into an aggregate file.
$uri = $this->dumper->dump($data, 'css');
// Set the URI for this group's aggregate file.
$css_assets[$order]['data'] = $uri;
// Persist the URI for this aggregate file.
$map[$key] = $uri;
$this->state->set('drupal_css_cache_files', $map);
}
else {
// Use the persisted URI for the optimized CSS file.
$css_assets[$order]['data'] = $uri;
}
$css_assets[$order]['preprocessed'] = TRUE;
}
break;
case 'external':
// We don't do any aggregation and hence also no caching for external
// CSS assets.
$uri = $css_group['items'][0]['data'];
$css_assets[$order]['data'] = $uri;
break;
}
}
return $css_assets;
}
/**
* Generate a hash for a given group of CSS assets.
*
* @param array $css_group
* A group of CSS assets.
*
* @return string
* A hash to uniquely identify the given group of CSS assets.
*/
protected function generateHash(array $css_group) {
$css_data = array();
foreach ($css_group['items'] as $css_file) {
$css_data[] = $css_file['data'];
}
return hash('sha256', serialize($css_data));
}
/**
* {@inheritdoc}
*/
public function getAll() {
return $this->state->get('drupal_css_cache_files');
}
/**
* {@inheritdoc}
*/
public function deleteAll() {
$this->state->delete('drupal_css_cache_files');
$delete_stale = function($uri) {
// Default stale file threshold is 30 days.
if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
file_unmanaged_delete($uri);
}
};
file_scan_directory('public://css', '/.*/', array('callback' => $delete_stale));
}
}

View file

@ -0,0 +1,218 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Html;
use Drupal\Core\State\StateInterface;
/**
* Renders CSS assets.
*
* For production websites, LINK tags are preferable to STYLE tags with @import
* statements, because:
* - They are the standard tag intended for linking to a resource.
* - On Firefox 2 and perhaps other browsers, CSS files included with @import
* statements don't get saved when saving the complete web page for offline
* use: https://www.drupal.org/node/145218.
* - On IE, if only LINK tags and no @import statements are used, all the CSS
* files are downloaded in parallel, resulting in faster page load, but if
* @import statements are used and span across multiple STYLE tags, all the
* ones from one STYLE tag must be downloaded before downloading begins for
* the next STYLE tag. Furthermore, IE7 does not support media declaration on
* the @import statement, so multiple STYLE tags must be used when different
* files are for different media types. Non-IE browsers always download in
* parallel, so this is an IE-specific performance quirk:
* http://www.stevesouders.com/blog/2009/04/09/dont-use-import/.
*
* However, IE has an annoying limit of 31 total CSS inclusion tags
* (https://www.drupal.org/node/228818) and LINK tags are limited to one file
* per tag, whereas STYLE tags can contain multiple @import statements allowing
* multiple files to be loaded per tag. When CSS aggregation is disabled, a
* Drupal site can easily have more than 31 CSS files that need to be loaded, so
* using LINK tags exclusively would result in a site that would display
* incorrectly in IE. Depending on different needs, different strategies can be
* employed to decide when to use LINK tags and when to use STYLE tags.
*
* The strategy employed by this class is to use LINK tags for all aggregate
* files and for all files that cannot be aggregated (e.g., if 'preprocess' is
* set to FALSE or the type is 'external'), and to use STYLE tags for groups
* of files that could be aggregated together but aren't (e.g., if the site-wide
* aggregation setting is disabled). This results in all LINK tags when
* aggregation is enabled, a guarantee that as many or only slightly more tags
* are used with aggregation disabled than enabled (so that if the limit were to
* be crossed with aggregation enabled, the site developer would also notice the
* problem while aggregation is disabled), and an easy way for a developer to
* view HTML source while aggregation is disabled and know what files will be
* aggregated together when aggregation becomes enabled.
*
* This class evaluates the aggregation enabled/disabled condition on a group
* by group basis by testing whether an aggregate file has been made for the
* group rather than by testing the site-wide aggregation setting. This allows
* this class to work correctly even if modules have implemented custom
* logic for grouping and aggregating files.
*/
class CssCollectionRenderer implements AssetCollectionRendererInterface {
/**
* The state key/value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a CssCollectionRenderer.
*
* @param \Drupal\Core\State\StateInterface $state
* The state key/value store.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function render(array $css_assets) {
$elements = array();
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
// flush, forcing browsers to load a new copy of the files, as the
// URL changed.
$query_string = $this->state->get('system.css_js_query_string') ?: '0';
// Defaults for LINK and STYLE elements.
$link_element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'link',
'#attributes' => array(
'rel' => 'stylesheet',
),
);
$style_element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'style',
);
// For filthy IE hack.
$current_ie_group_keys = NULL;
$get_ie_group_key = function ($css_asset) {
return array($css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['media'], $css_asset['browsers']);
};
// Loop through all CSS assets, by key, to allow for the special IE
// workaround.
$css_assets_keys = array_keys($css_assets);
for ($i = 0; $i < count($css_assets_keys); $i++) {
$css_asset = $css_assets[$css_assets_keys[$i]];
switch ($css_asset['type']) {
// For file items, there are three possibilities.
// - There are up to 31 CSS assets on the page (some of which may be
// aggregated). In this case, output a LINK tag for file CSS assets.
// - There are more than 31 CSS assets on the page, yet we must stay
// below IE<10's limit of 31 total CSS inclusion tags, we handle this
// in two ways:
// - file CSS assets that are not eligible for aggregation (their
// 'preprocess' flag has been set to FALSE): in this case, output a
// LINK tag.
// - file CSS assets that can be aggregated (and possibly have been):
// in this case, figure out which subsequent file CSS assets share
// the same key properties ('group', 'media' and 'browsers') and
// output this group into as few STYLE tags as possible (a STYLE
// tag may contain only 31 @import statements).
case 'file':
// The dummy query string needs to be added to the URL to control
// browser-caching.
$query_string_separator = (strpos($css_asset['data'], '?') !== FALSE) ? '&' : '?';
// As long as the current page will not run into IE's limit for CSS
// assets: output a LINK tag for a file CSS asset.
if (count($css_assets) <= 31) {
$element = $link_element_defaults;
$element['#attributes']['href'] = file_url_transform_relative(file_create_url($css_asset['data'])) . $query_string_separator . $query_string;
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
}
// The current page will run into IE's limits for CSS assets: work
// around these limits by performing a light form of grouping.
// Once Drupal only needs to support IE10 and later, we can drop this.
else {
// The file CSS asset is ineligible for aggregation: output it in a
// LINK tag.
if (!$css_asset['preprocess']) {
$element = $link_element_defaults;
$element['#attributes']['href'] = file_url_transform_relative(file_create_url($css_asset['data'])) . $query_string_separator . $query_string;
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
}
// The file CSS asset can be aggregated, but hasn't been: combine
// multiple items into as few STYLE tags as possible.
else {
$import = array();
// Start with the current CSS asset, iterate over subsequent CSS
// assets and find which ones have the same 'type', 'group',
// 'preprocess', 'media' and 'browsers' properties.
$j = $i;
$next_css_asset = $css_asset;
$current_ie_group_key = $get_ie_group_key($css_asset);
do {
// The dummy query string needs to be added to the URL to
// control browser-caching. IE7 does not support a media type on
// the @import statement, so we instead specify the media for
// the group on the STYLE tag.
$import[] = '@import url("' . Html::escape(file_url_transform_relative(file_create_url($next_css_asset['data'])) . '?' . $query_string) . '");';
// Move the outer for loop skip the next item, since we
// processed it here.
$i = $j;
// Retrieve next CSS asset, unless there is none: then break.
if ($j + 1 < count($css_assets_keys)) {
$j++;
$next_css_asset = $css_assets[$css_assets_keys[$j]];
}
else {
break;
}
} while ($get_ie_group_key($next_css_asset) == $current_ie_group_key);
// In addition to IE's limit of 31 total CSS inclusion tags, it
// also has a limit of 31 @import statements per STYLE tag.
while (!empty($import)) {
$import_batch = array_slice($import, 0, 31);
$import = array_slice($import, 31);
$element = $style_element_defaults;
// This simplifies the JavaScript regex, allowing each line
// (separated by \n) to be treated as a completely different
// string. This means that we can use ^ and $ on one line at a
// time, and not worry about style tags since they'll never
// match the regex.
$element['#value'] = "\n" . implode("\n", $import_batch) . "\n";
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
}
}
}
break;
// Output a LINK tag for an external CSS asset. The asset's 'data'
// property contains the full URL.
case 'external':
$element = $link_element_defaults;
$element['#attributes']['href'] = $css_asset['data'];
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
break;
default:
throw new \Exception('Invalid CSS asset type.');
}
}
return $elements;
}
}

View file

@ -0,0 +1,261 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Unicode;
/**
* Optimizes a CSS asset.
*/
class CssOptimizer implements AssetOptimizerInterface {
/**
* The base path used by rewriteFileURI().
*
* @var string
*/
public $rewriteFileURIBasePath;
/**
* {@inheritdoc}
*/
public function optimize(array $css_asset) {
if ($css_asset['type'] != 'file') {
throw new \Exception('Only file CSS assets can be optimized.');
}
if (!$css_asset['preprocess']) {
throw new \Exception('Only file CSS assets with preprocessing enabled can be optimized.');
}
return $this->processFile($css_asset);
}
/**
* Processes the contents of a CSS asset for cleanup.
*
* @param string $contents
* The contents of the CSS asset.
*
* @return string
* Contents of the CSS asset.
*/
public function clean($contents) {
// Remove multiple charset declarations for standards compliance (and fixing
// Safari problems).
$contents = preg_replace('/^@charset\s+[\'"](\S*?)\b[\'"];/i', '', $contents);
return $contents;
}
/**
* Build aggregate CSS file.
*/
protected function processFile($css_asset) {
$contents = $this->loadFile($css_asset['data'], TRUE);
$contents = $this->clean($contents);
// Get the parent directory of this file, relative to the Drupal root.
$css_base_path = substr($css_asset['data'], 0, strrpos($css_asset['data'], '/'));
// Store base path.
$this->rewriteFileURIBasePath = $css_base_path . '/';
// Anchor all paths in the CSS with its base URL, ignoring external and absolute paths.
return preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', array($this, 'rewriteFileURI'), $contents);
}
/**
* Loads the stylesheet and resolves all @import commands.
*
* Loads a stylesheet and replaces @import commands with the contents of the
* imported file. Use this instead of file_get_contents when processing
* stylesheets.
*
* The returned contents are compressed removing white space and comments only
* when CSS aggregation is enabled. This optimization will not apply for
* color.module enabled themes with CSS aggregation turned off.
*
* Note: the only reason this method is public is so color.module can call it;
* it is not on the AssetOptimizerInterface, so future refactorings can make
* it protected.
*
* @param $file
* Name of the stylesheet to be processed.
* @param $optimize
* Defines if CSS contents should be compressed or not.
* @param $reset_basepath
* Used internally to facilitate recursive resolution of @import commands.
*
* @return
* Contents of the stylesheet, including any resolved @import commands.
*/
public function loadFile($file, $optimize = NULL, $reset_basepath = TRUE) {
// These statics are not cache variables, so we don't use drupal_static().
static $_optimize, $basepath;
if ($reset_basepath) {
$basepath = '';
}
// Store the value of $optimize for preg_replace_callback with nested
// @import loops.
if (isset($optimize)) {
$_optimize = $optimize;
}
// Stylesheets are relative one to each other. Start by adding a base path
// prefix provided by the parent stylesheet (if necessary).
if ($basepath && !file_uri_scheme($file)) {
$file = $basepath . '/' . $file;
}
// Store the parent base path to restore it later.
$parent_base_path = $basepath;
// Set the current base path to process possible child imports.
$basepath = dirname($file);
// Load the CSS stylesheet. We suppress errors because themes may specify
// stylesheets in their .info.yml file that don't exist in the theme's path,
// but are merely there to disable certain module CSS files.
$content = '';
if ($contents = @file_get_contents($file)) {
// If a BOM is found, convert the file to UTF-8, then use substr() to
// remove the BOM from the result.
if ($encoding = (Unicode::encodingFromBOM($contents))) {
$contents = Unicode::substr(Unicode::convertToUtf8($contents, $encoding), 1);
}
// If no BOM, check for fallback encoding. Per CSS spec the regex is very strict.
elseif (preg_match('/^@charset "([^"]+)";/', $contents, $matches)) {
if ($matches[1] !== 'utf-8' && $matches[1] !== 'UTF-8') {
$contents = substr($contents, strlen($matches[0]));
$contents = Unicode::convertToUtf8($contents, $matches[1]);
}
}
// Return the processed stylesheet.
$content = $this->processCss($contents, $_optimize);
}
// Restore the parent base path as the file and its children are processed.
$basepath = $parent_base_path;
return $content;
}
/**
* Loads stylesheets recursively and returns contents with corrected paths.
*
* This function is used for recursive loading of stylesheets and
* returns the stylesheet content with all url() paths corrected.
*
* @param array $matches
* An array of matches by a preg_replace_callback() call that scans for
* @import-ed CSS files, except for external CSS files.
*
* @return
* The contents of the CSS file at $matches[1], with corrected paths.
*
* @see \Drupal\Core\Asset\AssetOptimizerInterface::loadFile()
*/
protected function loadNestedFile($matches) {
$filename = $matches[1];
// Load the imported stylesheet and replace @import commands in there as
// well.
$file = $this->loadFile($filename, NULL, FALSE);
// Determine the file's directory.
$directory = dirname($filename);
// If the file is in the current directory, make sure '.' doesn't appear in
// the url() path.
$directory = $directory == '.' ? '' : $directory . '/';
// Alter all internal url() paths. Leave external paths alone. We don't need
// to normalize absolute paths here because that will be done later.
return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)([^\'")]+)([\'"]?)\s*\)/i', 'url(\1' . $directory . '\2\3)', $file);
}
/**
* Processes the contents of a stylesheet for aggregation.
*
* @param $contents
* The contents of the stylesheet.
* @param $optimize
* (optional) Boolean whether CSS contents should be minified. Defaults to
* FALSE.
*
* @return
* Contents of the stylesheet including the imported stylesheets.
*/
protected function processCss($contents, $optimize = FALSE) {
// Remove unwanted CSS code that cause issues.
$contents = $this->clean($contents);
if ($optimize) {
// Perform some safe CSS optimizations.
// Regexp to match comment blocks.
$comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
// Regexp to match double quoted strings.
$double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
// Regexp to match single quoted strings.
$single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
// Strip all comment blocks, but keep double/single quoted strings.
$contents = preg_replace(
"<($double_quot|$single_quot)|$comment>Ss",
"$1",
$contents
);
// Remove certain whitespace.
// There are different conditions for removing leading and trailing
// whitespace.
// @see http://php.net/manual/regexp.reference.subpatterns.php
$contents = preg_replace('<
# Strip leading and trailing whitespace.
\s*([@{};,])\s*
# Strip only leading whitespace from:
# - Closing parenthesis: Retain "@media (bar) and foo".
| \s+([\)])
# Strip only trailing whitespace from:
# - Opening parenthesis: Retain "@media (bar) and foo".
# - Colon: Retain :pseudo-selectors.
| ([\(:])\s+
>xS',
// Only one of the three capturing groups will match, so its reference
// will contain the wanted value and the references for the
// two non-matching groups will be replaced with empty strings.
'$1$2$3',
$contents
);
// End the file with a new line.
$contents = trim($contents);
$contents .= "\n";
}
// Replaces @import commands with the actual stylesheet content.
// This happens recursively but omits external files.
$contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)(?!\/\/)([^\'"\()]+)[\'"]?\s*\)?\s*;/', array($this, 'loadNestedFile'), $contents);
return $contents;
}
/**
* Prefixes all paths within a CSS file for processFile().
*
* Note: the only reason this method is public is so color.module can call it;
* it is not on the AssetOptimizerInterface, so future refactorings can make
* it protected.
*
* @param array $matches
* An array of matches by a preg_replace_callback() call that scans for
* url() references in CSS files, except for external or absolute ones.
*
* @return string
* The file path.
*/
public function rewriteFileURI($matches) {
// Prefix with base and remove '../' segments where possible.
$path = $this->rewriteFileURIBasePath . $matches[1];
$last = '';
while ($path != $last) {
$last = $path;
$path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
}
return 'url(' . file_url_transform_relative(file_create_url($path)) . ')';
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception if a library has no CSS/JS/JS setting specified.
*/
class IncompleteLibraryDefinitionException extends \RuntimeException {
}

View file

@ -0,0 +1,10 @@
<?php
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception for an invalid libraries-extend specification.
*/
class InvalidLibrariesExtendSpecificationException extends \RuntimeException {
}

View file

@ -0,0 +1,10 @@
<?php
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception if a definition refers to a non-existent library.
*/
class InvalidLibrariesOverrideSpecificationException extends \RuntimeException {
}

View file

@ -0,0 +1,10 @@
<?php
namespace Drupal\Core\Asset\Exception;
/**
* Defines an exception if the library file could not be parsed.
*/
class InvalidLibraryFileException extends \RunTimeException {
}

View file

@ -0,0 +1,10 @@
<?php
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception if a library has a remote but no license.
*/
class LibraryDefinitionMissingLicenseException extends \RuntimeException {
}

View file

@ -0,0 +1,70 @@
<?php
namespace Drupal\Core\Asset;
/**
* Groups JavaScript assets.
*/
class JsCollectionGrouper implements AssetCollectionGrouperInterface {
/**
* {@inheritdoc}
*
* Puts multiple items into the same group if they are groupable and if they
* are for the same browsers. Items of the 'file' type are groupable if their
* 'preprocess' flag is TRUE. Items of the 'external' type are not groupable.
*
* Also ensures that the process of grouping items does not change their
* relative order. This requirement may result in multiple groups for the same
* type and browsers, if needed to accommodate other items in between.
*/
public function group(array $js_assets) {
$groups = array();
// If a group can contain multiple items, we track the information that must
// be the same for each item in the group, so that when we iterate the next
// item, we can determine if it can be put into the current group, or if a
// new group needs to be made for it.
$current_group_keys = NULL;
$index = -1;
foreach ($js_assets as $item) {
// The browsers for which the JavaScript item needs to be loaded is part
// of the information that determines when a new group is needed, but the
// order of keys in the array doesn't matter, and we don't want a new
// group if all that's different is that order.
ksort($item['browsers']);
switch ($item['type']) {
case 'file':
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['browsers']) : FALSE;
break;
case 'external':
// Do not group external items.
$group_keys = FALSE;
break;
}
// If the group keys don't match the most recent group we're working with,
// then a new group must be made.
if ($group_keys !== $current_group_keys) {
$index++;
// Initialize the new group with the same properties as the first item
// being placed into it. The item's 'data' and 'weight' properties are
// unique to the item and should not be carried over to the group.
$groups[$index] = $item;
unset($groups[$index]['data'], $groups[$index]['weight']);
$groups[$index]['items'] = array();
$current_group_keys = $group_keys ? $group_keys : NULL;
}
// Add the item to the current group.
$groups[$index]['items'][] = $item;
}
return $groups;
}
}

View file

@ -0,0 +1,190 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Core\State\StateInterface;
/**
* Optimizes JavaScript assets.
*/
class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
/**
* A JS asset grouper.
*
* @var \Drupal\Core\Asset\JsCollectionGrouper
*/
protected $grouper;
/**
* A JS asset optimizer.
*
* @var \Drupal\Core\Asset\JsOptimizer
*/
protected $optimizer;
/**
* An asset dumper.
*
* @var \Drupal\Core\Asset\AssetDumper
*/
protected $dumper;
/**
* The state key/value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a JsCollectionOptimizer.
*
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
* The grouper for JS assets.
* @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
* The optimizer for a single JS asset.
* @param \Drupal\Core\Asset\AssetDumperInterface $dumper
* The dumper for optimized JS assets.
* @param \Drupal\Core\State\StateInterface $state
* The state key/value store.
*/
public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) {
$this->grouper = $grouper;
$this->optimizer = $optimizer;
$this->dumper = $dumper;
$this->state = $state;
}
/**
* {@inheritdoc}
*
* The cache file name is retrieved on a page load via a lookup variable that
* contains an associative array. The array key is the hash of the names in
* $files while the value is the cache file name. The cache file is generated
* in two cases. First, if there is no file name value for the key, which will
* happen if a new file name has been added to $files or after the lookup
* variable is emptied to force a rebuild of the cache. Second, the cache file
* is generated if it is missing on disk. Old cache files are not deleted
* immediately when the lookup variable is emptied, but are deleted after a
* configurable period (@code system.performance.stale_file_threshold @endcode)
* to ensure that files referenced by a cached page will still be available.
*/
public function optimize(array $js_assets) {
// Group the assets.
$js_groups = $this->grouper->group($js_assets);
// Now optimize (concatenate, not minify) and dump each asset group, unless
// that was already done, in which case it should appear in
// system.js_cache_files.
// Drupal contrib can override this default JS aggregator to keep the same
// grouping, optimizing and dumping, but change the strategy that is used to
// determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …).
$map = $this->state->get('system.js_cache_files') ?: array();
$js_assets = array();
foreach ($js_groups as $order => $js_group) {
// We have to return a single asset, not a group of assets. It is now up
// to one of the pieces of code in the switch statement below to set the
// 'data' property to the appropriate value.
$js_assets[$order] = $js_group;
unset($js_assets[$order]['items']);
switch ($js_group['type']) {
case 'file':
// No preprocessing, single JS asset: just use the existing URI.
if (!$js_group['preprocess']) {
$uri = $js_group['items'][0]['data'];
$js_assets[$order]['data'] = $uri;
}
// Preprocess (aggregate), unless the aggregate file already exists.
else {
$key = $this->generateHash($js_group);
$uri = '';
if (isset($map[$key])) {
$uri = $map[$key];
}
if (empty($uri) || !file_exists($uri)) {
// Concatenate each asset within the group.
$data = '';
foreach ($js_group['items'] as $js_asset) {
// Optimize this JS file, but only if it's not yet minified.
if (isset($js_asset['minified']) && $js_asset['minified']) {
$data .= file_get_contents($js_asset['data']);
}
else {
$data .= $this->optimizer->optimize($js_asset);
}
// Append a ';' and a newline after each JS file to prevent them
// from running together.
$data .= ";\n";
}
// Remove unwanted JS code that cause issues.
$data = $this->optimizer->clean($data);
// Dump the optimized JS for this group into an aggregate file.
$uri = $this->dumper->dump($data, 'js');
// Set the URI for this group's aggregate file.
$js_assets[$order]['data'] = $uri;
// Persist the URI for this aggregate file.
$map[$key] = $uri;
$this->state->set('system.js_cache_files', $map);
}
else {
// Use the persisted URI for the optimized JS file.
$js_assets[$order]['data'] = $uri;
}
$js_assets[$order]['preprocessed'] = TRUE;
}
break;
case 'external':
// We don't do any aggregation and hence also no caching for external
// JS assets.
$uri = $js_group['items'][0]['data'];
$js_assets[$order]['data'] = $uri;
break;
}
}
return $js_assets;
}
/**
* Generate a hash for a given group of JavaScript assets.
*
* @param array $js_group
* A group of JavaScript assets.
*
* @return string
* A hash to uniquely identify the given group of JavaScript assets.
*/
protected function generateHash(array $js_group) {
$js_data = array();
foreach ($js_group['items'] as $js_file) {
$js_data[] = $js_file['data'];
}
return hash('sha256', serialize($js_data));
}
/**
* {@inheritdoc}
*/
public function getAll() {
return $this->state->get('system.js_cache_files');
}
/**
* {@inheritdoc}
*/
public function deleteAll() {
$this->state->delete('system.js_cache_files');
$delete_stale = function($uri) {
// Default stale file threshold is 30 days.
if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
file_unmanaged_delete($uri);
}
};
file_scan_directory('public://js', '/.*/', array('callback' => $delete_stale));
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Serialization\Json;
use Drupal\Core\State\StateInterface;
/**
* Renders JavaScript assets.
*/
class JsCollectionRenderer implements AssetCollectionRendererInterface {
/**
* The state key/value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a JsCollectionRenderer.
*
* @param \Drupal\Core\State\StateInterface $state
* The state key/value store.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* {@inheritdoc}
*
* This class evaluates the aggregation enabled/disabled condition on a group
* by group basis by testing whether an aggregate file has been made for the
* group rather than by testing the site-wide aggregation setting. This allows
* this class to work correctly even if modules have implemented custom
* logic for grouping and aggregating files.
*/
public function render(array $js_assets) {
$elements = array();
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
// flush, forcing browsers to load a new copy of the files, as the
// URL changed. Files that should not be cached get REQUEST_TIME as
// query-string instead, to enforce reload on every page request.
$default_query_string = $this->state->get('system.css_js_query_string') ?: '0';
// Defaults for each SCRIPT element.
$element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => '',
);
// Loop through all JS assets.
foreach ($js_assets as $js_asset) {
// Element properties that do not depend on JS asset type.
$element = $element_defaults;
$element['#browsers'] = $js_asset['browsers'];
// Element properties that depend on item type.
switch ($js_asset['type']) {
case 'setting':
$element['#attributes'] = array(
// This type attribute prevents this from being parsed as an
// inline script.
'type' => 'application/json',
'data-drupal-selector' => 'drupal-settings-json',
);
$element['#value'] = Json::encode($js_asset['data']);
break;
case 'file':
$query_string = $js_asset['version'] == -1 ? $default_query_string : 'v=' . $js_asset['version'];
$query_string_separator = (strpos($js_asset['data'], '?') !== FALSE) ? '&' : '?';
$element['#attributes']['src'] = file_url_transform_relative(file_create_url($js_asset['data']));
// Only add the cache-busting query string if this isn't an aggregate
// file.
if (!isset($js_asset['preprocessed'])) {
$element['#attributes']['src'] .= $query_string_separator . ($js_asset['cache'] ? $query_string : REQUEST_TIME);
}
break;
case 'external':
$element['#attributes']['src'] = $js_asset['data'];
break;
default:
throw new \Exception('Invalid JS asset type.');
}
// Attributes may only be set if this script is output independently.
if (!empty($element['#attributes']['src']) && !empty($js_asset['attributes'])) {
$element['#attributes'] += $js_asset['attributes'];
}
$elements[] = $element;
}
return $elements;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Unicode;
/**
* Optimizes a JavaScript asset.
*/
class JsOptimizer implements AssetOptimizerInterface {
/**
* {@inheritdoc}
*/
public function optimize(array $js_asset) {
if ($js_asset['type'] !== 'file') {
throw new \Exception('Only file JavaScript assets can be optimized.');
}
if (!$js_asset['preprocess']) {
throw new \Exception('Only file JavaScript assets with preprocessing enabled can be optimized.');
}
// If a BOM is found, convert the file to UTF-8, then use substr() to
// remove the BOM from the result.
$data = file_get_contents($js_asset['data']);
if ($encoding = (Unicode::encodingFromBOM($data))) {
$data = Unicode::substr(Unicode::convertToUtf8($data, $encoding), 1);
}
// If no BOM is found, check for the charset attribute.
elseif (isset($js_asset['attributes']['charset'])) {
$data = Unicode::convertToUtf8($data, $js_asset['attributes']['charset']);
}
// No-op optimizer: no optimizations are applied to JavaScript assets.
return $data;
}
/**
* Processes the contents of a javascript asset for cleanup.
*
* @param string $contents
* The contents of the javascript asset.
*
* @return string
* Contents of the javascript asset.
*/
public function clean($contents) {
// Remove JS source and source mapping urls or these may cause 404 errors.
$contents = preg_replace('/\/\/(#|@)\s(sourceURL|sourceMappingURL)=\s*(\S*?)\s*$/m', '', $contents);
return $contents;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Drupal\Core\Asset;
/**
* Resolves the dependencies of asset (CSS/JavaScript) libraries.
*/
class LibraryDependencyResolver implements LibraryDependencyResolverInterface {
/**
* The library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
*/
protected $libraryDiscovery;
/**
* Constructs a new LibraryDependencyResolver instance.
*
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
* The library discovery service.
*/
public function __construct(LibraryDiscoveryInterface $library_discovery) {
$this->libraryDiscovery = $library_discovery;
}
/**
* {@inheritdoc}
*/
public function getLibrariesWithDependencies(array $libraries) {
return $this->doGetDependencies($libraries);
}
/**
* Gets the given libraries with its dependencies.
*
* Helper method for ::getLibrariesWithDependencies().
*
* @param string[] $libraries_with_unresolved_dependencies
* A list of libraries, with unresolved dependencies, in the order they
* should be loaded.
* @param string[] $final_libraries
* The final list of libraries (the return value) that is being built
* recursively.
*
* @return string[]
* A list of libraries, in the order they should be loaded, including their
* dependencies.
*/
protected function doGetDependencies(array $libraries_with_unresolved_dependencies, array $final_libraries = []) {
foreach ($libraries_with_unresolved_dependencies as $library) {
if (!in_array($library, $final_libraries)) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (!empty($definition['dependencies'])) {
$final_libraries = $this->doGetDependencies($definition['dependencies'], $final_libraries);
}
$final_libraries[] = $library;
}
}
return $final_libraries;
}
/**
* {@inheritdoc}
*/
public function getMinimalRepresentativeSubset(array $libraries) {
$minimal = [];
// Determine each library's dependencies.
$with_deps = [];
foreach ($libraries as $library) {
$with_deps[$library] = $this->getLibrariesWithDependencies([$library]);
}
foreach ($libraries as $library) {
$exists = FALSE;
foreach ($with_deps as $other_library => $dependencies) {
if ($library == $other_library) {
continue;
}
if (in_array($library, $dependencies)) {
$exists = TRUE;
break;
}
}
if (!$exists) {
$minimal[] = $library;
}
}
return $minimal;
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\Core\Asset;
/**
* Resolves the dependencies of asset (CSS/JavaScript) libraries.
*/
interface LibraryDependencyResolverInterface {
/**
* Gets the given libraries with their dependencies.
*
* Given ['core/a', 'core/b', 'core/c'], with core/a depending on core/c and
* core/b on core/d, returns ['core/a', 'core/b', 'core/c', 'core/d'].
*
* @param string[] $libraries
* A list of libraries, in the order they should be loaded.
*
* @return string[]
* A list of libraries, in the order they should be loaded, including their
* dependencies.
*/
public function getLibrariesWithDependencies(array $libraries);
/**
* Gets the minimal representative subset of the given libraries.
*
* A minimal representative subset means that any library in the given set of
* libraries that is a dependency of another library in the set, is removed.
*
* Hence a minimal representative subset is the most compact representation
* possible of a set of libraries.
*
* (Each asset library has dependencies and can therefore be seen as a tree.
* Hence the given list of libraries represent a forest. This function returns
* all roots of trees that are not a subtree of another tree in the forest.)
*
* @param string[] $libraries
* A set of libraries.
*
* @return string[]
* A representative subset of the given set of libraries.
*/
public function getMinimalRepresentativeSubset(array $libraries);
}

View file

@ -0,0 +1,70 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Core\Cache\CacheCollectorInterface;
/**
* Discovers available asset libraries in Drupal.
*/
class LibraryDiscovery implements LibraryDiscoveryInterface {
/**
* The library discovery cache collector.
*
* @var \Drupal\Core\Cache\CacheCollectorInterface
*/
protected $collector;
/**
* The final library definitions, statically cached.
*
* hook_library_info_alter() and hook_js_settings_alter() allows modules
* and themes to dynamically alter a library definition (once per request).
*
* @var array
*/
protected $libraryDefinitions = [];
/**
* Constructs a new LibraryDiscovery instance.
*
* @param \Drupal\Core\Cache\CacheCollectorInterface $library_discovery_collector
* The library discovery cache collector.
*/
public function __construct(CacheCollectorInterface $library_discovery_collector) {
$this->collector = $library_discovery_collector;
}
/**
* {@inheritdoc}
*/
public function getLibrariesByExtension($extension) {
if (!isset($this->libraryDefinitions[$extension])) {
$libraries = $this->collector->get($extension);
$this->libraryDefinitions[$extension] = [];
foreach ($libraries as $name => $definition) {
$this->libraryDefinitions[$extension][$name] = $definition;
}
}
return $this->libraryDefinitions[$extension];
}
/**
* {@inheritdoc}
*/
public function getLibraryByName($extension, $name) {
$extension = $this->getLibrariesByExtension($extension);
return isset($extension[$name]) ? $extension[$name] : FALSE;
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
$this->libraryDefinitions = [];
$this->collector->clear();
}
}

View file

@ -0,0 +1,164 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException;
use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* A CacheCollector implementation for building library extension info.
*/
class LibraryDiscoveryCollector extends CacheCollector {
/**
* The library discovery parser.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryParser
*/
protected $discoveryParser;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* Constructs a CacheCollector object.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Asset\LibraryDiscoveryParser $discovery_parser
* The library discovery parser.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
*/
public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, LibraryDiscoveryParser $discovery_parser, ThemeManagerInterface $theme_manager) {
$this->themeManager = $theme_manager;
parent::__construct(NULL, $cache, $lock, ['library_info']);
$this->discoveryParser = $discovery_parser;
}
/**
* {@inheritdoc}
*/
protected function getCid() {
if (!isset($this->cid)) {
$this->cid = 'library_info:' . $this->themeManager->getActiveTheme()->getName();
}
return $this->cid;
}
/**
* {@inheritdoc}
*/
protected function resolveCacheMiss($key) {
$this->storage[$key] = $this->getLibraryDefinitions($key);
$this->persist($key);
return $this->storage[$key];
}
/**
* Returns the library definitions for a given extension.
*
* This also implements libraries-overrides for entire libraries that have
* been specified by the LibraryDiscoveryParser.
*
* @param string $extension
* The name of the extension for which library definitions will be returned.
*
* @return array
* The library definitions for $extension with overrides applied.
*
* @throws \Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException
*/
protected function getLibraryDefinitions($extension) {
$libraries = $this->discoveryParser->buildByExtension($extension);
foreach ($libraries as $name => $definition) {
// Handle libraries that are marked for override or removal.
// @see \Drupal\Core\Asset\LibraryDiscoveryParser::applyLibrariesOverride()
if (isset($definition['override'])) {
if ($definition['override'] === FALSE) {
// Remove the library definition if FALSE is given.
unset($libraries[$name]);
}
else {
// Otherwise replace with existing library definition if it exists.
// Throw an exception if it doesn't.
list($replacement_extension, $replacement_name) = explode('/', $definition['override']);
$replacement_definition = $this->get($replacement_extension);
if (isset($replacement_definition[$replacement_name])) {
$libraries[$name] = $replacement_definition[$replacement_name];
}
else {
throw new InvalidLibrariesOverrideSpecificationException(sprintf('The specified library %s does not exist.', $definition['override']));
}
}
}
else {
// If libraries are not overridden, then apply libraries-extend.
$libraries[$name] = $this->applyLibrariesExtend($extension, $name, $definition);
}
}
return $libraries;
}
/**
* Applies the libraries-extend specified by the active theme.
*
* This extends the library definitions with the those specified by the
* libraries-extend specifications for the active theme.
*
* @param string $extension
* The name of the extension for which library definitions will be extended.
* @param string $library_name
* The name of the library whose definitions is to be extended.
* @param $library_definition
* The library definition to be extended.
*
* @return array
* The library definition extended as specified by libraries-extend.
*
* @throws \Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException
*/
protected function applyLibrariesExtend($extension, $library_name, $library_definition) {
$libraries_extend = $this->themeManager->getActiveTheme()->getLibrariesExtend();
if (!empty($libraries_extend["$extension/$library_name"])) {
foreach ($libraries_extend["$extension/$library_name"] as $library_extend_name) {
if (!is_string($library_extend_name)) {
// Only string library names are allowed.
throw new InvalidLibrariesExtendSpecificationException('The libraries-extend specification for each library must be a list of strings.');
}
list($new_extension, $new_library_name) = explode('/', $library_extend_name, 2);
$new_libraries = $this->get($new_extension);
if (isset($new_libraries[$new_library_name])) {
$library_definition = NestedArray::mergeDeep($library_definition, $new_libraries[$new_library_name]);
}
else {
throw new InvalidLibrariesExtendSpecificationException(sprintf('The specified library "%s" does not exist.', $library_extend_name));
}
}
}
return $library_definition;
}
/**
* {@inheritdoc}
*/
public function reset() {
parent::reset();
$this->cid = NULL;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\Core\Asset;
/**
* Discovers information for asset (CSS/JavaScript) libraries.
*
* Library information is statically cached. Libraries are keyed by extension
* for several reasons:
* - Libraries are not unique. Multiple extensions might ship with the same
* library in a different version or variant. This registry cannot (and does
* not attempt to) prevent library conflicts.
* - Extensions implementing and thereby depending on a library that is
* registered by another extension can only rely on that extension's library.
* - Two (or more) extensions can still register the same library and use it
* without conflicts in case the libraries are loaded on certain pages only.
*/
interface LibraryDiscoveryInterface {
/**
* Gets all libraries defined by an extension.
*
* @param string $extension
* The name of the extension that registered a library.
*
* @return array
* An associative array of libraries registered by $extension is returned
* (which may be empty).
*
* @see self::getLibraryByName()
*/
public function getLibrariesByExtension($extension);
/**
* Gets a single library defined by an extension by name.
*
* @param string $extension
* The name of the extension that registered a library.
* @param string $name
* The name of a registered library to retrieve.
*
* @return array|false
* The definition of the requested library, if $name was passed and it
* exists, otherwise FALSE.
*/
public function getLibraryByName($extension, $name);
/**
* Clears static and persistent library definition caches.
*/
public function clearCachedDefinitions();
}

View file

@ -0,0 +1,463 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Utility\NestedArray;
/**
* Parses library files to get extension data.
*/
class LibraryDiscoveryParser {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new LibraryDiscoveryParser instance.
*
* @param string $root
* The app root.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
*/
public function __construct($root, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
$this->root = $root;
$this->moduleHandler = $module_handler;
$this->themeManager = $theme_manager;
}
/**
* Parses and builds up all the libraries information of an extension.
*
* @param string $extension
* The name of the extension that registered a library.
*
* @return array
* All library definitions of the passed extension.
*
* @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException
* Thrown when a library has no js/css/setting.
* @throws \UnexpectedValueException
* Thrown when a js file defines a positive weight.
*/
public function buildByExtension($extension) {
$libraries = array();
if ($extension === 'core') {
$path = 'core';
$extension_type = 'core';
}
else {
if ($this->moduleHandler->moduleExists($extension)) {
$extension_type = 'module';
}
else {
$extension_type = 'theme';
}
$path = $this->drupalGetPath($extension_type, $extension);
}
$libraries = $this->parseLibraryInfo($extension, $path);
$libraries = $this->applyLibrariesOverride($libraries, $extension);
foreach ($libraries as $id => &$library) {
if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) {
throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for definition '%s' in extension '%s'", $id, $extension));
}
$library += array('dependencies' => array(), 'js' => array(), 'css' => array());
if (isset($library['header']) && !is_bool($library['header'])) {
throw new \LogicException(sprintf("The 'header' key in the library definition '%s' in extension '%s' is invalid: it must be a boolean.", $id, $extension));
}
if (isset($library['version'])) {
// @todo Retrieve version of a non-core extension.
if ($library['version'] === 'VERSION') {
$library['version'] = \Drupal::VERSION;
}
// Remove 'v' prefix from external library versions.
elseif ($library['version'][0] === 'v') {
$library['version'] = substr($library['version'], 1);
}
}
// If this is a 3rd party library, the license info is required.
if (isset($library['remote']) && !isset($library['license'])) {
throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for definition '%s' extension '%s': it has a remote, but no license.", $id, $extension));
}
// Assign Drupal's license to libraries that don't have license info.
if (!isset($library['license'])) {
$library['license'] = array(
'name' => 'GNU-GPL-2.0-or-later',
'url' => 'https://www.drupal.org/licensing/faq',
'gpl-compatible' => TRUE,
);
}
foreach (array('js', 'css') as $type) {
// Prepare (flatten) the SMACSS-categorized definitions.
// @todo After Asset(ic) changes, retain the definitions as-is and
// properly resolve dependencies for all (css) libraries per category,
// and only once prior to rendering out an HTML page.
if ($type == 'css' && !empty($library[$type])) {
foreach ($library[$type] as $category => $files) {
foreach ($files as $source => $options) {
if (!isset($options['weight'])) {
$options['weight'] = 0;
}
// Apply the corresponding weight defined by CSS_* constants.
$options['weight'] += constant('CSS_' . strtoupper($category));
$library[$type][$source] = $options;
}
unset($library[$type][$category]);
}
}
foreach ($library[$type] as $source => $options) {
unset($library[$type][$source]);
// Allow to omit the options hashmap in YAML declarations.
if (!is_array($options)) {
$options = array();
}
if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) {
throw new \UnexpectedValueException("The $extension/$id library defines a positive weight for '$source'. Only negative weights are allowed (but should be avoided). Instead of a positive weight, specify accurate dependencies for this library.");
}
// Unconditionally apply default groups for the defined asset files.
// The library system is a dependency management system. Each library
// properly specifies its dependencies instead of relying on a custom
// processing order.
if ($type == 'js') {
$options['group'] = JS_LIBRARY;
}
elseif ($type == 'css') {
$options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT;
}
// By default, all library assets are files.
if (!isset($options['type'])) {
$options['type'] = 'file';
}
if ($options['type'] == 'external') {
$options['data'] = $source;
}
// Determine the file asset URI.
else {
if ($source[0] === '/') {
// An absolute path maps to DRUPAL_ROOT / base_path().
if ($source[1] !== '/') {
$options['data'] = substr($source, 1);
}
// A protocol-free URI (e.g., //cdn.com/example.js) is external.
else {
$options['type'] = 'external';
$options['data'] = $source;
}
}
// A stream wrapper URI (e.g., public://generated_js/example.js).
elseif ($this->fileValidUri($source)) {
$options['data'] = $source;
}
// A regular URI (e.g., http://example.com/example.js) without
// 'external' explicitly specified, which may happen if, e.g.
// libraries-override is used.
elseif ($this->isValidUri($source)) {
$options['type'] = 'external';
$options['data'] = $source;
}
// By default, file paths are relative to the registering extension.
else {
$options['data'] = $path . '/' . $source;
}
}
if (!isset($library['version'])) {
// @todo Get the information from the extension.
$options['version'] = -1;
}
else {
$options['version'] = $library['version'];
}
// Set the 'minified' flag on JS file assets, default to FALSE.
if ($type == 'js' && $options['type'] == 'file') {
$options['minified'] = isset($options['minified']) ? $options['minified'] : FALSE;
}
$library[$type][] = $options;
}
}
}
return $libraries;
}
/**
* Parses a given library file and allows modules and themes to alter it.
*
* This method sets the parsed information onto the library property.
*
* Library information is parsed from *.libraries.yml files; see
* editor.library.yml for an example. Every library must have at least one js
* or css entry. Each entry starts with a machine name and defines the
* following elements:
* - js: A list of JavaScript files to include. Each file is keyed by the file
* path. An item can have several attributes (like HTML
* attributes). For example:
* @code
* js:
* path/js/file.js: { attributes: { defer: true } }
* @endcode
* If the file has no special attributes, just use an empty object:
* @code
* js:
* path/js/file.js: {}
* @endcode
* The path of the file is relative to the module or theme directory, unless
* it starts with a /, in which case it is relative to the Drupal root. If
* the file path starts with //, it will be treated as a protocol-free,
* external resource (e.g., //cdn.com/library.js). Full URLs
* (e.g., http://cdn.com/library.js) as well as URLs that use a valid
* stream wrapper (e.g., public://path/to/file.js) are also supported.
* - css: A list of categories for which the library provides CSS files. The
* available categories are:
* - base
* - layout
* - component
* - state
* - theme
* Each category is itself a key for a sub-list of CSS files to include:
* @code
* css:
* component:
* css/file.css: {}
* @endcode
* Just like with JavaScript files, each CSS file is the key of an object
* that can define specific attributes. The format of the file path is the
* same as for the JavaScript files.
* - dependencies: A list of libraries this library depends on.
* - version: The library version. The string "VERSION" can be used to mean
* the current Drupal core version.
* - header: By default, JavaScript files are included in the footer. If the
* script must be included in the header (along with all its dependencies),
* set this to true. Defaults to false.
* - minified: If the file is already minified, set this to true to avoid
* minifying it again. Defaults to false.
* - remote: If the library is a third-party script, this provides the
* repository URL for reference.
* - license: If the remote property is set, the license information is
* required. It has 3 properties:
* - name: The human-readable name of the license.
* - url: The URL of the license file/information for the version of the
* library used.
* - gpl-compatible: A Boolean for whether this library is GPL compatible.
*
* See https://www.drupal.org/node/2274843#define-library for more
* information.
*
* @param string $extension
* The name of the extension that registered a library.
* @param string $path
* The relative path to the extension.
*
* @return array
* An array of parsed library data.
*
* @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException
* Thrown when a parser exception got thrown.
*/
protected function parseLibraryInfo($extension, $path) {
$libraries = [];
$library_file = $path . '/' . $extension . '.libraries.yml';
if (file_exists($this->root . '/' . $library_file)) {
try {
$libraries = Yaml::decode(file_get_contents($this->root . '/' . $library_file));
}
catch (InvalidDataTypeException $e) {
// Rethrow a more helpful exception to provide context.
throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
}
}
// Allow modules to add dynamic library definitions.
$hook = 'library_info_build';
if ($this->moduleHandler->implementsHook($extension, $hook)) {
$libraries = NestedArray::mergeDeep($libraries, $this->moduleHandler->invoke($extension, $hook));
}
// Allow modules to alter the module's registered libraries.
$this->moduleHandler->alter('library_info', $libraries, $extension);
$this->themeManager->alter('library_info', $libraries, $extension);
return $libraries;
}
/**
* Apply libraries overrides specified for the current active theme.
*
* @param array $libraries
* The libraries definitions.
* @param string $extension
* The extension in which these libraries are defined.
*
* @return array
* The modified libraries definitions.
*/
protected function applyLibrariesOverride($libraries, $extension) {
$active_theme = $this->themeManager->getActiveTheme();
// ActiveTheme::getLibrariesOverride() returns libraries-overrides for the
// current theme as well as all its base themes.
$all_libraries_overrides = $active_theme->getLibrariesOverride();
foreach ($all_libraries_overrides as $theme_path => $libraries_overrides) {
foreach ($libraries as $library_name => $library) {
// Process libraries overrides.
if (isset($libraries_overrides["$extension/$library_name"])) {
// Active theme defines an override for this library.
$override_definition = $libraries_overrides["$extension/$library_name"];
if (is_string($override_definition) || $override_definition === FALSE) {
// A string or boolean definition implies an override (or removal)
// for the whole library. Use the override key to specify that this
// library will be overridden when it is called.
// @see \Drupal\Core\Asset\LibraryDiscovery::getLibraryByName()
if ($override_definition) {
$libraries[$library_name]['override'] = $override_definition;
}
else {
$libraries[$library_name]['override'] = FALSE;
}
}
elseif (is_array($override_definition)) {
// An array definition implies an override for an asset within this
// library.
foreach ($override_definition as $sub_key => $value) {
// Throw an exception if the asset is not properly specified.
if (!is_array($value)) {
throw new InvalidLibrariesOverrideSpecificationException(sprintf('Library asset %s is not correctly specified. It should be in the form "extension/library_name/sub_key/path/to/asset.js".', "$extension/$library_name/$sub_key"));
}
if ($sub_key === 'drupalSettings') {
// drupalSettings may not be overridden.
throw new InvalidLibrariesOverrideSpecificationException(sprintf('drupalSettings may not be overridden in libraries-override. Trying to override %s. Use hook_library_info_alter() instead.', "$extension/$library_name/$sub_key"));
}
elseif ($sub_key === 'css') {
// SMACSS category should be incorporated into the asset name.
foreach ($value as $category => $overrides) {
$this->setOverrideValue($libraries[$library_name], [$sub_key, $category], $overrides, $theme_path);
}
}
else {
$this->setOverrideValue($libraries[$library_name], [$sub_key], $value, $theme_path);
}
}
}
}
}
}
return $libraries;
}
/**
* Wraps drupal_get_path().
*/
protected function drupalGetPath($type, $name) {
return drupal_get_path($type, $name);
}
/**
* Wraps file_valid_uri().
*/
protected function fileValidUri($source) {
return file_valid_uri($source);
}
/**
* Determines if the supplied string is a valid URI.
*/
protected function isValidUri($string) {
return count(explode('://', $string)) === 2;
}
/**
* Overrides the specified library asset.
*
* @param array $library
* The containing library definition.
* @param array $sub_key
* An array containing the sub-keys specifying the library asset, e.g.
* @code['js']@endcode or @code['css', 'component']@endcode
* @param array $overrides
* Specifies the overrides, this is an array where the key is the asset to
* be overridden while the value is overriding asset.
*/
protected function setOverrideValue(array &$library, array $sub_key, array $overrides, $theme_path) {
foreach ($overrides as $original => $replacement) {
// Get the attributes of the asset to be overridden. If the key does
// not exist, then throw an exception.
$key_exists = NULL;
$parents = array_merge($sub_key, [$original]);
// Save the attributes of the library asset to be overridden.
$attributes = NestedArray::getValue($library, $parents, $key_exists);
if ($key_exists) {
// Remove asset to be overridden.
NestedArray::unsetValue($library, $parents);
// No need to replace if FALSE is specified, since that is a removal.
if ($replacement) {
// Ensure the replacement path is relative to drupal root.
$replacement = $this->resolveThemeAssetPath($theme_path, $replacement);
$new_parents = array_merge($sub_key, [$replacement]);
// Replace with an override if specified.
NestedArray::setValue($library, $new_parents, $attributes);
}
}
}
}
/**
* Ensures that a full path is returned for an overriding theme asset.
*
* @param string $theme_path
* The theme or base theme.
* @param string $overriding_asset
* The overriding library asset.
*
* @return string
* A fully resolved theme asset path relative to the Drupal directory.
*/
protected function resolveThemeAssetPath($theme_path, $overriding_asset) {
if ($overriding_asset[0] !== '/' && !$this->isValidUri($overriding_asset)) {
// The destination is not an absolute path and it's not a URI (e.g.
// public://generated_js/example.js or http://example.com/js/my_js.js), so
// it's relative to the theme.
return '/' . $theme_path . '/' . $overriding_asset;
}
return $overriding_asset;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Drupal\Core\Authentication;
/**
* A collector class for authentication providers.
*/
class AuthenticationCollector implements AuthenticationCollectorInterface {
/**
* Array of all registered authentication providers, keyed by ID.
*
* @var \Drupal\Core\Authentication\AuthenticationProviderInterface[]
*/
protected $providers;
/**
* Array of all providers and their priority.
*
* @var array
*/
protected $providerOrders = [];
/**
* Sorted list of registered providers.
*
* @var \Drupal\Core\Authentication\AuthenticationProviderInterface[]
*/
protected $sortedProviders;
/**
* List of providers which are allowed on routes with no _auth option.
*
* @var string[]
*/
protected $globalProviders;
/**
* {@inheritdoc}
*/
public function addProvider(AuthenticationProviderInterface $provider, $provider_id, $priority = 0, $global = FALSE) {
$this->providers[$provider_id] = $provider;
$this->providerOrders[$priority][$provider_id] = $provider;
// Force the providers to be re-sorted.
$this->sortedProviders = NULL;
if ($global) {
$this->globalProviders[$provider_id] = TRUE;
}
}
/**
* {@inheritdoc}
*/
public function isGlobal($provider_id) {
return isset($this->globalProviders[$provider_id]);
}
/**
* {@inheritdoc}
*/
public function getProvider($provider_id) {
return isset($this->providers[$provider_id]) ? $this->providers[$provider_id] : NULL;
}
/**
* {@inheritdoc}
*/
public function getSortedProviders() {
if (!isset($this->sortedProviders)) {
// Sort the providers according to priority.
krsort($this->providerOrders);
// Merge nested providers from $this->providers into $this->sortedProviders.
$this->sortedProviders = [];
foreach ($this->providerOrders as $providers) {
$this->sortedProviders = array_merge($this->sortedProviders, $providers);
}
}
return $this->sortedProviders;
}
}

Some files were not shown because too many files have changed in this diff Show more