Move into nested docroot
This commit is contained in:
parent
83a0d3a149
commit
c8b70abde9
13405 changed files with 0 additions and 0 deletions
web/core/lib/Drupal/Core
Access
AccessArgumentsResolverFactory.phpAccessArgumentsResolverFactoryInterface.phpAccessCheckInterface.phpAccessException.phpAccessManager.phpAccessManagerInterface.phpAccessResult.phpAccessResultAllowed.phpAccessResultForbidden.phpAccessResultInterface.phpAccessResultNeutral.phpAccessResultReasonInterface.phpAccessibleInterface.phpCheckProvider.phpCheckProviderInterface.phpCsrfAccessCheck.phpCsrfRequestHeaderAccessCheck.phpCsrfTokenGenerator.phpCustomAccessCheck.phpDefaultAccessCheck.phpRouteProcessorCsrf.php
Action
ActionBase.phpActionInterface.phpActionManager.phpActionPluginCollection.phpConfigurableActionBase.php
Ajax
AddCssCommand.phpAfterCommand.phpAjaxResponse.phpAjaxResponseAttachmentsProcessor.phpAlertCommand.phpAppendCommand.phpBaseCommand.phpBeforeCommand.phpChangedCommand.phpCloseDialogCommand.phpCloseModalDialogCommand.phpCommandInterface.phpCommandWithAttachedAssetsInterface.phpCommandWithAttachedAssetsTrait.phpCssCommand.phpDataCommand.phpHtmlCommand.phpInsertCommand.phpInvokeCommand.phpOpenDialogCommand.phpOpenModalDialogCommand.phpPrependCommand.phpRedirectCommand.phpRemoveCommand.phpReplaceCommand.phpRestripeCommand.phpSetDialogOptionCommand.phpSetDialogTitleCommand.phpSettingsCommand.phpUpdateBuildIdCommand.php
Annotation
AppRootFactory.phpArchiver
Asset
AssetCollectionGrouperInterface.phpAssetCollectionOptimizerInterface.phpAssetCollectionRendererInterface.phpAssetDumper.phpAssetDumperInterface.phpAssetOptimizerInterface.phpAssetResolver.phpAssetResolverInterface.phpAttachedAssets.phpAttachedAssetsInterface.phpCssCollectionGrouper.phpCssCollectionOptimizer.phpCssCollectionRenderer.phpCssOptimizer.php
Exception
IncompleteLibraryDefinitionException.phpInvalidLibrariesExtendSpecificationException.phpInvalidLibrariesOverrideSpecificationException.phpInvalidLibraryFileException.phpLibraryDefinitionMissingLicenseException.php
JsCollectionGrouper.phpJsCollectionOptimizer.phpJsCollectionRenderer.phpJsOptimizer.phpLibraryDependencyResolver.phpLibraryDependencyResolverInterface.phpLibraryDiscovery.phpLibraryDiscoveryCollector.phpLibraryDiscoveryInterface.phpLibraryDiscoveryParser.phpAuthentication
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
24
web/core/lib/Drupal/Core/Access/AccessCheckInterface.php
Normal file
24
web/core/lib/Drupal/Core/Access/AccessCheckInterface.php
Normal 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);
|
||||
|
||||
}
|
12
web/core/lib/Drupal/Core/Access/AccessException.php
Normal file
12
web/core/lib/Drupal/Core/Access/AccessException.php
Normal 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 {
|
||||
}
|
172
web/core/lib/Drupal/Core/Access/AccessManager.php
Normal file
172
web/core/lib/Drupal/Core/Access/AccessManager.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
83
web/core/lib/Drupal/Core/Access/AccessManagerInterface.php
Normal file
83
web/core/lib/Drupal/Core/Access/AccessManagerInterface.php
Normal 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);
|
||||
|
||||
}
|
404
web/core/lib/Drupal/Core/Access/AccessResult.php
Normal file
404
web/core/lib/Drupal/Core/Access/AccessResult.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
17
web/core/lib/Drupal/Core/Access/AccessResultAllowed.php
Normal file
17
web/core/lib/Drupal/Core/Access/AccessResultAllowed.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
50
web/core/lib/Drupal/Core/Access/AccessResultForbidden.php
Normal file
50
web/core/lib/Drupal/Core/Access/AccessResultForbidden.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
98
web/core/lib/Drupal/Core/Access/AccessResultInterface.php
Normal file
98
web/core/lib/Drupal/Core/Access/AccessResultInterface.php
Normal 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);
|
||||
|
||||
}
|
17
web/core/lib/Drupal/Core/Access/AccessResultNeutral.php
Normal file
17
web/core/lib/Drupal/Core/Access/AccessResultNeutral.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
34
web/core/lib/Drupal/Core/Access/AccessibleInterface.php
Normal file
34
web/core/lib/Drupal/Core/Access/AccessibleInterface.php
Normal 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);
|
||||
|
||||
}
|
168
web/core/lib/Drupal/Core/Access/CheckProvider.php
Normal file
168
web/core/lib/Drupal/Core/Access/CheckProvider.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
65
web/core/lib/Drupal/Core/Access/CheckProviderInterface.php
Normal file
65
web/core/lib/Drupal/Core/Access/CheckProviderInterface.php
Normal 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();
|
||||
|
||||
}
|
67
web/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
Normal file
67
web/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
115
web/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
Normal file
115
web/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
112
web/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php
Normal file
112
web/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
71
web/core/lib/Drupal/Core/Access/CustomAccessCheck.php
Normal file
71
web/core/lib/Drupal/Core/Access/CustomAccessCheck.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
34
web/core/lib/Drupal/Core/Access/DefaultAccessCheck.php
Normal file
34
web/core/lib/Drupal/Core/Access/DefaultAccessCheck.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
83
web/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
Normal file
83
web/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
Normal 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',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
26
web/core/lib/Drupal/Core/Action/ActionBase.php
Normal file
26
web/core/lib/Drupal/Core/Action/ActionBase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
61
web/core/lib/Drupal/Core/Action/ActionInterface.php
Normal file
61
web/core/lib/Drupal/Core/Action/ActionInterface.php
Normal 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);
|
||||
|
||||
}
|
55
web/core/lib/Drupal/Core/Action/ActionManager.php
Normal file
55
web/core/lib/Drupal/Core/Action/ActionManager.php
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
21
web/core/lib/Drupal/Core/Action/ActionPluginCollection.php
Normal file
21
web/core/lib/Drupal/Core/Action/ActionPluginCollection.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
57
web/core/lib/Drupal/Core/Action/ConfigurableActionBase.php
Normal file
57
web/core/lib/Drupal/Core/Action/ConfigurableActionBase.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
48
web/core/lib/Drupal/Core/Ajax/AddCssCommand.php
Normal file
48
web/core/lib/Drupal/Core/Ajax/AddCssCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
35
web/core/lib/Drupal/Core/Ajax/AfterCommand.php
Normal file
35
web/core/lib/Drupal/Core/Ajax/AfterCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
68
web/core/lib/Drupal/Core/Ajax/AjaxResponse.php
Normal file
68
web/core/lib/Drupal/Core/Ajax/AjaxResponse.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
40
web/core/lib/Drupal/Core/Ajax/AlertCommand.php
Normal file
40
web/core/lib/Drupal/Core/Ajax/AlertCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
35
web/core/lib/Drupal/Core/Ajax/AppendCommand.php
Normal file
35
web/core/lib/Drupal/Core/Ajax/AppendCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
47
web/core/lib/Drupal/Core/Ajax/BaseCommand.php
Normal file
47
web/core/lib/Drupal/Core/Ajax/BaseCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
35
web/core/lib/Drupal/Core/Ajax/BeforeCommand.php
Normal file
35
web/core/lib/Drupal/Core/Ajax/BeforeCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
60
web/core/lib/Drupal/Core/Ajax/ChangedCommand.php
Normal file
60
web/core/lib/Drupal/Core/Ajax/ChangedCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
50
web/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php
Normal file
50
web/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
23
web/core/lib/Drupal/Core/Ajax/CloseModalDialogCommand.php
Normal file
23
web/core/lib/Drupal/Core/Ajax/CloseModalDialogCommand.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
20
web/core/lib/Drupal/Core/Ajax/CommandInterface.php
Normal file
20
web/core/lib/Drupal/Core/Ajax/CommandInterface.php
Normal 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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
77
web/core/lib/Drupal/Core/Ajax/CssCommand.php
Normal file
77
web/core/lib/Drupal/Core/Ajax/CssCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
73
web/core/lib/Drupal/Core/Ajax/DataCommand.php
Normal file
73
web/core/lib/Drupal/Core/Ajax/DataCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
35
web/core/lib/Drupal/Core/Ajax/HtmlCommand.php
Normal file
35
web/core/lib/Drupal/Core/Ajax/HtmlCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
78
web/core/lib/Drupal/Core/Ajax/InsertCommand.php
Normal file
78
web/core/lib/Drupal/Core/Ajax/InsertCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
73
web/core/lib/Drupal/Core/Ajax/InvokeCommand.php
Normal file
73
web/core/lib/Drupal/Core/Ajax/InvokeCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
140
web/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
Normal file
140
web/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
37
web/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
Normal file
37
web/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
35
web/core/lib/Drupal/Core/Ajax/PrependCommand.php
Normal file
35
web/core/lib/Drupal/Core/Ajax/PrependCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
40
web/core/lib/Drupal/Core/Ajax/RedirectCommand.php
Normal file
40
web/core/lib/Drupal/Core/Ajax/RedirectCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
47
web/core/lib/Drupal/Core/Ajax/RemoveCommand.php
Normal file
47
web/core/lib/Drupal/Core/Ajax/RemoveCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
36
web/core/lib/Drupal/Core/Ajax/ReplaceCommand.php
Normal file
36
web/core/lib/Drupal/Core/Ajax/ReplaceCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
49
web/core/lib/Drupal/Core/Ajax/RestripeCommand.php
Normal file
49
web/core/lib/Drupal/Core/Ajax/RestripeCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
63
web/core/lib/Drupal/Core/Ajax/SetDialogOptionCommand.php
Normal file
63
web/core/lib/Drupal/Core/Ajax/SetDialogOptionCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
27
web/core/lib/Drupal/Core/Ajax/SetDialogTitleCommand.php
Normal file
27
web/core/lib/Drupal/Core/Ajax/SetDialogTitleCommand.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
64
web/core/lib/Drupal/Core/Ajax/SettingsCommand.php
Normal file
64
web/core/lib/Drupal/Core/Ajax/SettingsCommand.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
60
web/core/lib/Drupal/Core/Ajax/UpdateBuildIdCommand.php
Normal file
60
web/core/lib/Drupal/Core/Ajax/UpdateBuildIdCommand.php
Normal 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,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
66
web/core/lib/Drupal/Core/Annotation/Action.php
Normal file
66
web/core/lib/Drupal/Core/Annotation/Action.php
Normal 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;
|
||||
|
||||
}
|
134
web/core/lib/Drupal/Core/Annotation/ContextDefinition.php
Normal file
134
web/core/lib/Drupal/Core/Annotation/ContextDefinition.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
47
web/core/lib/Drupal/Core/Annotation/Mail.php
Normal file
47
web/core/lib/Drupal/Core/Annotation/Mail.php
Normal 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;
|
||||
|
||||
}
|
105
web/core/lib/Drupal/Core/Annotation/PluralTranslation.php
Normal file
105
web/core/lib/Drupal/Core/Annotation/PluralTranslation.php
Normal 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,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
63
web/core/lib/Drupal/Core/Annotation/QueueWorker.php
Normal file
63
web/core/lib/Drupal/Core/Annotation/QueueWorker.php
Normal 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;
|
||||
|
||||
}
|
94
web/core/lib/Drupal/Core/Annotation/Translation.php
Normal file
94
web/core/lib/Drupal/Core/Annotation/Translation.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
36
web/core/lib/Drupal/Core/AppRootFactory.php
Normal file
36
web/core/lib/Drupal/Core/AppRootFactory.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
55
web/core/lib/Drupal/Core/Archiver/Annotation/Archiver.php
Normal file
55
web/core/lib/Drupal/Core/Archiver/Annotation/Archiver.php
Normal 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;
|
||||
|
||||
}
|
2458
web/core/lib/Drupal/Core/Archiver/ArchiveTar.php
Normal file
2458
web/core/lib/Drupal/Core/Archiver/ArchiveTar.php
Normal file
File diff suppressed because it is too large
Load diff
8
web/core/lib/Drupal/Core/Archiver/ArchiverException.php
Normal file
8
web/core/lib/Drupal/Core/Archiver/ArchiverException.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Archiver;
|
||||
|
||||
/**
|
||||
* Defines an exception class for Drupal\Core\Archiver\ArchiverInterface.
|
||||
*/
|
||||
class ArchiverException extends \Exception {}
|
60
web/core/lib/Drupal/Core/Archiver/ArchiverInterface.php
Normal file
60
web/core/lib/Drupal/Core/Archiver/ArchiverInterface.php
Normal 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();
|
||||
|
||||
}
|
64
web/core/lib/Drupal/Core/Archiver/ArchiverManager.php
Normal file
64
web/core/lib/Drupal/Core/Archiver/ArchiverManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
91
web/core/lib/Drupal/Core/Archiver/Tar.php
Normal file
91
web/core/lib/Drupal/Core/Archiver/Tar.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
93
web/core/lib/Drupal/Core/Archiver/Zip.php
Normal file
93
web/core/lib/Drupal/Core/Archiver/Zip.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
47
web/core/lib/Drupal/Core/Asset/AssetDumper.php
Normal file
47
web/core/lib/Drupal/Core/Asset/AssetDumper.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
23
web/core/lib/Drupal/Core/Asset/AssetDumperInterface.php
Normal file
23
web/core/lib/Drupal/Core/Asset/AssetDumperInterface.php
Normal 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);
|
||||
|
||||
}
|
32
web/core/lib/Drupal/Core/Asset/AssetOptimizerInterface.php
Normal file
32
web/core/lib/Drupal/Core/Asset/AssetOptimizerInterface.php
Normal 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);
|
||||
|
||||
}
|
393
web/core/lib/Drupal/Core/Asset/AssetResolver.php
Normal file
393
web/core/lib/Drupal/Core/Asset/AssetResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
83
web/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
Normal file
83
web/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
Normal 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);
|
||||
|
||||
}
|
94
web/core/lib/Drupal/Core/Asset/AttachedAssets.php
Normal file
94
web/core/lib/Drupal/Core/Asset/AttachedAssets.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
81
web/core/lib/Drupal/Core/Asset/AttachedAssetsInterface.php
Normal file
81
web/core/lib/Drupal/Core/Asset/AttachedAssetsInterface.php
Normal 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();
|
||||
|
||||
}
|
85
web/core/lib/Drupal/Core/Asset/CssCollectionGrouper.php
Normal file
85
web/core/lib/Drupal/Core/Asset/CssCollectionGrouper.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
187
web/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
Normal file
187
web/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
218
web/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
Normal file
218
web/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
261
web/core/lib/Drupal/Core/Asset/CssOptimizer.php
Normal file
261
web/core/lib/Drupal/Core/Asset/CssOptimizer.php
Normal 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)) . ')';
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Asset\Exception;
|
||||
|
||||
/**
|
||||
* Defines a custom exception for an invalid libraries-extend specification.
|
||||
*/
|
||||
class InvalidLibrariesExtendSpecificationException extends \RuntimeException {
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
70
web/core/lib/Drupal/Core/Asset/JsCollectionGrouper.php
Normal file
70
web/core/lib/Drupal/Core/Asset/JsCollectionGrouper.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
190
web/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
Normal file
190
web/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
104
web/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
Normal file
104
web/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
54
web/core/lib/Drupal/Core/Asset/JsOptimizer.php
Normal file
54
web/core/lib/Drupal/Core/Asset/JsOptimizer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
95
web/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
Normal file
95
web/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
70
web/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
Normal file
70
web/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
164
web/core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php
Normal file
164
web/core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
53
web/core/lib/Drupal/Core/Asset/LibraryDiscoveryInterface.php
Normal file
53
web/core/lib/Drupal/Core/Asset/LibraryDiscoveryInterface.php
Normal 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();
|
||||
|
||||
}
|
463
web/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
Normal file
463
web/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
Reference in a new issue