diff --git a/core/core.services.yml b/core/core.services.yml index d915e10..1167f20 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -559,3 +559,7 @@ services: tags: - { name: event_subscriber } arguments: ['@authentication'] + asset.library_repository: + class: Drupal\Core\Asset\AssetLibraryRepository + arguments: ['@module_handler'] + diff --git a/core/lib/Drupal/Core/Asset/AssetBag.php b/core/lib/Drupal/Core/Asset/AssetBag.php new file mode 100644 index 0000000..6beec9d --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetBag.php @@ -0,0 +1,150 @@ +isFrozen()) { + throw new \LogicException('Assets cannot be added to a frozen AssetBag.', E_ERROR); + } + + $this->assets[] = $asset; + if ($asset instanceof JavascriptAssetInterface) { + $this->hasJs = TRUE; + } + if ($asset instanceof StylesheetAssetInterface) { + $this->hasCss = TRUE; + } + } + + /** + * {@inheritdoc} + */ + public function addAssetBag(AssetBagInterface $bag, $freeze = TRUE) { + if ($this->isFrozen()) { + throw new \LogicException('Assets cannot be added to a frozen AssetBag.', E_ERROR); + } + + foreach ($bag->all() as $asset) { + $this->add($asset); + } + + if ($freeze) { + $bag->freeze(); + } + } + + /** + * {@inheritdoc} + */ + public function hasCss() { + return $this->hasCss; + } + + /** + * {@inheritdoc} + */ + public function getCss() { + $css = array(); + foreach ($this->assets as $asset) { + if ($asset instanceof StylesheetAssetInterface) { + $css[] = $asset; + } + } + + return $css; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assets; + } + + /** + * {@inheritdoc} + */ + public function addJsSetting($data) { + $this->javascript['settings']['data'][] = $data; + } + + /** + * {@inheritdoc} + */ + public function hasJs() { + return $this->hasJs; + } + + /** + * {@inheritdoc} + */ + public function getJs() { + $js = array(); + foreach ($this->assets as $asset) { + if ($asset instanceof JavascriptAssetInterface) { + $js[] = $asset; + } + } + + return $js; + } + + /** + * {@inheritdoc} + */ + public function freeze() { + $this->frozen = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isFrozen() { + return $this->frozen; + } + +} diff --git a/core/lib/Drupal/Core/Asset/AssetBagInterface.php b/core/lib/Drupal/Core/Asset/AssetBagInterface.php new file mode 100644 index 0000000..b32ada4 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetBagInterface.php @@ -0,0 +1,111 @@ + array( + 'group' => CSS_AGGREGATE_DEFAULT, + 'weight' => 0, + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + ), + 'js' => array( + 'group' => JS_DEFAULT, + 'every_page' => FALSE, + 'weight' => 0, + 'scope' => 'header', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + ), + ); + + protected $assetDefaults = array(); + + protected $classMap = array( + 'css' => array( + 'file' => 'Drupal\\Core\\Asset\\StylesheetFileAsset', + 'external' => 'Drupal\\Core\\Asset\\StylesheetExternalAsset', + 'string' => 'Drupal\\Core\\Asset\\StylesheetStringAsset', + ), + 'js' => array( + 'file' => 'Drupal\\Core\\Asset\\JavascriptFileAsset', + 'external' => 'Drupal\\Core\\Asset\\JavascriptExternalAsset', + 'string' => 'Drupal\\Core\\Asset\\JavascriptStringAsset', + ), + ); + + public function __construct() { + $this->restoreDefaults(); + } + + /** + * Adds an asset to the injected AM + * + * @todo Document. + */ + public function add(AssetInterface $asset) { + if (empty($this->bag)) { + throw new \Exception('No bag is currently attached to this collector.'); + } + $this->bag->add($asset); + return $this; + } + + /** + * Creates an asset and returns it. + * + * @param string $asset_type + * 'css' or 'js'. + * @param string $source_type + * 'file', 'external' or 'string'. + * @param ??? $data + * @param array $options + * ??? + * @param array $filters + * ??? + * + * @return \Drupal\Core\Asset\AssetInterface + */ + public function create($asset_type, $source_type, $data, $options = array(), $filters = array()) { + if (!isset($this->classMap[$asset_type])) { + throw new \InvalidArgumentException('Only assets of type "js" or "css" are allowed.'); + } + if (!isset($this->classMap[$asset_type][$source_type])) { + throw new \InvalidArgumentException('Only sources of type "file", "string", or "external" are allowed.'); + } + + $class = $this->classMap[$asset_type][$source_type]; + $asset = new $class($data, $options, $filters); + $asset->setDefaults($this->getDefaults($asset_type)); + + if (!empty($this->bag)) { + $this->add($asset); + } + + return $asset; + } + + public function setBag(AssetBagInterface $bag) { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. A new bag cannot assigned on a locked collector.'); + } + $this->bag = $bag; + } + + public function clearBag() { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. Bags cannot be cleared on a locked collector.'); + } + $this->bag = NULL; + } + + public function createJavascriptSetting() { + // TODO figure out settings + } + + public function lock($key) { + if ($this->isLocked()) { + throw new \Exception('Collector is already locked.', E_WARNING); + } + + $this->locked = TRUE; + $this->lockKey = $key; + return TRUE; + } + + public function unlock($key) { + if (!$this->isLocked()) { + throw new \Exception('Collector is not locked', E_WARNING); + } + + if ($this->lockKey !== $key) { + throw new \Exception('Attempted to unlock Collector with incorrect key.', E_WARNING); + } + + $this->locked = FALSE; + $this->lockKey = NULL; + return TRUE; + } + + public function isLocked() { + return $this->locked; + } + + public function setDefaults($type, array $defaults) { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + $this->assetDefaults[$type] = array_merge($this->assetDefaults[$type], $defaults); + } + + public function getDefaults($type = NULL) { + if (!isset($type)) { + return $this->assetDefaults; + } + + if (!isset($this->assetDefaults[$type])) { + throw new \InvalidArgumentException(sprintf('The type provided, "%s", is not known.', $type)); + } + + return $this->assetDefaults[$type]; + } + + public function restoreDefaults() { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + $this->assetDefaults = $this->defaultAssetDefaults; + } +} diff --git a/core/lib/Drupal/Core/Asset/AssetDependencyInterface.php b/core/lib/Drupal/Core/Asset/AssetDependencyInterface.php new file mode 100644 index 0000000..bf37208 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetDependencyInterface.php @@ -0,0 +1,52 @@ + $val) { + $this->$key = $val; + } + } + + /** + * Set the asset library's title. + * + * @param string $title + * The title of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setTitle($title) { + $this->attemptWrite(); + $this->title = $title; + return $this; + } + + /** + * Get the asset library's title. + * + * @return string + * The title of the asset library. + */ + public function getTitle() { + return $this->title; + } + + /** + * Set the asset library's website. + * + * @param string $website + * The website of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setWebsite($website) { + $this->attemptWrite(); + $this->website = $website; + return $this; + } + + /** + * Get the asset library's website. + * + * @return string + * The website of the asset library. + */ + public function getWebsite() { + return $this->website; + } + + /** + * Set the asset library's version. + * + * @param string $version + * The version of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setVersion($version) { + $this->attemptWrite(); + $this->version = $version; + return $this; + } + + /** + * Get the asset library's version. + * + * @return string + * The version of the asset library. + */ + public function getVersion() { + return $this->version; + } + + /** + * {@inheritdoc} + */ + public function hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function addDependency($module, $name) { + $this->attemptWrite(); + $this->dependencies[] = array($module, $name); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->attemptWrite(); + $this->dependencies = array(); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() { + return $this->dependencies; + } + + /** + * Checks if the asset library is frozen, throws an exception if it is. + */ + protected function attemptWrite() { + if ($this->isFrozen()) { + throw new \LogicException('Metadata cannot be modified on a frozen AssetLibrary.'); + } + } +} diff --git a/core/lib/Drupal/Core/Asset/AssetLibraryCollector.php b/core/lib/Drupal/Core/Asset/AssetLibraryCollector.php new file mode 100644 index 0000000..b1239cd --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetLibraryCollector.php @@ -0,0 +1,95 @@ +manager = $manager; + } + + public function add($name, AssetLibrary $library) { + $this->manager->add($this->module, $name, $library); + return $this; + } + + public function buildLibrary($name, $values) { + $library = $this->createLibrary($name, $values); + + $collector = new AssetCollector(); + $collector->setBag($library); + $collector->setDefaults('js', array('group' => JS_LIBRARY)); + $collector->lock($this->getPrivateKey()); // TODO is locking here a bad idea? + + return $collector; + } + + public function createLibrary($name, $values) { + $library = new AssetLibrary($values); + $this->add($name, $library); + + return $library; + } + + public function setModule($module) { + $this->module = $module; + } + + public function lock($key) { + if ($this->isLocked()) { + throw new \Exception('Collector is already locked.', E_WARNING); + } + + $this->locked = TRUE; + $this->lockKey = $key; + return TRUE; + } + + public function unlock($key) { + if (!$this->isLocked()) { + throw new \Exception('Collector is not locked', E_WARNING); + } + + if ($this->lockKey !== $key) { + throw new \Exception('Attempted to unlock Collector with incorrect key.', E_WARNING); + } + + $this->locked = FALSE; + $this->lockKey = NULL; + return TRUE; + } + + public function isLocked() { + return $this->locked; + } + + protected function getPrivateKey() { + if (empty($this->privateKey)) { + // This doesn't need to be highly secure, just decently random. + $this->privateKey = Crypt::randomStringHashed(8); + } + return $this->privateKey; + } + +} diff --git a/core/lib/Drupal/Core/Asset/AssetLibraryRepository.php b/core/lib/Drupal/Core/Asset/AssetLibraryRepository.php new file mode 100644 index 0000000..55108fc --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetLibraryRepository.php @@ -0,0 +1,157 @@ +moduleHandler = $module_handler; + } + + protected function initialize() { + if ($this->initialized) { + return; + } + $this->initialized = TRUE; + + $library_collector = new AssetLibraryCollector($this); + foreach ($this->moduleHandler->getImplementations('library_info') as $module) { + $library_collector->setModule($module); + $libraries = call_user_func("{$module}_library_info"); + foreach ($libraries as $name => $info) { + // Normalize - apparently hook_library_info is allowed to be sloppy. + $info += array('dependencies' => array(), 'js' => array(), 'css' => array()); + + // @todo This works sorta sanely because of the array_intersect_key() hack in AssetLibrary::construct() + $asset_collector = $library_collector->buildLibrary($name, $info); + foreach (array('js', 'css') as $type) { + if (!empty($info[$type])) { + foreach ($info[$type] as $data => $options) { + if (is_scalar($options)) { + $data = $options; + $options = array(); + } + // @todo good enough for now to assume these are all file assets + $asset_collector->create($type, 'file', $data, $options); + } + } + } + } + } + } + + /** + * Gets a library by composite key. + * + * @param string $module + * The module owner that declared the library. + * + * @param string $name + * The library name. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The requested library. + * + * @throws \InvalidArgumentException If there is no library by that name + */ + public function get($module, $name) { + $this->initialize(); + if (!isset($this->libraries[$module][$name])) { + throw new \InvalidArgumentException(sprintf('There is library identified by "%s/%s" in the manager.', $module, $name)); + } + + return $this->libraries[$module][$name]; + } + + /** + * Checks if the current library manager has a certain library. + * + * @param string $module + * The module owner that declared the library. + * + * @param string $name + * The library name. + * + * @return bool + * True if the library has been set, false if not + */ + public function has($module, $name) { + $this->initialize(); + return isset($this->libraries[$module][$name]); + } + + public function add($module, $name, AssetLibrary $library) { + // TODO add validation - alphanum + underscore only + if (!isset($this->libraries[$module])) { + $this->libraries[$module] = array(); + } + + $this->libraries[$module][$name] = $library; + $this->flattened = NULL; + } + + /** + * Returns an array of library names. + * + * @return array An array of library names + */ + public function getNames() { + $this->initialize(); + return array_keys($this->libraries); + } + + /** + * Clears all libraries. + */ + public function clear() { + $this->initialize(); + $this->libraries = array(); + $this->flattened = NULL; + } + + public function getIterator() { + $this->initialize(); + if (is_null($this->flattened)) { + $this->flattened = array(); + foreach ($this->libraries as $module => $set) { + foreach ($set as $name => $library) { + $this->flattened["$module:$name"] = $library; + } + } + } + + return new \ArrayIterator($this->flattened); + } + +} diff --git a/core/lib/Drupal/Core/Asset/BaseAsset.php b/core/lib/Drupal/Core/Asset/BaseAsset.php new file mode 100644 index 0000000..e2316df --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseAsset.php @@ -0,0 +1,284 @@ +filters = new FilterCollection($filters); + $this->sourceRoot = $sourceRoot; + $this->sourcePath = $sourcePath; + $this->vars = array(); // TODO remove + $this->values = array(); // TODO remove + $this->loaded = FALSE; + + foreach ($options as $k => $v) { + $this->metadata[$k] = $v; + } + } + + public function __clone() { + $this->filters = clone $this->filters; + } + + /** + * {@inheritdoc} + */ + public function ensureFilter(FilterInterface $filter) { + $this->filters->ensure($filter); + } + + /** + * {@inheritdoc} + */ + public function getFilters() { + return $this->filters->all(); + } + + /** + * {@inheritdoc} + */ + public function clearFilters() { + $this->filters->clear(); + } + + /** + * Encapsulates asset loading logic. + * + * @param string $content The asset content + * @param FilterInterface $additionalFilter An additional filter + */ + protected function doLoad($content, FilterInterface $additionalFilter = NULL) { + $filter = clone $this->filters; + if ($additionalFilter) { + $filter->ensure($additionalFilter); + } + + $asset = clone $this; + $asset->setContent($content); + + $filter->filterLoad($asset); + $this->content = $asset->getContent(); + + $this->loaded = TRUE; + } + + /** + * {@inheritdoc} + */ + public function dump(FilterInterface $additionalFilter = NULL) { + if (!$this->loaded) { + $this->load(); + } + + $filter = clone $this->filters; + if ($additionalFilter) { + $filter->ensure($additionalFilter); + } + + $asset = clone $this; + $filter->filterDump($asset); + + return $asset->getContent(); + } + + /** + * {@inheritdoc} + */ + public function getContent() { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent($content) { + $this->content = $content; + } + + /** + * {@inheritdoc} + */ + public function getSourceRoot() { + return $this->sourceRoot; + } + + /** + * {@inheritdoc} + */ + public function getSourcePath() { + return $this->sourcePath; + } + + /** + * {@inheritdoc} + */ + public function getTargetPath() { + return $this->targetPath; + } + + /** + * {@inheritdoc} + */ + public function setTargetPath($targetPath) { + if ($this->vars) { + foreach ($this->vars as $var) { + if (FALSE === strpos($targetPath, $var)) { + throw new \RuntimeException(sprintf('The asset target path "%s" must contain the variable "{%s}".', $targetPath, $var)); + } + } + } + + $this->targetPath = $targetPath; + } + + /** + * {@inheritdoc} + */ + public function getVars() { + // TODO turn this off + return $this->vars; + } + + /** + * {@inheritdoc} + */ + public function setValues(array $values) { + // TODO turn this off + foreach ($values as $var => $v) { + if (!in_array($var, $this->vars, TRUE)) { + throw new \InvalidArgumentException(sprintf('The asset with source path "%s" has no variable named "%s".', $this->sourcePath, $var)); + } + } + + $this->values = $values; + $this->loaded = FALSE; + } + + /** + * {@inheritdoc} + */ + public function getValues() { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function isPreprocessable() { + return (bool) $this->metadata['preprocess']; + } + + public function setDefaults(array $defaults) { + $this->metadataDefaults = $defaults; + } + + /** + * {@inheritdoc} + */ + public function isDefault($offset) { + if (!$this->offsetExists($offset)) { + return; + } + + return !isset($this->metadata); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) { + return isset($this->metadata) || isset($this->metadataDefaults); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) { + return $this->metadata[$offset] ?: $this->metadataDefaults[$offset]; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) { + $this->metadata[$offset] = $value; + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) { + // TODO probably a gotcha that this only unsets the explicit val, but still better than breaking pattern around how defaults work + unset($this->metadata[$offset]); + } + + /** + * {@inheritdoc} + */ + public function hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function addDependency($module, $name) { + $this->dependencies[] = array($module, $name); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->dependencies = array(); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() { + return $this->dependencies; + } +} diff --git a/core/lib/Drupal/Core/Asset/BaseExternalAsset.php b/core/lib/Drupal/Core/Asset/BaseExternalAsset.php new file mode 100644 index 0000000..abe6834 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseExternalAsset.php @@ -0,0 +1,74 @@ +sourceUrl = $sourceUrl; + $this->ignoreErrors = FALSE; // TODO expose somehow + + list($scheme, $url) = explode('://', $sourceUrl, 2); + list($host, $path) = explode('/', $url, 2); + + parent::__construct($options, $filters, $scheme.'://'.$host, $path); + } + /** + * Returns the time the current asset was last modified. + * + * @todo copied right from Assetic. needs to be made more Drupalish. + * + * @return integer|null A UNIX timestamp + */ + public function getLastModified() { + if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(array('http' => array('method' => 'HEAD'))))) { + foreach ($http_response_header as $header) { + if (0 === stripos($header, 'Last-Modified: ')) { + list(, $mtime) = explode(':', $header, 2); + + return strtotime(trim($mtime)); + } + } + } + } + + /** + * Loads the asset into memory and applies load filters. + * + * You may provide an additional filter to apply during load. + * + * @param FilterInterface $additionalFilter An additional filter + */ + public function load(FilterInterface $additionalFilter = NULL) { + // TODO convert PathUtils call + if (false === $content = @file_get_contents(PathUtils::resolvePath( + $this->sourceUrl, $this->getVars(), $this->getValues()))) { + if ($this->ignoreErrors) { + return; + } else { + throw new \RuntimeException(sprintf('Unable to load asset from URL "%s"', $this->sourceUrl)); + } + } + + $this->doLoad($content, $additionalFilter); + } + +} diff --git a/core/lib/Drupal/Core/Asset/BaseFileAsset.php b/core/lib/Drupal/Core/Asset/BaseFileAsset.php new file mode 100644 index 0000000..415532a --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseFileAsset.php @@ -0,0 +1,56 @@ +source = $source; + + parent::__construct($options, $filters, $sourceRoot, $sourcePath); + } + + /** + * Returns the time the current asset was last modified. + * + * @return integer|null A UNIX timestamp + */ + public function getLastModified() { + if (!is_file($this->source)) { + throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source)); + } + + return filemtime($this->source); + } + + /** + * Loads the asset into memory and applies load filters. + * + * You may provide an additional filter to apply during load. + * + * @todo copied right from Assetic. needs to be made more Drupalish. + * + * @param FilterInterface $additionalFilter An additional filter + */ + public function load(FilterInterface $additionalFilter = NULL) { + if (!is_file($this->source)) { + throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source)); + } + + $this->doLoad(file_get_contents($this->source), $additionalFilter); + } + +} diff --git a/core/lib/Drupal/Core/Asset/BaseStringAsset.php b/core/lib/Drupal/Core/Asset/BaseStringAsset.php new file mode 100644 index 0000000..e51a3ef --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseStringAsset.php @@ -0,0 +1,35 @@ +content = $content; + $this->lastModified = REQUEST_TIME; + + parent::__construct($options, $filters); + } + + public function setLastModified($last_modified) { + $this->lastModified = $last_modified; + } + + public function getLastModified() { + return $this->lastModified; + } + + public function load(FilterInterface $additionalFilter = NULL) { + $this->doLoad($this->content, $additionalFilter); + } +} diff --git a/core/lib/Drupal/Core/Asset/JavascriptAssetInterface.php b/core/lib/Drupal/Core/Asset/JavascriptAssetInterface.php new file mode 100644 index 0000000..01f4930 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JavascriptAssetInterface.php @@ -0,0 +1,28 @@ +scope = $scope; + } + + public function getScope() { + return empty($this->scope) ? $this->scopeDefault : $this->scope; + } + + public function getScopeDefault() { + return $this->scopeDefault; + } +} diff --git a/core/lib/Drupal/Core/Asset/JavascriptFileAsset.php b/core/lib/Drupal/Core/Asset/JavascriptFileAsset.php new file mode 100644 index 0000000..3235e14 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JavascriptFileAsset.php @@ -0,0 +1,39 @@ +scope = $scope; + } + + public function getScope() { + return empty($this->scope) ? $this->scopeDefault : $this->scope; + } + + public function getScopeDefault() { + return $this->scopeDefault; + } +} diff --git a/core/lib/Drupal/Core/Asset/JavascriptStringAsset.php b/core/lib/Drupal/Core/Asset/JavascriptStringAsset.php new file mode 100644 index 0000000..c8f3767 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JavascriptStringAsset.php @@ -0,0 +1,38 @@ +scope = $scope; + } + + public function getScope() { + return empty($this->scope) ? $this->scopeDefault : $this->scope; + } + + public function getScopeDefault() { + return $this->scopeDefault; + } +} diff --git a/core/lib/Drupal/Core/Asset/StylesheetAssetInterface.php b/core/lib/Drupal/Core/Asset/StylesheetAssetInterface.php new file mode 100644 index 0000000..9a81daf --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StylesheetAssetInterface.php @@ -0,0 +1,40 @@ +media; + } + + /** + * Returns the default value of the media property on this stylesheet asset. + * + * @return mixed + */ + public function getMediaDefault() { + return $this->mediaDefault; + } + + /** + * Sets the media property to be applied on this stylesheet asset. + * + * @param string $type + * Either a media type, or a media query. + * + * @return NULL + */ + public function setMedia($type) { + $this->media = $type; + } +} diff --git a/core/lib/Drupal/Core/Asset/StylesheetFileAsset.php b/core/lib/Drupal/Core/Asset/StylesheetFileAsset.php new file mode 100644 index 0000000..38b3a6b --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StylesheetFileAsset.php @@ -0,0 +1,55 @@ +media; + } + + /** + * Returns the default value of the media property on this stylesheet asset. + * + * @return mixed + */ + public function getMediaDefault() { + return $this->mediaDefault; + } + + /** + * Sets the media property to be applied on this stylesheet asset. + * + * @param string $type + * Either a media type, or a media query. + * + * @return NULL + */ + public function setMedia($type) { + $this->media = $type; + } +} diff --git a/core/lib/Drupal/Core/Asset/StylesheetStringAsset.php b/core/lib/Drupal/Core/Asset/StylesheetStringAsset.php new file mode 100644 index 0000000..639f6c1 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StylesheetStringAsset.php @@ -0,0 +1,57 @@ +media; + } + + /** + * Returns the default value of the media property on this stylesheet asset. + * + * @return mixed + */ + public function getMediaDefault() { + return $this->mediaDefault; + } + + /** + * Sets the media property to be applied on this stylesheet asset. + * + * @param string $type + * Either a media type, or a media query. + * + * @return NULL + */ + public function setMedia($type) { + $this->media = $type; + } +} diff --git a/core/modules/block/lib/Drupal/block/BlockBase.php b/core/modules/block/lib/Drupal/block/BlockBase.php index 6efda89..c285870 100644 --- a/core/modules/block/lib/Drupal/block/BlockBase.php +++ b/core/modules/block/lib/Drupal/block/BlockBase.php @@ -8,6 +8,7 @@ namespace Drupal\block; use Drupal\Component\Plugin\PluginBase; +use Drupal\Core\Asset\AssetCollector; /** * Defines a base block implementation that most blocks plugins will extend. @@ -224,4 +225,9 @@ public function submit($form, &$form_state) { * @see \Drupal\block\BlockBase::submit() */ public function blockSubmit($form, &$form_state) {} + + /** + * {@inheritdoc} + */ + public function declareAssets(AssetCollector $collector) {} } diff --git a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php index 1887816..74e449e 100644 --- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php +++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php @@ -7,6 +7,8 @@ namespace Drupal\block; +use Drupal\Core\Asset\AssetCollector; + /** * Defines the required interface for all block plugins. * @@ -102,4 +104,12 @@ public function submit($form, &$form_state); */ public function build(); + /** + * Declares the assets required by this block to a collector. + * + * @param \Drupal\Core\Asset\AssetCollector $collector + * + * @return void + */ + public function declareAssets(AssetCollector $collector); } diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetAssemblyTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetAssemblyTest.php new file mode 100644 index 0000000..4ee8134 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetAssemblyTest.php @@ -0,0 +1,94 @@ + 'Asset Assembly tests', + 'description' => 'Tests to ensure assets declared via the various possible approaches come out with the correct properties, in the proper order.', + 'group' => 'Asset', + ); + } + + public function createJQueryAssetLibrary() { + $library = new AssetLibrary(array(new JavascriptFileAsset('core/misc/jquery.js'))); + return $library->setTitle('jQuery') + ->setVersion('1.8.2') + ->setWebsite('http://jquery.com'); + } + + /** + * Tests various simple single-bag asset assembly scenarios. + * + * Much of the real complexity of asset ordering in AssetBags comes from + * nesting them, but these tests are focused on the basic mechanics of + * assembly within a single bag. + */ + public function testSingleBagAssetAssemblies() { + // Dead-simple bag - contains just one css and one js assets, both local files. + $bag = new AssetBag(); + + $css1 = new StylesheetFileAsset(DRUPAL_ROOT . '/core/misc/vertical-tabs.css'); + $js1 = new JavascriptFileAsset(DRUPAL_ROOT . '/core/misc/ajax.js'); + + $bag->add($css1); + $bag->add($js1); + + $this->assertTrue($bag->hasCss(), 'AssetBag correctly reports that it contains CSS assets.'); + $this->assertTrue($bag->hasJs(), 'AssetBag correctly reports that it contains javascript assets.'); + + $this->assertEquals(array($css1), $bag->getCss()); + $this->assertEquals(array($js1), $bag->getJs()); + + $css2 = new StylesheetFileAsset(DRUPAL_ROOT . 'core/misc/dropbutton/dropbutton.base.css'); + $bag->add($css2); + + $this->assertEquals(array($css1, $css2), $bag->getCss()); + + $this->assertEquals(array($css1, $js1, $css2), $bag->all()); + } + + public function testSortingAndDependencyResolution() { + $bag = new AssetBag(); + + $alm = new AssetLibraryRepository(); + $alm->add('system', 'jquery', $this->createJQueryAssetLibrary()); + $dep = new AssetLibraryReference('jquery', $alm); + + $css1 = new StylesheetFileAsset(DRUPAL_ROOT . '/core/misc/vertical-tabs.css'); + $js1 = new JavascriptFileAsset(DRUPAL_ROOT . '/core/misc/ajax.js'); + // $js1->addDependency($dep); + + $bag->add($css1); + $bag->add($js1); + + $this->assertEquals(array(new JavascriptFileAsset('core/misc/jquery.js'), $js1), $bag->getJs()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetBagTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetBagTest.php new file mode 100644 index 0000000..4246e92 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetBagTest.php @@ -0,0 +1,34 @@ + '', // TODO give me a name + 'description' => '', // TODO give me a description + 'group' => '', // TODO give me the same group as above + ); + } + + public function setUp() { + parent::setUp(); + } + + public function testStub() { + // TODO anything. without this, phpunit blows up. + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetCollectorTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetCollectorTest.php new file mode 100644 index 0000000..479adf9 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetCollectorTest.php @@ -0,0 +1,231 @@ + array( + 'group' => CSS_AGGREGATE_DEFAULT, + 'weight' => 0, + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + ), + 'js' => array( + 'group' => JS_DEFAULT, + 'every_page' => FALSE, + 'weight' => 0, + 'scope' => 'header', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + ), + ); + + + public static function getInfo() { + return array( + 'name' => 'Asset Collector tests', + 'description' => 'Tests that the AssetCollector system works correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + $this->collector = new AssetCollector(); + } + + /** + * Tests that the collector injects provided metadata to created assets. + */ + public function testMetadataInjection() { + // Test a single value first + $asset = $this->collector->create('css', 'file', 'foo', array('group' => CSS_AGGREGATE_THEME)); + $this->assertEquals(CSS_AGGREGATE_THEME, $asset['group'], 'Collector injected user-passed parameters into the created asset.'); + + // TODO is it worth testing multiple params? what about weird ones, like weight? + } + + /** + * @expectedException Exception + */ + public function testExceptionOnAddingAssetWithoutBagPresent() { + $asset = $this->collector->create('css', 'string', 'foo'); + $this->collector->add($asset); + } + + /** + * TODO separate test for an explicit add() call. + */ + public function testAssetsImplicitlyArriveInInjectedBag() { + $bag = new AssetBag(); + $this->collector->setBag($bag); + + $asset2 = $this->collector->create('css', 'file', 'bar'); + $this->assertContains($asset2, $bag->getCss(), 'Created asset was implicitly added to bag.'); + } + + /** + * @expectedException Exception + */ + public function testClearBag() { + $bag = new AssetBag(); + $this->collector->setBag($bag); + $this->collector->clearBag(); + + $this->collector->add($this->collector->create('css', 'file', 'bar')); + } + + public function testLock() { + $this->assertTrue($this->collector->lock($this), 'Collector locked successfully.'); + $this->assertTrue($this->collector->isLocked(), 'Collector accurately reports that it is locked via isLocked() method.'); + } + + public function testUnlock() { + $this->collector->lock($this); + $this->assertTrue($this->collector->unlock($this), 'Collector unlocked successfully when appropriate key was provided.'); + $this->assertFalse($this->collector->isLocked(), 'Collector correctly reported unlocked state via isLocked() method after unlocking.'); + } + + /** + * @expectedException Exception + */ + public function testUnlockFailsWithoutCorrectSecret() { + $this->collector->lock('foo'); + $this->collector->unlock('bar'); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsSettingDefaults() { + $this->collector->lock($this); + $this->collector->setDefaults('css', array('foo' => 'bar')); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsRestoringDefaults() { + $this->collector->lock($this); + $this->collector->restoreDefaults(); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsClearingBag() { + $this->collector->lock($this); + $this->collector->clearBag(); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsSettingBag() { + $this->collector->lock($this); + $this->collector->setBag(new AssetBag()); + } + + public function testBuiltinDefaultAreTheSame() { + $this->assertEquals($this->builtinDefaults, $this->collector->getDefaults(), 'Expected set of built-in defaults reside in the collector.'); + } + + public function testChangeAndRestoreDefaults() { + $changed_defaults = array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME); + $this->collector->setDefaults('css', $changed_defaults); + $this->assertEquals($changed_defaults + $this->builtinDefaults['css'], $this->collector->getDefaults('css'), 'Expected combination of built-in and injected defaults reside in the collector.'); + + $this->collector->restoreDefaults(); + $this->assertEquals($this->builtinDefaults, $this->collector->getDefaults(), 'Built-in defaults were correctly restored.'); + + } + + /** + * @expectedException InvalidArgumentException + */ + public function testGetNonexistentDefault() { + $this->collector->getDefaults('foo'); + $this->fail('No exception thrown when an invalid key was requested.'); + } + + public function testDefaultPropagation() { + // Test that defaults are correctly applied by the factory. + $this->collector->setDefaults('css', array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME)); + $css1 = $this->collector->create('css', 'file', 'foo'); + $this->assertTrue($css1['every_page'], 'Correct default propagated for "every_page" property.'); + $this->assertEquals(CSS_AGGREGATE_THEME, $css1['group'], 'Correct default propagated for "group" property.'); + + // TODO bother testing js? it seems logically redundant + } + + public function testCreateStylesheetFileAsset() { + $css_file1 = $this->collector->create('css', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StylesheetFileAsset', $css_file1, 'Collector correctly created a StylesheetFileAsset instance.'); + } + + public function testCreateStylesheetExternalAsset() { + $css_external1 = $this->collector->create('css', 'external', 'http://foo.bar/path/to/asset.css'); + $this->assertInstanceOf('\Drupal\Core\Asset\StylesheetExternalAsset', $css_external1, 'Collector correctly created a StylesheetExternalAsset instance.'); + } + + public function testCreateStylesheetStringAsset() { + $css_string1 = $this->collector->create('css', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StylesheetStringAsset', $css_string1, 'Collector correctly created a StylesheetStringAsset instance .'); + } + + public function testCreateJavascriptFileAsset() { + $js_file1 = $this->collector->create('js', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\JavascriptFileAsset', $js_file1, 'Collector correctly created a JavascriptFileAsset instance .'); + } + + public function testCreateJavascriptExternalAsset() { + $js_external1 = $this->collector->create('js', 'external', 'http://foo.bar/path/to/asset.js'); + $this->assertInstanceOf('\Drupal\Core\Asset\JavascriptExternalAsset', $js_external1, 'Collector correctly created a JavascriptExternalAsset instance .'); + } + + public function testCreateJavascriptStringAsset() { + $js_string1 = $this->collector->create('js', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\JavascriptStringAsset', $js_string1, 'Collector correctly created a JavascriptStringAsset instance .'); + } +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetLibraryManagerTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryManagerTest.php new file mode 100644 index 0000000..59a7b11 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryManagerTest.php @@ -0,0 +1,31 @@ + '', // TODO give me a name + 'description' => '', // TODO give me a description + 'group' => '', // TODO give me the same group as above + ); + } + + public function setUp() { + parent::setUp(); + } + + public function testStub() { + // TODO anything. without this, phpunit blows up. + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetLibraryTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryTest.php new file mode 100644 index 0000000..fc3ff96 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryTest.php @@ -0,0 +1,96 @@ + 'Asset Library tests', + 'description' => 'Tests that the AssetLibrary behaves correctly.', + 'group' => 'Asset', + ); + } + + public function getLibraryFixture() { + $library = new AssetLibrary(); + $library->setTitle('foo') + ->setVersion('1.2.3') + ->setWebsite('http://foo.bar') + ->addDependency('foo', 'bar'); + return $library; + } + + public function testConstructorValueInjection() { + $values = array( + 'title' => 'foo', + 'version' => '1.2.3', + 'website' => 'http://foo.bar', + 'dependencies' => array(array('foo', 'bar')), + ); + $library = new AssetLibrary($values); + + $fixture = $this->getLibraryFixture(); + $this->assertEquals($fixture->getTitle(), $library->getTitle(), 'Title passed correctly through the constructor.'); + $this->assertEquals($fixture->getVersion(), $library->getVersion(), 'Version passed correctly through the constructor.'); + $this->assertEquals($fixture->getWebsite(), $library->getWebsite(), 'Website passed correctly through the constructor.'); + $this->assertEquals($fixture->getDependencies(), $library->getDependencies(), 'Dependencies information passed correctly through the constructor.'); + } + + public function testAddDependency() { + $library = $this->getLibraryFixture(); + $library->addDependency('baz', 'bing'); + $this->assertEquals($library->getDependencies(), array(array('foo', 'bar'), array('baz', 'bing')), 'Dependencies added to library successfully.'); + } + + public function testClearDependencies() { + $library = $this->getLibraryFixture(); + $library->clearDependencies(); + $this->assertEmpty($library->getDependencies(), 'Dependencies recorded in the library were cleared correctly.'); + } + + public function testFrozenNonwriteability() { + $library = $this->getLibraryFixture(); + $library->freeze(); + try { + $library->setTitle('bar'); + $this->fail('No exception thrown when attempting to set a new title on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->setVersion('2.3.4'); + $this->fail('No exception thrown when attempting to set a new version on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->setWebsite('http://bar.baz'); + $this->fail('No exception thrown when attempting to set a new website on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->addDependency('bing', 'bang'); + $this->fail('No exception thrown when attempting to add a new dependency on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->clearDependencies(); + $this->fail('No exception thrown when attempting to clear dependencies from a frozen library.'); + } + catch (\LogicException $e) {} + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetMetadataTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetMetadataTest.php new file mode 100644 index 0000000..b06b9a0 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetMetadataTest.php @@ -0,0 +1,52 @@ + 'Asset Metadata Tests', + 'description' => 'Tests that asset classes handle their metadata and defaults correctly.', + 'group' => 'Asset', + ); + } + + public function testDefaultsOverriddenByExplicitValues() { + // As this logic is implemented on the common parent, BaseAsset, testing + // one type of asset is sufficient. + $asset = new StylesheetFileAsset('foo', array('group' => CSS_AGGREGATE_THEME, 'every_page' => TRUE)); + $defaults = array( + 'group' => CSS_AGGREGATE_DEFAULT, + 'weight' => 0, + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + ); + $asset->setDefaults($defaults); + + foreach ($defaults as $key => $value) { + if (in_array($key, array('group', 'every_page'))) { + $this->assertNotEquals($value, $asset[$key], 'Explicit value correctly overrides default.'); + } + else { + $this->assertEquals($value, $asset[$key], 'Default value comes through when no explicit value is present.'); + } + } + } +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetTest.php new file mode 100644 index 0000000..2752d8c --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetTest.php @@ -0,0 +1,37 @@ + 'Asset tests', + 'description' => 'Unit tests for all base asset classes.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + } + + public function testStub() { + // TODO anything. without this, phpunit blows up. + } +}