diff --git a/composer.json b/composer.json index 436dae9..b45baf8 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "symfony-cmf/routing": "1.1.*@alpha", "easyrdf/easyrdf": "0.8.*@beta", "phpunit/phpunit": "3.7.*", - "zendframework/zend-feed": "2.2.*" + "zendframework/zend-feed": "2.2.*", + "sdboyer/gliph": "0.1.*" }, "autoload": { "psr-0": { diff --git a/composer.lock b/composer.lock index 5a70a6e..82c158d 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "4c727150110aeae10efe10a92ff256f5", + "hash": "5fad1bdb3b642274dcb00854fbd08808", "packages": [ { "name": "doctrine/annotations", @@ -647,21 +647,21 @@ }, { "name": "kriswallsmith/assetic", - "version": "v1.1.1", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/kriswallsmith/assetic.git", - "reference": "v1.1.1" + "reference": "735cffd3982c6e8cdebe292d5db39d077f65890f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kriswallsmith/assetic/zipball/v1.1.1", - "reference": "v1.1.1", + "url": "https://api.github.com/repos/kriswallsmith/assetic/zipball/735cffd3982c6e8cdebe292d5db39d077f65890f", + "reference": "735cffd3982c6e8cdebe292d5db39d077f65890f", "shasum": "" }, "require": { "php": ">=5.3.1", - "symfony/process": ">=2.1,<3.0" + "symfony/process": "~2.1" }, "require-dev": { "cssmin/cssmin": "*", @@ -671,9 +671,9 @@ "leafo/scssphp": "*", "leafo/scssphp-compass": "*", "mrclay/minify": "*", - "phpunit/phpunit": ">=3.7,<4.0", + "phpunit/phpunit": "~3.7", "ptachoire/cssembed": "*", - "twig/twig": ">=1.6,<2.0" + "twig/twig": "~1.6" }, "suggest": { "leafo/lessphp": "Assetic provides the integration with the lessphp LESS compiler", @@ -714,7 +714,7 @@ "compression", "minification" ], - "time": "2013-06-01 22:13:43" + "time": "2013-07-19 00:03:27" }, { "name": "phpunit/php-code-coverage", @@ -1547,17 +1547,17 @@ }, { "name": "symfony/process", - "version": "v2.3.4", + "version": "v2.3.6", "target-dir": "Symfony/Component/Process", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", - "reference": "1e91553e1cedd0b8fb1da6ea4f89b02e21713d5b" + "reference": "81191e354fe9dad0451036ccf0fdf283649d3f1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/1e91553e1cedd0b8fb1da6ea4f89b02e21713d5b", - "reference": "1e91553e1cedd0b8fb1da6ea4f89b02e21713d5b", + "url": "https://api.github.com/repos/symfony/Process/zipball/81191e354fe9dad0451036ccf0fdf283649d3f1e", + "reference": "81191e354fe9dad0451036ccf0fdf283649d3f1e", "shasum": "" }, "require": { @@ -1590,7 +1590,7 @@ ], "description": "Symfony Process Component", "homepage": "http://symfony.com", - "time": "2013-08-22 06:42:25" + "time": "2013-10-09 21:17:57" }, { "name": "symfony/routing", diff --git a/core/core.services.yml b/core/core.services.yml index 608c439..9dd036c 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -631,3 +631,4 @@ services: class: Drupal\Core\Asset\JsCollectionGrouper asset.js.dumper: class: Drupal\Core\Asset\AssetDumper + diff --git a/core/includes/common.inc b/core/includes/common.inc index 77ea718..687eeb9 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1631,6 +1631,9 @@ function drupal_add_css($data = NULL, $options = NULL) { // Create an array of CSS files for each media type first, since each type needs to be served // to the browser differently. if (isset($data)) { + $options['type'] = isset($options['type']) ? $options['type'] : 'file'; + drupal_collect_assets($data, $options, 'css'); + $options += array( 'type' => 'file', 'group' => CSS_AGGREGATE_DEFAULT, @@ -1682,6 +1685,29 @@ function drupal_add_css($data = NULL, $options = NULL) { return $css; } +function drupal_collect_assets($data, $options, $type = '') { + $collection = &drupal_static('global_asset_bag', FALSE); + $collector = &drupal_static('global_asset_collector', FALSE); + + $collection = ($collection instanceof \Drupal\Core\Asset\Collection\AssetCollection) ? $collection : new \Drupal\Core\Asset\Collection\AssetCollection(); + if (!$collector instanceof \Drupal\Core\Asset\Factory\AssetCollector) { + $collector = new \Drupal\Core\Asset\Factory\AssetCollector(); + $collector->setCollection($collection); + } + + if ($data instanceof \Drupal\Core\Asset\AssetInterface) { + $collector->add($data); + return; + } + + if ($type == 'js-setting') { + // TODO handle js settings + return; + } + + $collector->create($type, $options['type'], $data, $options); +} + /** * Returns a themed representation of all stylesheets to attach to the page. * @@ -2212,6 +2238,11 @@ function drupal_add_js($data = NULL, $options = NULL) { } $options += drupal_js_defaults($data); + if (isset($data) && is_array($options)) { + $options['type'] = isset($options['type']) ? $options['type'] : 'file'; + drupal_collect_assets($data, $options, $options['type'] == 'setting' ? 'js-setting' : 'js'); + } + // Preprocess can only be set if caching is enabled and no attributes are set. $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE; diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php new file mode 100644 index 0000000..8d37137 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php @@ -0,0 +1,53 @@ +metadata = $metadata; + $this->assetStorage = new \SplObjectStorage(); + $this->nestedStorage = new \SplObjectStorage(); + + foreach ($assets as $asset) { + $this->add($asset); + } + } + + /** + * {@inheritdoc} + */ + public function id() { + if (empty($this->id)) { + $this->calculateId(); + } + + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function getAssetType() { + return $this->metadata->getType(); + } + + /** + * Calculates and stores an id for this aggregate from the contained assets. + * + * @return void + */ + protected function calculateId() { + $id = ''; + foreach ($this->assetStorage as $asset) { + // Preserve a little id stability by not composing id from aggregates + if (!$asset instanceof AssetAggregateInterface) { + $id .= $asset->id(); + } + } + // TODO come up with something stabler/more serialization friendly than object hash + $this->id = hash('sha256', $id ?: spl_object_hash($this)); + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + // TODO should this immutable? doable if we further granulate the interfaces + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function add(AsseticAssetInterface $asset) { + if (!$asset instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); + } + $this->ensureCorrectType($asset); + + if (!$this->contains($asset)) { + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + } + + if ($asset instanceof AssetAggregateInterface) { + $this->nestedStorage->attach($asset); + } + } + + /** + * {@inheritdoc} + */ + public function contains(AssetInterface $asset) { + if ($this->assetStorage->contains($asset)) { + return TRUE; + } + + foreach ($this->nestedStorage as $aggregate) { + if ($aggregate->contains($asset)) { + return TRUE; + } + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getById($id, $graceful = TRUE) { + if (isset($this->assetIdMap[$id])) { + return $this->assetIdMap[$id]; + } + else { + // Recursively search for the id + foreach ($this->nestedStorage as $aggregate) { + if ($found = $aggregate->getById($id)) { + return $found; + } + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException(sprintf('This aggregate does not contain an asset with id %s.', $id)); + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = TRUE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + + return $this->removeLeaf($needle, $graceful); + } + + /** + * {@inheritdoc} + */ + public function removeLeaf(AsseticAssetInterface $needle, $graceful = FALSE) { + if (!$needle instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); + } + $this->ensureCorrectType($needle); + + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]); + + return TRUE; + } + + if ($asset instanceof AssetAggregateInterface && $asset->removeLeaf($needle, $graceful)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Asset not found.'); + } + + /** + * {@inheritdoc} + */ + public function replace($needle, AssetInterface $replacement, $graceful = TRUE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + + return $this->replaceLeaf($needle, $replacement, $graceful); + } + + /** + * {@inheritdoc} + */ + public function replaceLeaf(AsseticAssetInterface $needle, AsseticAssetInterface $replacement, $graceful = FALSE) { + if (!($needle instanceof AssetInterface && $replacement instanceof AssetInterface)) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset(s) provided; Drupal aggregates require Drupal-flavored assets.'); + } + $this->ensureCorrectType($needle); + $this->ensureCorrectType($replacement); + + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->nestedStorage[$asset]); + + array_splice($this->assetIdMap, $i, 1, array($replacement->id() => $replacement)); + $this->assetStorage->attach($replacement); + if ($replacement instanceof AssetAggregateInterface) { + $this->nestedStorage->attach($replacement); + } + + return TRUE; + } + + if ($asset instanceof AssetAggregateInterface && $asset->replaceLeaf($needle, $replacement, $graceful)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Asset not found.'); + } + + /** + * {@inheritdoc} + * + * Aggregate assets are inherently eligible for preprocessing, so this is + * always true. + */ + public function isPreprocessable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // loop through leaves and load each asset + $parts = array(); + foreach ($this as $asset) { + $asset->load($additionalFilter); + $parts[] = $asset->getContent(); + } + + $this->content = implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function dump(FilterInterface $additionalFilter = NULL) { + // loop through leaves and dump each asset + $parts = array(); + foreach ($this as $asset) { + $parts[] = $asset->dump($additionalFilter); + } + + return implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function getContent() { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent($content) { + $this->content = $content; + } + + /** + * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us? + */ + public function getIterator() { + // TODO this is totally junk + return new \ArrayIterator($this->assetIdMap); + } + + /** + * Indicates whether this collection contains any assets. + * + * @return bool + * TRUE if contained assets are present, FALSE otherwise. + */ + public function isEmpty() { + return $this->assetStorage->count() === 0; + } + + /** + * Ensures that the asset is of the correct subtype (e.g., css vs. js). + * + * @param AssetInterface $asset + * + * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + abstract protected function ensureCorrectType(AssetInterface $asset); +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php new file mode 100644 index 0000000..10ca555 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php @@ -0,0 +1,40 @@ +getAssetType() !== 'css') { + throw new AssetTypeMismatchException('CSS aggregates can only work with CSS assets.'); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php new file mode 100644 index 0000000..2c9f86d --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php @@ -0,0 +1,40 @@ +getAssetType() !== 'js') { + throw new AssetTypeMismatchException('JS aggregates can only work with JS assets.'); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php new file mode 100644 index 0000000..17b7191 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php @@ -0,0 +1,28 @@ +process = $process; + } + + /** + * {@inheritdoc} + */ + public function addVertex($vertex) { + if (!$vertex instanceof AssetInterface) { + throw new InvalidVertexTypeException('AssetGraph requires vertices to implement AssetInterface.'); + } + + if (!$this->hasVertex($vertex)) { + $this->vertices[$vertex] = new \SplObjectStorage(); + $this->verticesById[$vertex->id()] = $vertex; + + if ($this->process) { + $this->processNewVertex($vertex); + } + } + } + + /** + * Processes all positioning information for a given vertex. + * + * @param AssetInterface $vertex + */ + protected function processNewVertex(AssetInterface $vertex) { + $id = $vertex->id(); + // First, check if anything has a watch out for this vertex. + if (isset($this->before[$id])) { + foreach ($this->before[$id] as $predecessor) { + $this->addDirectedEdge($predecessor, $vertex); + } + unset($this->before[$id]); + } + + if (isset($this->after[$id])) { + foreach ($this->after[$id] as $successor) { + $this->addDirectedEdge($vertex, $successor); + } + unset($this->after[$id]); + } + + // Add watches for this vertex, if it implements the interface. + if ($vertex instanceof RelativePositionInterface) { + // TODO this logic assumes collections enforce uniqueness - ensure that's the case. + foreach ($vertex->getPredecessors() as $predecessor) { + // Normalize to id string. + $predecessor = is_string($predecessor) ? $predecessor : $predecessor->id(); + + if (isset($this->verticesById[$predecessor])) { + $this->addDirectedEdge($vertex, $this->verticesById[$predecessor]); + } + else { + if (!isset($this->before[$predecessor])) { + $this->before[$predecessor] = array(); + } + $this->before[$predecessor][] = $vertex; + } + } + + foreach ($vertex->getSuccessors() as $successor) { + // Normalize to id string. + $successor = is_string($successor) ? $successor : $successor->id(); + + if (isset($this->verticesById[$successor])) { + $this->addDirectedEdge($this->verticesById[$successor], $vertex); + } + else { + if (!isset($this->before[$successor])) { + $this->after[$successor] = array(); + } + $this->after[$successor][] = $vertex; + } + } + } + } + + /** + * Remove a vertex from the graph. Unsupported in AssetGraph. + * + * Vertex removals are unsupported because it would necessitate permanent + * bookkeeping on positioning data. With forty or fifty assets, each having + * only a few dependencies, there would be a fair bit of pointless iterating. + * + * @throws \LogicException + * This exception will always be thrown. + */ + public function removeVertex($vertex) { + throw new \LogicException('AssetGraph does not support vertex removals.'); + } + + /** + * {@inheritdoc} + */ + public function transpose() { + $graph = new self(FALSE); + $this->eachEdge(function($edge) use (&$graph) { + $graph->addDirectedEdge($edge[1], $edge[0]); + }); + + return $graph; + } +} diff --git a/core/lib/Drupal/Core/Asset/AssetInterface.php b/core/lib/Drupal/Core/Asset/AssetInterface.php new file mode 100644 index 0000000..ac1512b --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetInterface.php @@ -0,0 +1,59 @@ +metadata = $metadata; + parent::__construct($filters, $sourceRoot, $sourcePath); + } + + public function __clone() { + parent::__clone(); + $this->metadata = clone $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function getAssetType() { + return $this->metadata->getType(); + } + + /** + * {@inheritdoc} + */ + public function isPreprocessable() { + return (bool) $this->metadata->get('preprocess'); + } + + /** + * {@inheritdoc} + */ + public function hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function addDependency($module, $name) { + if (!(is_string($module) && is_string($name))) { + throw new \InvalidArgumentException('Dependencies must be expressed as 2-tuple with the first element being owner/module, and the second being name.'); + } + + $this->dependencies[] = array($module, $name); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->dependencies = array(); + } + + /** + * {@inheritdoc} + */ + public function getDependencyInfo() { + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function before($asset) { + if (!($asset instanceof AssetInterface || is_string($asset))) { + throw new \InvalidArgumentException('Ordering information must be declared using either an asset string id or the full AssetInterface object.'); + } + + $this->successors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function after($asset) { + if (!($asset instanceof AssetInterface || is_string($asset))) { + throw new \InvalidArgumentException('Ordering information must be declared using either an asset string id or the full AssetInterface object.'); + } + + $this->predecessors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function getPredecessors() { + return $this->predecessors; + } + + /** + * {@inheritdoc} + */ + public function getSuccessors() { + return $this->successors; + } + + /** + * {@inheritdoc} + */ + public function clearSuccessors() { + $this->successors = array(); + } + + /** + * {@inheritdoc} + */ + public function clearPredecessors() { + $this->predecessors = array(); + } +} diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php new file mode 100644 index 0000000..713ba1e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php @@ -0,0 +1,198 @@ +assetStorage = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function add(AssetInterface $asset) { + $this->attemptWrite(); + + if (!$this->contains($asset)) { + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + } + } + + /** + * {@inheritdoc} + */ + public function contains(AssetInterface $asset) { + // TODO decide whether to do this by id or object instance + return $this->assetStorage->contains($asset); + } + + /** + * {@inheritdoc} + */ + public function getById($id, $graceful = TRUE) { + if (isset($this->assetIdMap[$id])) { + return $this->assetIdMap[$id]; + } + else if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $id)); + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = TRUE) { + // TODO fix horrible complexity of conditionals, exceptions, and returns. + $this->attemptWrite(); + + // Validate and normalize type to AssetInterface + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + // Asset couldn't be found but we're in graceful mode - return FALSE. + return FALSE; + } + } + else if (!$needle instanceof AssetInterface) { + throw new \InvalidArgumentException('Invalid type provided to AssetCollection::remove(); must provide either a string asset id or AssetInterface instance.'); + } + + // Check for membership + if ($this->contains($needle)) { + unset($this->assetIdMap[$needle->id()], $this->assetStorage[$needle]); + return TRUE; + } + else if (!$graceful) { + throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $needle->id())); + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + */ + public function mergeCollection(AssetCollectionInterface $collection, $freeze = TRUE) { + $this->attemptWrite(); + + foreach ($collection as $asset) { + if (!$this->contains($asset)) { + $this->add($asset); + } + } + + if ($freeze) { + $collection->freeze(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function freeze() { + $this->frozen = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isFrozen() { + return $this->frozen; + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + return new \ArrayIterator($this->assetIdMap); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return empty($this->assetIdMap); + } + + /** + * {@inheritdoc} + */ + public function getCss() { + // TODO evaluate potential performance impact if this is done a lot... + $collection = new self(); + foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'css') as $asset) { + $collection->add($asset); + } + + return $collection; + } + + /** + * {@inheritdoc} + */ + public function getJs() { + $collection = new self(); + foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'js') as $asset) { + $collection->add($asset); + } + + return $collection; + } + + /** + * {@inheritdoc} + */ + public function sort($callback) { + uksort($this->assetIdMap, $callback); + } + + /** + * {@inheritdoc} + */ + public function ksort() { + ksort($this->assetIdMap); + } + + /** + * Checks if the asset library is frozen, throws an exception if it is. + */ + protected function attemptWrite() { + if ($this->isFrozen()) { + throw new \LogicException('Cannot write to a frozen AssetCollection.'); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php new file mode 100644 index 0000000..b837b6e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php @@ -0,0 +1,78 @@ + $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 getDependencyInfo() { + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function before($asset) { + $this->successors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function after($asset) { + $this->predecessors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function getPredecessors() { + return $this->predecessors; + } + + /** + * {@inheritdoc} + */ + public function getSuccessors() { + return $this->successors; + } + + /** + * {@inheritdoc} + */ + public function clearSuccessors() { + $this->successors = array(); + } + + /** + * {@inheritdoc} + */ + public function clearPredecessors() { + $this->predecessors = array(); + } + + /** + * 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/Collection/Iterator/AssetSubtypeFilterIterator.php b/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php new file mode 100644 index 0000000..5b2c1e5 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php @@ -0,0 +1,36 @@ +match = $match; + } + + /** + * {@inheritdoc} + */ + public function accept() { + return $this->current()->getAssetType() === $this->match; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php b/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php new file mode 100644 index 0000000..626cc1c --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php @@ -0,0 +1,68 @@ +sorter = $sorter; + } + + /** + * {@inheritdoc} + */ + public function aggregate(AssetCollectionInterface $collection) { + $tsl = $this->sorter->groupAndSort($collection); + + $processed = new AssetCollection(); + $last_key = FALSE; + foreach ($tsl as $asset) { + $key = $this->sorter->getGroupingKey($asset); + + if ($key && $key !== $last_key) { + $aggregate = new CssAggregateAsset($asset->getMetadata()); + $processed->add($aggregate); + } + + $aggregate->add($asset); + $last_key = $key; + } + + return $processed; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php new file mode 100644 index 0000000..1e64377 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php @@ -0,0 +1,89 @@ +aggregator = $aggregator; + $this->optimizer = $optimizer; + $this->dumper = $dumper; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function optimize(AssetCollectionInterface $collection) { + $collection = $this->aggregator->aggregate($collection); + + // Get the map of all aggregates that have been generated so far. + $map = $this->state->get('drupal_css_cache_files') ?: array(); + foreach ($collection as $asset) { + if ($asset->isPreprocessable()) { + $id = $asset->id(); + $uri = isset($map[$id]) ? $map[$id] : ''; + if (empty($uri) || !file_exists($uri)) { + // TODO optimizer needs to be refactored to basically just set filters. + $this->optimizer->optimize($asset); + // TODO refactor dumper to not need second param + $this->dumper->dump($asset, 'css'); + } + } + } + + return $collection; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/DependencyInterface.php b/core/lib/Drupal/Core/Asset/DependencyInterface.php new file mode 100644 index 0000000..35132a8 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/DependencyInterface.php @@ -0,0 +1,47 @@ +sourceUrl = $sourceUrl; + $this->ignoreErrors = FALSE; // TODO expose somehow + + list($scheme, $url) = explode('://', $sourceUrl, 2); + list($host, $path) = explode('/', $url, 2); + + parent::__construct($metadata, $filters, $scheme.'://'.$host, $path); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->sourceUrl; + } + + /** + * 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)); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // TODO dumb and kinda wrong, decide how to do this right. + throw new UnsupportedAsseticBehaviorException('Drupal does not support the retrieval or manipulation of remote assets.'); + } + +} diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php new file mode 100644 index 0000000..e74023c --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php @@ -0,0 +1,249 @@ + 'Drupal\\Core\\Asset\\FileAsset', + 'external' => 'Drupal\\Core\\Asset\\ExternalAsset', + 'string' => 'Drupal\\Core\\Asset\\StringAsset', + ); + + public function __construct(AssetCollectionInterface $collection = NULL, MetadataFactoryInterface $factory = NULL) { + if (!is_null($factory)) { + $this->metadataFactory = $factory; + } + else { + $this->restoreDefaults(); + } + + if (!is_null($collection)) { + $this->setCollection($collection); + } + } + + /** + * {@inheritdoc} + */ + public function add(AssetInterface $asset) { + if (empty($this->collection)) { + throw new \RuntimeException('No collection is currently attached to this collector.'); + } + $this->collection->add($asset); + return $this; + } + + /** + * {@inheritdoc} + */ + public function create($asset_type, $source_type, $data, $options = array(), $filters = array(), $keep_last = TRUE) { + // TODO this normalization points to a deeper modeling problem. + $source_type = $source_type == 'inline' ? 'string' : $source_type; + + if (!in_array($asset_type, array('css', 'js'))) { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are allowed, "%s" requested.', $asset_type)); + } + if (!isset($this->classMap[$source_type])) { + throw new \InvalidArgumentException(sprintf('Only sources of type "file", "string", or "external" are allowed, "%s" requested.', $source_type)); + } + + $metadata = $this->getMetadataDefaults($asset_type); + if (!empty($options)) { + $metadata->add($options); + } + + $class = $this->classMap[$source_type]; + $asset = new $class($metadata, $data, $filters); + + if (!empty($this->collection)) { + $this->add($asset); + } + + if ($asset_type == 'css' && !empty($this->lastCss)) { + $asset->after($this->lastCss); + } + + if ($keep_last) { + $this->lastCss = $asset; + } + + return $asset; + } + + /** + * {@inheritdoc} + */ + public function clearLastCss() { + unset($this->lastCss); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setCollection(AssetCollectionInterface $collection) { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. A new collection cannot be attached to a locked collector.'); + } + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function clearCollection() { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. Collections cannot be cleared on a locked collector.'); + } + $this->collection = NULL; + } + + /** + * {@inheritdoc} + */ + public function hasCollection() { + return $this->collection instanceof AssetCollectionInterface; + } + + /** + * {@inheritdoc} + */ + public function lock($key) { + if ($this->isLocked()) { + throw new LockedObjectException('Collector is already locked.', E_WARNING); + } + + $this->locked = TRUE; + $this->lockKey = $key; + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function unlock($key) { + if (!$this->isLocked()) { + throw new LockedObjectException('Collector is not locked', E_WARNING); + } + + if ($this->lockKey !== $key) { + throw new LockedObjectException('Attempted to unlock Collector with incorrect key.', E_WARNING); + } + + $this->locked = FALSE; + $this->lockKey = NULL; + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function isLocked() { + return $this->locked; + } + + /** + * {@inheritdoc} + */ + public function setMetadataFactory(MetadataFactoryInterface $factory) { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + + $this->metadataFactory = $factory; + } + + /** + * {@inheritdoc} + */ + public function getMetadataDefaults($type) { + if ($type === 'css') { + return $this->metadataFactory->createCssMetadata(); + } + elseif ($type === 'js') { + return $this->metadataFactory->createJsMetadata(); + } + else { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $type)); + } + } + + /** + * {@inheritdoc} + */ + public function restoreDefaults() { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + + $this->metadataFactory = new DefaultAssetMetadataFactory(); + } +} + diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php new file mode 100644 index 0000000..1d2b0fe --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php @@ -0,0 +1,211 @@ +source = $source; + + parent::__construct($metadata, $filters, $sourceRoot, $sourcePath); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->source; + } + + /** + * Returns the time the current asset was last modified. + * + * @return integer|null A UNIX timestamp + * + * @throws \RuntimeException + * Thrown if the source file does not exist. + */ + 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. + * + * @param FilterInterface $additionalFilter An additional filter + * + * @throws \RuntimeException + * Thrown if the source file does not exist. + */ + 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/GroupSort/AssetGraphSorter.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php new file mode 100644 index 0000000..6311f9f --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php @@ -0,0 +1,53 @@ +insert($vertex, count($reach_visitor->getReachable($vertex))); + } + + // Dump the priority queue into a normal queue + // TODO maybe gliph should support pq/heaps as a queue type on which to operate? + $queue = new \SplQueue(); + foreach ($pq as $vertex) { + $queue->push($vertex); + } + + return $queue; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php new file mode 100644 index 0000000..eeb771c --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php @@ -0,0 +1,45 @@ +getMetadata(); + // 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. + $browsers = $meta->get('browsers'); + ksort($browsers); + + if ($asset instanceof FileAsset) { + // Compose a string key out of the set of relevant properties. + // TODO - this ignores group, which is used in core's current implementation. wishful thinking? maybe, maybe not. + // TODO media has been pulled out - needs to be handled by the aggregator, wrapping css in media queries + $k = $asset->isPreprocessable() + ? implode(':', array('file', $meta->get('every_page'), implode('', $browsers))) + : FALSE; + } + else if ($asset instanceof StringAsset) { + // String items are always grouped. + // TODO use the term 'inline' here? do "string" and "inline" necessarily mean the same? + $k = implode(':', 'string', implode('', $browsers)); + } + else if ($asset instanceof ExternalAsset) { + // Never group external assets. + $k = FALSE; + } + else { + throw new \UnexpectedValueException(sprintf('Unknown CSS asset type "%s" somehow made it into the CSS collection during grouping.', get_class($asset))); + } + + return $k; + } + + /** + * {@inheritdoc} + */ + public function groupAndSort(AssetCollectionInterface $collection) { + // We need to define the optimum minimal group set, given metadata + // boundaries across which aggregates cannot be safely made. + $optimal = array(); + + // Also create an SplObjectStorage to act as a lookup table on an asset to + // its group, if any. + // TODO try and find an elegant way to pass this out so we don't have to calculate keys twice + $optimal_lookup = new \SplObjectStorage(); + + // Finally, create a specialized directed adjacency list that will capture + // all ordering information. + $graph = new AssetGraph(); + + foreach ($collection->getCss() as $asset) { + $graph->addVertex($asset); + + $k = self::getGroupingKey($asset); + + if ($k === FALSE) { + // Record no optimality information for ungroupable assets; they will + // be visited normally and rearranged as needed. + continue; + } + + if (!isset($optimal[$k])) { + // Create an SplObjectStorage to represent each set of assets that would + // optimally be grouped together. + $optimal[$k] = new \SplObjectStorage(); + } + $optimal[$k]->attach($asset, $k); + $optimal_lookup->attach($asset, $optimal[$k]); + } + + // First, transpose the graph in order to get an appropriate answer + $transpose = $graph->transpose(); + + // Create a queue of start vertices to prime the traversal. + $queue = $this->createSourceQueue($graph, $transpose); + + // Now, create the visitor and walk the graph to get an optimal TSL. + $visitor = new OptimallyGroupedTSLVisitor($optimal, $optimal_lookup); + DepthFirst::traverse($transpose, $visitor, $queue); + + return $visitor->getTSL(); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php new file mode 100644 index 0000000..f9b8728 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php @@ -0,0 +1,127 @@ +default = array_replace_recursive($this->default, $default); + } + + /** + * {@inheritdoc} + */ + abstract public function getType(); + + public function all() { + return array_replace_recursive($this->default, $this->explicit); + } + + /** + * {@inheritdoc} + */ + public function keys() { + return array_keys($this->all()); + } + + /** + * {@inheritdoc} + */ + public function has($key) { + return array_key_exists($key, $this->explicit) || + array_key_exists($key, $this->default); + } + + /** + * {@inheritdoc} + */ + public function set($key, $value) { + $this->explicit[$key] = $value; + } + + /** + * {@inheritdoc} + */ + public function revert($key) { + unset($this->explicit[$key]); + } + + /** + * {@inheritdoc} + */ + public function isDefault($key) { + return !array_key_exists($key, $this->explicit) && + array_key_exists($key, $this->default); + } + + /** + * {@inheritdoc} + */ + public function add(array $values = array()) { + $this->explicit = array_replace_recursive($this->explicit, $values); + } + + /** + * {@inheritdoc} + */ + public function replace(array $values = array()) { + $this->explicit = $values; + } + + /** + * {@inheritdoc} + */ + public function get($key) { + if (array_key_exists($key, $this->explicit)) { + return $this->explicit[$key]; + } + + if (array_key_exists($key, $this->default)) { + return $this->default[$key]; + } + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + return new \ArrayIterator($this->all()); + } + + /** + * {@inheritdoc} + */ + public function count() { + return count($this->all()); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php new file mode 100644 index 0000000..0b7e2e9 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php @@ -0,0 +1,104 @@ + CSS_AGGREGATE_DEFAULT, // TODO Just removing this would be *awesome*. + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + ); + + public function __construct(array $default = array()) { + $this->default = array_replace_recursive($this->default, $default); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return 'css'; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/DefaultAssetMetadataFactory.php b/core/lib/Drupal/Core/Asset/Metadata/DefaultAssetMetadataFactory.php new file mode 100644 index 0000000..35e584a --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/DefaultAssetMetadataFactory.php @@ -0,0 +1,44 @@ + FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + )); + } + + /** + * {@inheritdoc} + */ + public function createJsMetadata() { + return new JsMetadataBag(array( + 'every_page' => FALSE, + 'scope' => 'footer', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + )); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php new file mode 100644 index 0000000..39927c0 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php @@ -0,0 +1,37 @@ + JS_DEFAULT, + 'every_page' => FALSE, + 'scope' => 'header', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + ); + + public function __construct(array $default = array()) { + $this->default = array_replace_recursive($this->default, $default); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return 'js'; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php b/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php new file mode 100644 index 0000000..df6490e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php @@ -0,0 +1,27 @@ +tsl = array(); + $this->groups = $groups; + $this->vertexMap = $vertex_map; + } + + /** + * {@inheritdoc} + */ + public function onInitializeVertex($vertex, $source, \SplQueue $queue) {} + + /** + * {@inheritdoc} + */ + public function onBackEdge($vertex, \Closure $visit) { + // TODO: Implement onBackEdge() method. + } + + /** + * {@inheritdoc} + */ + public function onStartVertex($vertex, \Closure $visit) { + $this->active->attach($vertex); + + // If there's a record in the vertex map, it means this vertex has an + // optimal group. Remove it from that group, as it being here means it's + // been visited. + if ($this->vertexMap->contains($vertex)) { + $this->vertexMap[$vertex]->detach($vertex); + } + } + + /** + * {@inheritdoc} + */ + public function onExamineEdge($from, $to, \Closure $visit) {} + + /** + * Here be the unicorns. + * + * Once the depth-first traversal is done for a vertex, rather than + * simply pushing it onto the TSL and moving on (as in a basic depth-first + * traversal), if the finished vertex is a member of an optimality group, then + * visit all other (unvisited) members of that optimality group. + * + * This ensures the final TSL has the tightest possible adherence to the + * defined optimal groupings while still respecting the DAG. + * + */ + public function onFinishVertex($vertex, \Closure $visit) { + // TODO this still isn't quite optimal; it can split groups unnecessarily. tweak a little more. + // TODO explore risk of hitting the 100 call stack limit + if ($this->vertexMap->contains($vertex)) { + foreach ($this->vertexMap[$vertex] as $vertex) { + $visit($vertex); + } + } + $this->tsl[] = $vertex; + } + + /** + * Returns the TSL produced by a depth-first traversal. + * + * @return array + * A topologically sorted list of vertices. + */ + public function getTSL() { + return $this->tsl; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/RelativePositionInterface.php b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php new file mode 100644 index 0000000..66293d8 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php @@ -0,0 +1,74 @@ +id= empty($content) ? Crypt::randomStringHashed(32) : hash('sha256', $content); + $this->setContent($content); + $this->lastModified = REQUEST_TIME; // TODO this is terrible + + parent::__construct($metadata, $filters); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->id; + } + + public function setLastModified($last_modified) { + $this->lastModified = $last_modified; + } + + public function getLastModified() { + return $this->lastModified; + } + + public function load(FilterInterface $additionalFilter = NULL) { + $this->doLoad($this->getContent(), $additionalFilter); + } +} diff --git a/core/modules/block/lib/Drupal/block/BlockBase.php b/core/modules/block/lib/Drupal/block/BlockBase.php index 579e841..1768991 100644 --- a/core/modules/block/lib/Drupal/block/BlockBase.php +++ b/core/modules/block/lib/Drupal/block/BlockBase.php @@ -11,6 +11,7 @@ use Drupal\block\BlockInterface; use Drupal\Component\Utility\Unicode; use Drupal\Core\Language\Language; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines a base block implementation that most blocks plugins will extend. @@ -181,5 +182,10 @@ public function getMachineNameSuggestion() { return $transliterated; } - + /** + * {@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 b5433e6..d625616 100644 --- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php +++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php @@ -10,6 +10,7 @@ use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Core\Plugin\PluginFormInterface; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines the required interface for all block plugins. @@ -122,4 +123,12 @@ public function blockSubmit($form, &$form_state); */ public function getMachineNameSuggestion(); + /** + * Declares the assets required by this block to a collector. + * + * @param \Drupal\Core\Asset\Factory\AssetCollector $collector + * + * @return void + */ + public function declareAssets(AssetCollector $collector); } diff --git a/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php new file mode 100644 index 0000000..8fe3f8d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php @@ -0,0 +1,156 @@ + 'Asset aggregate tests', + 'description' => 'Unit tests on BaseAggregateAsset', + 'group' => 'Asset', + ); + } + + /** + * Generates a simple BaseAggregateAsset mock. + * + * @param array $defaults + * Defaults to inject into the aggregate's metadata bag. + * + * @return BaseAggregateAsset + */ + public function getAggregate($defaults = array()) { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $defaults); + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); + } + + public function testId() { + $aggregate = $this->getAggregate(); + + $asset1 = $this->createMockFileAsset('css'); + $asset2 = $this->createMockFileAsset('css'); + $aggregate->add($asset1); + $aggregate->add($asset2); + + $this->assertEquals(hash('sha256', $asset1->id() . $asset2->id()), $aggregate->id()); + } + + public function testGetAssetType() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $mockmeta->expects($this->once()) + ->method('getType') + ->will($this->returnValue('unicorns')); + $aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); + + $this->assertEquals('unicorns', $aggregate->getAssetType()); + } + + public function testGetMetadata() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); + + $this->assertSame($mockmeta, $aggregate->getMetadata()); + } + + public function testAdd() { + $aggregate = $this->getAggregate(); + + $metamock = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $asset = $this->getMock('\\Drupal\\Core\\Asset\\FileAsset', array(), array($metamock, 'foo')); + $asset->expects($this->once()) + ->method('id') + ->will($this->returnValue('foo')); + + $aggregate->add($asset); + + $this->assertContains($asset, $aggregate); + } + + public function testContains() { + $aggregate = $this->getAggregate(); + $css = $this->createMockFileAsset('css'); + + $aggregate->add($css); + $this->assertTrue($aggregate->contains($css)); + } + + /** + * @expectedException OutOfBoundsException + */ + public function testGetById() { + $aggregate = $this->getAggregate(); + + $asset = $this->createMockFileAsset('css'); + $aggregate->add($asset); + $this->assertSame($asset, $aggregate->getById($asset->id())); + + // Nonexistent asset + $this->assertFalse($aggregate->getById('bar')); + + // Nonexistent asset, non-graceful + $aggregate->getById('bar', FALSE); + } + + public function testIsPreprocessable() { + $this->assertTrue($this->getAggregate()->isPreprocessable()); + } + + public function testAll() { + $aggregate = $this->getAggregate(); + + $asset1 = $this->createMockFileAsset('css'); + $asset2 = $this->createMockFileAsset('css'); + $aggregate->add($asset1); + $aggregate->add($asset2); + + $output = array( + $asset1->id() => $asset1, + $asset2->id() => $asset2, + ); + + $this->assertEquals($output, $aggregate->all()); + } + + public function testIsEmpty() { + $this->assertTrue($this->getAggregate()->isEmpty()); + } + + public function testRemove() { + $this->fail(); + } + + public function testRemoveLeaf() { + $this->fail(); + } + + public function testReplace() { + $this->fail(); + } + + public function testReplaceLeaf() { + $this->fail(); + } + + public function testLoad() { + $this->fail(); + } + + public function testDump() { + $this->fail(); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php new file mode 100644 index 0000000..b83698d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php @@ -0,0 +1,258 @@ + 'Asset graph test', + 'description' => 'Tests that custom additions in the asset graph work correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + $this->graph = new AssetGraph(); + } + + /** + * Generates a simple mock asset object. + * + * @param string $id + * An id to give the asset; it will returned from the mocked + * AssetInterface::id() method. + * + * @return \PHPUnit_Framework_MockObject_MockObject + * A mock of a BaseAsset object. + */ + public function createBasicAssetMock($id = 'foo') { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $mock = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $mock->expects($this->any()) + ->method('id') + ->will($this->returnValue($id)); + + $mock->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array())); + + $mock->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array())); + + return $mock; + } + + public function doCheckVertexCount($count, AssetGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->graph : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertCount($count, $found); + } + + public function doCheckVerticesEqual($vertices, AssetGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->graph : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertEquals($vertices, $found); + } + + public function testAddSingleVertex() { + $mock = $this->createBasicAssetMock(); + + $mock->expects($this->exactly(2)) + ->method('id') + ->will($this->returnValue('foo')); + + $this->graph->addVertex($mock); + + $this->doCheckVerticesEqual(array($mock)); + } + + /** + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddInvalidVertexType() { + $this->graph->addVertex(new \stdClass()); + } + + /** + * @expectedException \LogicException + */ + public function testExceptionOnRemoval() { + $mock = $this->createBasicAssetMock(); + $this->graph->addVertex($mock); + $this->graph->removeVertex($mock); + } + + public function testAddUnconnectedVertices() { + $foo = $this->createBasicAssetMock('foo'); + $bar = $this->createBasicAssetMock('bar'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + + $this->doCheckVerticesEqual(array($foo, $bar)); + } + + /** + * Tests that edges are automatically created correctly when assets have + * sequencing information. + */ + public function testAddConnectedVertices() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $foo = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $foo->expects($this->exactly(3)) + ->method('id') + ->will($this->returnValue('foo')); + + $foo->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array('bar'))); + + $foo->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('baz'))); + + $bar = $this->createBasicAssetMock('bar'); + $baz = $this->createBasicAssetMock('baz'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + $this->graph->addVertex($baz); + + $this->doCheckVerticesEqual(array($foo, $bar, $baz)); + + $lister = function($vertex) use (&$out) { + $out[] = $vertex; + }; + + $out = array(); + $this->graph->eachAdjacent($foo, $lister); + $this->assertEquals(array($bar), $out); + + $out = array(); + $this->graph->eachAdjacent($baz, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $this->graph->eachAdjacent($bar, $lister); + $this->assertEmpty($out); + + // Now add another vertex with sequencing info that targets already-inserted + // vertices. + + $qux = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $qux->expects($this->exactly(2)) + ->method('id') + ->will($this->returnValue('qux')); + + // Do this one with the foo vertex itself, not its string id. + $qux->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array($foo))); + + $qux->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('bar', 'baz'))); + + $this->graph->addVertex($qux); + + $this->doCheckVerticesEqual(array($foo, $bar, $baz, $qux)); + + $out = array(); + $this->graph->eachAdjacent($qux, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $this->graph->eachAdjacent($bar, $lister); + $this->assertEquals(array($qux), $out); + + $out = array(); + $this->graph->eachAdjacent($baz, $lister); + $this->assertEquals(array($foo, $qux), $out); + } + + public function testTranspose() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $foo = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $foo->expects($this->exactly(3)) + ->method('id') + ->will($this->returnValue('foo')); + + $foo->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array('bar'))); + + $foo->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('baz'))); + + $bar = $this->createBasicAssetMock('bar'); + $baz = $this->createBasicAssetMock('baz'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + $this->graph->addVertex($baz); + + $transpose = $this->graph->transpose(); + $this->doCheckVerticesEqual(array($foo, $bar, $baz), $transpose); + + // Verify that the transpose has a fully inverted edge set. + $lister = function($vertex) use (&$out) { + $out[] = $vertex; + }; + + $out = array(); + $transpose->eachAdjacent($bar, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $transpose->eachAdjacent($foo, $lister); + $this->assertEquals(array($baz), $out); + + $out = array(); + $transpose->eachAdjacent($baz, $lister); + $this->assertEmpty($out); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php new file mode 100644 index 0000000..ca85afb --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php @@ -0,0 +1,40 @@ +getMock('Drupal\\Core\\Asset\\FileAsset', array(), array(), '', FALSE); + $asset->expects($this->any()) + ->method('getAssetType') + ->will($this->returnValue($type)); + + $asset->expects($this->any()) + ->method('id') + ->will($this->returnValue($this->randomName())); + + return $asset; + } +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php new file mode 100644 index 0000000..69a2bdb --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php @@ -0,0 +1,58 @@ + 'Assetic adapter asset test', + 'description' => 'Tests that certain Assetic methods throw known exceptions in a Drupal context', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->mock = $this->getMockForAbstractClass('Drupal\\Core\\Asset\\AsseticAdapterAsset'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testGetVars() { + $this->mock->getVars(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testSetValues() { + $this->mock->setValues(array()); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testGetValues() { + $this->mock->getValues(); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php new file mode 100644 index 0000000..1639f1a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php @@ -0,0 +1,138 @@ + 'Base Asset tests', + 'description' => 'Unit tests for Drupal\'s BaseAsset.', + 'group' => 'Asset', + ); + } + + /** + * Creates a BaseAsset for testing purposes. + * + * @param array $defaults + * + * @return BaseAsset + */ + public function createBaseAsset($defaults = array()) { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $defaults); + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + } + + public function testGetMetadata() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + + $this->assertSame($mockmeta, $asset->getMetadata()); + } + + public function testGetAssetType() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $mockmeta->expects($this->once()) + ->method('getType') + ->will($this->returnValue('css')); + $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + + $this->assertEquals('css', $asset->getAssetType()); + } + + public function testIsPreprocessable() { + $mockmeta = $this->getMock('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $mockmeta->expects($this->once()) + ->method('get') + ->with('preprocess') + ->will($this->returnValue(TRUE)); + $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + + $this->assertTrue($asset->isPreprocessable()); + } + + /** + * Tests all dependency-related methods. + */ + public function testDependencies() { + $asset = $this->createBaseAsset(); + + $asset->addDependency('foo', 'bar'); + $this->assertEquals(array(array('foo', 'bar')), $asset->getDependencyInfo()); + $this->assertTrue($asset->hasDependencies()); + + $asset->clearDependencies(); + $this->assertEmpty($asset->getDependencyInfo()); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $asset->addDependency($val, $val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testSuccessors() { + $asset = $this->createBaseAsset(); + $dep = $this->createBaseAsset(); + + $asset->before('foo'); + $asset->before($dep); + + $this->assertEquals(array('foo', $dep), $asset->getSuccessors()); + + $asset->clearSuccessors(); + $this->assertEmpty($asset->getSuccessors()); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $asset->before($val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testPredecessors() { + $asset = $this->createBaseAsset(); + $dep = $this->createBaseAsset(); + + $asset->after('foo'); + $asset->after($dep); + $this->assertEquals(array('foo', $dep), $asset->getPredecessors()); + + $asset->clearPredecessors(); + $this->assertEmpty($asset->getPredecessors()); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $asset->after($val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testClone() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + + $clone = clone $asset; + $this->assertNotSame($mockmeta, $clone->getMetadata()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php new file mode 100644 index 0000000..14952c9 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php @@ -0,0 +1,252 @@ + 'Asset collection tests', + 'description' => 'Unit tests on AssetCollection', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->collection = new AssetCollection(); + } + + public function testAdd() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $this->assertContains($css, $this->collection); + $this->assertContains($js, $this->collection); + } + + public function testContains() { + $css = $this->createMockFileAsset('css'); + $this->collection->add($css); + $this->assertTrue($this->collection->contains($css)); + } + + public function testGetCss() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $css_result = array(); + foreach ($this->collection->getCss() as $asset) { + $css_result[] = $asset; + } + + $this->assertEquals(array($css), $css_result); + } + + public function testGetJs() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $js_result = array(); + foreach ($this->collection->getJs() as $asset) { + $js_result[] = $asset; + } + + $this->assertEquals(array($js), $js_result); + } + + public function testAll() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $this->assertEquals(array($css->id() => $css, $js->id() => $js), $this->collection->all()); + } + + public function testRemoveByAsset() { + $stub = $this->createMockFileAsset('css'); + + $this->collection->add($stub); + $this->collection->remove($stub); + + $this->assertNotContains($stub, $this->collection); + } + + public function testRemoveById() { + $stub = $this->createMockFileAsset('css'); + + $this->collection->add($stub); + $this->collection->remove($stub->id()); + + $this->assertNotContains($stub, $this->collection); + } + + /** + * @expectedException OutOfBoundsException + */ + public function testRemoveNonexistentId() { + $this->assertFalse($this->collection->remove('foo')); + $this->collection->remove('foo', FALSE); + } + + /** + * @expectedException OutOfBoundsException + */ + public function testRemoveNonexistentAsset() { + $stub = $this->createMockFileAsset('css'); + $this->assertFalse($this->collection->remove($stub)); + $this->collection->remove($stub, FALSE); + } + + public function testRemoveInvalidType() { + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + try { + foreach ($invalid as $val) { + $this->collection->remove($val); + $this->fail('AssetCollection::remove() did not throw exception on invalid argument type.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testMergeCollection() { + $coll2 = new AssetCollection(); + $stub1 = $this->createMockFileAsset('css'); + $stub2 = $this->createMockFileAsset('js'); + + $coll2->add($stub1); + $this->collection->mergeCollection($coll2); + + $this->assertContains($stub1, $this->collection); + $this->assertTrue($coll2->isFrozen()); + + $coll3 = new AssetCollection(); + $coll3->add($stub1); + $coll3->add($stub2); + // Ensure no duplicates, and don't freeze merged bag + $this->collection->mergeCollection($coll3, FALSE); + + $contained = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + ); + $this->assertEquals($contained, $this->collection->all()); + $this->assertFalse($coll3->isFrozen()); + } + + /** + * Tests that all methods should be disabled by freezing the collection + * correctly trigger an exception. + */ + public function testExceptionOnWriteWhenFrozen() { + $stub = $this->createMockFileAsset('css'); + $write_protected = array( + 'add' => $stub, + 'remove' => $stub, + 'mergeCollection' => $this->getMock('\\Drupal\\Core\\Asset\\Collection\\AssetCollection'), + ); + + $this->collection->freeze(); + foreach ($write_protected as $method => $arg) { + try { + $this->collection->$method($arg); + $this->fail('Was able to run writable method on frozen AssetCollection'); + } + catch (\LogicException $e) {} + } + } + + /** + * @expectedException OutOfBoundsException + */ + public function testGetById() { + $metamock = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + + $asset = $this->getMock('\\Drupal\\Core\\Asset\\FileAsset', array(), array($metamock, 'foo')); + $asset->expects($this->once()) + ->method('id') + ->will($this->returnValue('foo')); + + $this->collection->add($asset); + $this->assertSame($asset, $this->collection->getById('foo')); + + // Nonexistent asset + $this->assertFalse($this->collection->getById('bar')); + + // Nonexistent asset, non-graceful + $this->collection->getById('bar', FALSE); + } + + public function testIsEmpty() { + $this->assertTrue($this->collection->isEmpty()); + } + + public function testSort() { + $stub1 = $this->createMockFileAsset('css'); + $stub2 = $this->createMockFileAsset('js'); + $stub3 = $this->createMockFileAsset('css'); + + $this->collection->add($stub1); + $this->collection->add($stub2); + $this->collection->add($stub3); + + $assets = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + $stub3->id() => $stub3, + ); + + $dummysort = function ($a, $b) { + return strnatcasecmp($a, $b); + }; + + $this->collection->sort($dummysort); + uksort($assets, $dummysort); + $this->assertEquals($assets, $this->collection->all()); + } + + public function testKsort() { + $stub1 = $this->createMockFileAsset('css'); + $stub2 = $this->createMockFileAsset('js'); + $stub3 = $this->createMockFileAsset('css'); + + $this->collection->add($stub1); + $this->collection->add($stub2); + $this->collection->add($stub3); + + $assets = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + $stub3->id() => $stub3, + ); + + $this->collection->ksort(); + ksort($assets); + $this->assertEquals($assets, $this->collection->all()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php new file mode 100644 index 0000000..5608951 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/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->getDependencyInfo(), $library->getDependencyInfo(), 'Dependencies information passed correctly through the constructor.'); + } + + public function testAddDependency() { + $library = $this->getLibraryFixture(); + $library->addDependency('baz', 'bing'); + $this->assertEquals($library->getDependencyInfo(), array(array('foo', 'bar'), array('baz', 'bing')), 'Dependencies added to library successfully.'); + } + + public function testClearDependencies() { + $library = $this->getLibraryFixture(); + $library->clearDependencies(); + $this->assertEmpty($library->getDependencyInfo(), '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/Factory/AssetCollectorTest.php b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php new file mode 100644 index 0000000..4933315 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php @@ -0,0 +1,275 @@ + '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() { + $asset = $this->collector->create('css', 'file', 'foo', array('group' => CSS_AGGREGATE_THEME)); + $meta = $asset->getMetadata(); + $this->assertEquals(CSS_AGGREGATE_THEME, $meta->get('group'), 'Collector injected user-passed parameters into the created asset.'); + $this->assertFalse($meta->isDefault('group')); + } + + public function testDefaultPropagation() { + // Test that defaults are correctly applied by the factory. + $meta = new CssMetadataBag(array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME)); + $factory = $this->getMock('Drupal\\Core\\Asset\\Metadata\\DefaultAssetMetadataFactory'); + $factory->expects($this->once()) + ->method('createCssMetadata') + ->will($this->returnValue($meta)); + + $this->collector->setMetadataFactory($factory); + $css1 = $this->collector->create('css', 'file', 'foo'); + + $asset_meta = $css1->getMetadata(); + $this->assertTrue($asset_meta->get('every_page')); + $this->assertEquals(CSS_AGGREGATE_THEME, $asset_meta->get('group')); + } + + /** + * @expectedException \RuntimeException + */ + public function testExceptionOnAddingAssetWithoutCollectionPresent() { + $asset = $this->collector->create('css', 'string', 'foo'); + $this->collector->add($asset); + } + + /** + * TODO separate test for an explicit add() call. + */ + public function testAssetsImplicitlyArriveInInjectedCollection() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + + $asset = $this->collector->create('css', 'file', 'bar'); + $this->assertContains($asset, $collection->getCss(), 'Created asset was implicitly added to collection.'); + } + + public function testAddAssetExplicitly() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + + $mock = $this->createMockFileAsset('css'); + $this->collector->add($mock); + + $this->assertContains($mock, $collection); + } + + public function testSetCollection() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + $this->assertTrue($this->collector->hasCollection()); + } + + public function testClearCollection() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + $this->collector->clearCollection(); + $this->assertFalse($this->collector->hasCollection()); + } + + 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 \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testUnlockFailsWithoutCorrectSecret() { + $this->collector->lock('foo'); + $this->collector->unlock('bar'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testUnlockFailsIfNotLocked() { + $this->collector->unlock('foo'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockFailsIfLocked() { + $this->collector->lock('foo'); + $this->collector->lock('error'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsSettingDefaults() { + $this->collector->lock($this); + $this->collector->setMetadataFactory($this->getMock('Drupal\\Core\\Asset\\Metadata\\DefaultAssetMetadataFactory')); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsRestoringDefaults() { + $this->collector->lock($this); + $this->collector->restoreDefaults(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsClearingCollection() { + $this->collector->lock($this); + $this->collector->clearCollection(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsSettingCollection() { + $this->collector->lock($this); + $this->collector->setCollection(new AssetCollection()); + } + + public function testChangeAndRestoreDefaults() { + // TODO this test is now in fuzzy territory - kinda more the factory's responsibility + $default_factory = new DefaultAssetMetadataFactory(); + // Ensure we're in a good state first + $this->assertEquals($default_factory->createCssMetadata(), $this->collector->getMetadataDefaults('css')); + + $changed_css = new CssMetadataBag(array('foo' => 'bar', 'every_page' => TRUE)); + $factory = $this->getMock('Drupal\\Core\\Asset\\Metadata\\DefaultAssetMetadataFactory'); + $factory->expects($this->exactly(2)) + ->method('createCssMetadata') + ->will($this->returnValue(clone $changed_css)); + + $this->collector->setMetadataFactory($factory); + + $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css')); + // TODO this is totally cheating, only passes because we clone earlier. but it should be a guarantee of the interface...how to test this? + $this->assertNotSame($changed_css, $this->collector->getMetadataDefaults('css'), 'New metadata instance is created on retrieval from collector.'); + + $this->collector->restoreDefaults(); + $this->assertEquals($default_factory->createCssMetadata(), $this->collector->getMetadataDefaults('css')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetNonexistentDefault() { + $this->collector->getMetadataDefaults('foo'); + } + + + public function testCreateCssFileAsset() { + $css_file = $this->collector->create('css', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\FileAsset', $css_file); + $this->assertEquals('css', $css_file->getAssetType()); + } + + public function testCreateStylesheetExternalAsset() { + $css_external = $this->collector->create('css', 'external', 'http://foo.bar/path/to/asset.css'); + $this->assertInstanceOf('\Drupal\Core\Asset\ExternalAsset', $css_external); + $this->assertEquals('css', $css_external->getAssetType()); + } + + public function testCreateStylesheetStringAsset() { + $css_string = $this->collector->create('css', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StringAsset', $css_string); + $this->assertEquals('css', $css_string->getAssetType()); + } + + public function testCreateJavascriptFileAsset() { + $js_file = $this->collector->create('js', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\FileAsset', $js_file); + $this->assertEquals('js', $js_file->getAssetType()); + } + + public function testCreateJavascriptExternalAsset() { + $js_external = $this->collector->create('js', 'external', 'http://foo.bar/path/to/asset.js'); + $this->assertInstanceOf('\Drupal\Core\Asset\ExternalAsset', $js_external); + $this->assertEquals('js', $js_external->getAssetType()); + } + + public function testCreateJavascriptStringAsset() { + $js_string = $this->collector->create('js', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StringAsset', $js_string); + $this->assertEquals('js', $js_string->getAssetType()); + } + + public function testLastCssAutoAfter() { + $css1 = $this->collector->create('css', 'file', 'foo.css'); + $css2 = $this->collector->create('css', 'file', 'foo2.css'); + $this->assertEquals(array($css1), $css2->getPredecessors()); + + $this->collector->clearLastCss(); + $css3 = $this->collector->create('css', 'file', 'foo3.css'); + $this->assertEmpty($css3->getPredecessors()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testExceptionOnInvalidSourceType() { + $this->collector->create('foo', 'bar', 'baz'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testExceptionOnInvalidAssetType() { + $this->collector->create('css', 'bar', 'qux'); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php new file mode 100644 index 0000000..00876f8 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php @@ -0,0 +1,95 @@ + 'Asset Metadata bag test', + 'description' => 'Tests various methods of AssetMetadatabag', + 'group' => 'Asset', + ); + } + + public function createBag($args = array(array('foo' => 'bar', 'baz' => 'qux'))) { + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $args); + } + + /** + * A unified test for all operations that rely on calling get() in order to + * verify their correctness. + */ + public function testGetValueOperations() { + // First, ensure that constructor-injected defaults are correctly tracked. + $mock = $this->createBag(); + $this->assertEquals('bar', $mock->get('foo')); + // Ensure that constructor-injected defaults are correctly reported as such. + $this->assertTrue($mock->isDefault('foo')); + + // Set an explicit value, and ensure that it comes back out correctly. + $mock->set('bing', 'bang'); + $this->assertEquals('bang', $mock->get('bing')); + $this->assertFalse($mock->isDefault('bing')); + + // Set an explicit value that overrides a default, this time. + $mock->set('foo', 'kablow'); + $this->assertEquals('kablow', $mock->get('foo')); + $this->assertFalse($mock->isDefault('foo')); + + // Revert the set value, and ensure the old default comes through. + $mock->revert('foo'); + $this->assertEquals('bar', $mock->get('foo')); + $this->assertTrue($mock->isDefault('foo')); + + // Add value via add(), now + $mock->add(array('llama' => 'a pink one')); + $this->assertEquals('a pink one', $mock->get('llama')); + $this->assertFalse($mock->isDefault('llama')); + + // Finally, check that getting an unknown key returns nothing + $this->assertNull($mock->get('nonexistent')); + } + + public function testAll() { + $this->assertEquals(array('foo' => 'bar', 'baz' => 'qux'), $this->createBag()->all()); + } + + public function testKeys() { + $this->assertEquals(array('foo', 'baz'), $this->createBag()->keys()); + } + + public function testHas() { + $this->assertTrue($this->createBag()->has('foo')); + } + + public function testIteration() { + $found = array(); + foreach ($this->createBag() as $val) { + $found[] = $val; + } + + $this->assertEquals(array('bar', 'qux'), $found); + } + + public function testCount() { + $this->assertCount(2, $this->createBag()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php new file mode 100644 index 0000000..08fbb83 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php @@ -0,0 +1,32 @@ + 'CSS Metadata bag test', + 'description' => 'Tests various methods of CssMetadatabag', + 'group' => 'Asset', + ); + } + + public function testGetType() { + $bag = new CssMetadataBag(); + $this->assertEquals('css', $bag->getType()); + } +} + diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php new file mode 100644 index 0000000..8210603 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php @@ -0,0 +1,58 @@ + 'DefaultAssetMetadataFactory test', + 'description' => 'Unit tests for DefaultAssetMetadataFactory', + 'group' => 'Asset', + ); + } + + public function testCreateCssMetadata() { + $factory = new DefaultAssetMetadataFactory(); + $bag = new CssMetadataBag(array( + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + )); + + $this->assertEquals($bag, $factory->createCssMetadata()); + } + + public function testCreateJsMetadata() { + $factory = new DefaultAssetMetadataFactory(); + $bag = new JsMetadataBag(array( + 'every_page' => FALSE, + 'scope' => 'footer', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + )); + + $this->assertEquals($bag, $factory->createJsMetadata()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php new file mode 100644 index 0000000..7cc5600 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php @@ -0,0 +1,32 @@ + 'JS Metadata bag test', + 'description' => 'Tests various methods of JsMetadatabag', + 'group' => 'Asset', + ); + } + + public function testGetType() { + $bag = new JsMetadataBag(); + $this->assertEquals('js', $bag->getType()); + } +} + diff --git a/core/vendor/autoload.php b/core/vendor/autoload.php index aac8b62..db8c291 100644 --- a/core/vendor/autoload.php +++ b/core/vendor/autoload.php @@ -4,4 +4,4 @@ require_once __DIR__ . '/composer' . '/autoload_real.php'; -return ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2::getLoader(); +return ComposerAutoloaderInitf333b4f71be112d333dd097f0ffff964::getLoader(); diff --git a/core/vendor/autoload.php b/core/vendor/autoload.php.orig similarity index 61% copy from core/vendor/autoload.php copy to core/vendor/autoload.php.orig index aac8b62..01b379d 100644 --- a/core/vendor/autoload.php +++ b/core/vendor/autoload.php.orig @@ -4,4 +4,8 @@ require_once __DIR__ . '/composer' . '/autoload_real.php'; +<<<<<<< HEAD +return ComposerAutoloaderInitd1cf02b92e6e91cd998c4d7a12700700::getLoader(); +======= return ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2::getLoader(); +>>>>>>> origin/8.x diff --git a/core/vendor/composer/autoload_real.php b/core/vendor/composer/autoload_real.php index b7dd370..ba8452a 100644 --- a/core/vendor/composer/autoload_real.php +++ b/core/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2 +class ComposerAutoloaderInitf333b4f71be112d333dd097f0ffff964 { private static $loader; @@ -19,9 +19,9 @@ public static function getLoader() return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInitf333b4f71be112d333dd097f0ffff964', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); - spl_autoload_unregister(array('ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInitf333b4f71be112d333dd097f0ffff964', 'loadClassLoader')); $vendorDir = dirname(__DIR__); $baseDir = dirname(dirname($vendorDir)); diff --git a/core/vendor/composer/autoload_real.php b/core/vendor/composer/autoload_real.php.orig similarity index 77% copy from core/vendor/composer/autoload_real.php copy to core/vendor/composer/autoload_real.php.orig index b7dd370..1f6a320 100644 --- a/core/vendor/composer/autoload_real.php +++ b/core/vendor/composer/autoload_real.php.orig @@ -2,7 +2,11 @@ // autoload_real.php @generated by Composer +<<<<<<< HEAD +class ComposerAutoloaderInitd1cf02b92e6e91cd998c4d7a12700700 +======= class ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2 +>>>>>>> origin/8.x { private static $loader; @@ -19,9 +23,15 @@ public static function getLoader() return self::$loader; } +<<<<<<< HEAD + spl_autoload_register(array('ComposerAutoloaderInitd1cf02b92e6e91cd998c4d7a12700700', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + spl_autoload_unregister(array('ComposerAutoloaderInitd1cf02b92e6e91cd998c4d7a12700700', 'loadClassLoader')); +======= spl_autoload_register(array('ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit7ffa68419492d19fe654de54c86ae5d2', 'loadClassLoader')); +>>>>>>> origin/8.x $vendorDir = dirname(__DIR__); $baseDir = dirname(dirname($vendorDir)); diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json index 8347a64..964eee0 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -147,79 +147,6 @@ ] }, { - "name": "kriswallsmith/assetic", - "version": "v1.1.1", - "version_normalized": "1.1.1.0", - "source": { - "type": "git", - "url": "https://github.com/kriswallsmith/assetic.git", - "reference": "v1.1.1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kriswallsmith/assetic/zipball/v1.1.1", - "reference": "v1.1.1", - "shasum": "" - }, - "require": { - "php": ">=5.3.1", - "symfony/process": ">=2.1,<3.0" - }, - "require-dev": { - "cssmin/cssmin": "*", - "joliclic/javascript-packer": "*", - "kamicane/packager": "*", - "leafo/lessphp": "*", - "leafo/scssphp": "*", - "leafo/scssphp-compass": "*", - "mrclay/minify": "*", - "phpunit/phpunit": ">=3.7,<4.0", - "ptachoire/cssembed": "*", - "twig/twig": ">=1.6,<2.0" - }, - "suggest": { - "leafo/lessphp": "Assetic provides the integration with the lessphp LESS compiler", - "leafo/scssphp": "Assetic provides the integration with the scssphp SCSS compiler", - "leafo/scssphp-compass": "Assetic provides the integration with the SCSS compass plugin", - "ptachoire/cssembed": "Assetic provides the integration with phpcssembed to embed data uris", - "twig/twig": "Assetic provides the integration with the Twig templating engine" - }, - "time": "2013-06-01 22:13:43", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-0": { - "Assetic": "src/" - }, - "files": [ - "src/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kris Wallsmith", - "email": "kris.wallsmith@gmail.com", - "homepage": "http://kriswallsmith.net/" - } - ], - "description": "Asset Management for PHP", - "homepage": "https://github.com/kriswallsmith/assetic", - "keywords": [ - "assets", - "compression", - "minification" - ] - }, - { "name": "symfony-cmf/routing", "version": "1.1.0-beta1", "version_normalized": "1.1.0.0-beta1", @@ -2017,25 +1944,71 @@ "homepage": "http://symfony.com" }, { + "name": "sdboyer/gliph", + "version": "0.1.4", + "version_normalized": "0.1.4.0", + "source": { + "type": "git", + "url": "https://github.com/sdboyer/gliph.git", + "reference": "aad932ef7d808105341cc9a36538e9fe2cb5ee82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sdboyer/gliph/zipball/aad932ef7d808105341cc9a36538e9fe2cb5ee82", + "reference": "aad932ef7d808105341cc9a36538e9fe2cb5ee82", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "time": "2013-09-27 01:15:21", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Gliph": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sam Boyer", + "email": "tech@samboyer.org" + } + ], + "description": "A graph library for PHP.", + "homepage": "http://github.com/sdboyer/gliph", + "keywords": [ + "gliph", + "graph", + "library", + "php", + "spl" + ] + }, + { "name": "symfony/process", - "version": "v2.3.4", - "version_normalized": "2.3.4.0", + "version": "v2.3.6", + "version_normalized": "2.3.6.0", "target-dir": "Symfony/Component/Process", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", - "reference": "1e91553e1cedd0b8fb1da6ea4f89b02e21713d5b" + "reference": "81191e354fe9dad0451036ccf0fdf283649d3f1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/1e91553e1cedd0b8fb1da6ea4f89b02e21713d5b", - "reference": "1e91553e1cedd0b8fb1da6ea4f89b02e21713d5b", + "url": "https://api.github.com/repos/symfony/Process/zipball/81191e354fe9dad0451036ccf0fdf283649d3f1e", + "reference": "81191e354fe9dad0451036ccf0fdf283649d3f1e", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "time": "2013-08-22 06:42:25", + "time": "2013-10-09 21:17:57", "type": "library", "extra": { "branch-alias": { @@ -2066,30 +2039,58 @@ "homepage": "http://symfony.com" }, { - "name": "sdboyer/gliph", - "version": "0.1.4", - "version_normalized": "0.1.4.0", + "name": "kriswallsmith/assetic", + "version": "v1.1.2", + "version_normalized": "1.1.2.0", "source": { "type": "git", - "url": "https://github.com/sdboyer/gliph.git", - "reference": "aad932ef7d808105341cc9a36538e9fe2cb5ee82" + "url": "https://github.com/kriswallsmith/assetic.git", + "reference": "735cffd3982c6e8cdebe292d5db39d077f65890f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sdboyer/gliph/zipball/aad932ef7d808105341cc9a36538e9fe2cb5ee82", - "reference": "aad932ef7d808105341cc9a36538e9fe2cb5ee82", + "url": "https://api.github.com/repos/kriswallsmith/assetic/zipball/735cffd3982c6e8cdebe292d5db39d077f65890f", + "reference": "735cffd3982c6e8cdebe292d5db39d077f65890f", "shasum": "" }, "require": { - "php": ">=5.3" + "php": ">=5.3.1", + "symfony/process": "~2.1" }, - "time": "2013-09-27 01:15:21", + "require-dev": { + "cssmin/cssmin": "*", + "joliclic/javascript-packer": "*", + "kamicane/packager": "*", + "leafo/lessphp": "*", + "leafo/scssphp": "*", + "leafo/scssphp-compass": "*", + "mrclay/minify": "*", + "phpunit/phpunit": "~3.7", + "ptachoire/cssembed": "*", + "twig/twig": "~1.6" + }, + "suggest": { + "leafo/lessphp": "Assetic provides the integration with the lessphp LESS compiler", + "leafo/scssphp": "Assetic provides the integration with the scssphp SCSS compiler", + "leafo/scssphp-compass": "Assetic provides the integration with the SCSS compass plugin", + "ptachoire/cssembed": "Assetic provides the integration with phpcssembed to embed data uris", + "twig/twig": "Assetic provides the integration with the Twig templating engine" + }, + "time": "2013-07-19 00:03:27", "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, "installation-source": "dist", "autoload": { "psr-0": { - "Gliph": "src/" - } + "Assetic": "src/" + }, + "files": [ + "src/functions.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2097,18 +2098,17 @@ ], "authors": [ { - "name": "Sam Boyer", - "email": "tech@samboyer.org" + "name": "Kris Wallsmith", + "email": "kris.wallsmith@gmail.com", + "homepage": "http://kriswallsmith.net/" } ], - "description": "A graph library for PHP.", - "homepage": "http://github.com/sdboyer/gliph", + "description": "Asset Management for PHP", + "homepage": "https://github.com/kriswallsmith/assetic", "keywords": [ - "gliph", - "graph", - "library", - "php", - "spl" + "assets", + "compression", + "minification" ] } ] diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json.orig similarity index 99% copy from core/vendor/composer/installed.json copy to core/vendor/composer/installed.json.orig index 8347a64..29e609d 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json.orig @@ -2067,6 +2067,19 @@ }, { "name": "sdboyer/gliph", +<<<<<<< HEAD + "version": "0.1.3", + "version_normalized": "0.1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sdboyer/gliph.git", + "reference": "f85ca76fde4913e3b6996691672998e646e0c642" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sdboyer/gliph/zipball/f85ca76fde4913e3b6996691672998e646e0c642", + "reference": "f85ca76fde4913e3b6996691672998e646e0c642", +======= "version": "0.1.4", "version_normalized": "0.1.4.0", "source": { @@ -2078,12 +2091,17 @@ "type": "zip", "url": "https://api.github.com/repos/sdboyer/gliph/zipball/aad932ef7d808105341cc9a36538e9fe2cb5ee82", "reference": "aad932ef7d808105341cc9a36538e9fe2cb5ee82", +>>>>>>> origin/8.x "shasum": "" }, "require": { "php": ">=5.3" }, +<<<<<<< HEAD + "time": "2013-09-22 03:30:09", +======= "time": "2013-09-27 01:15:21", +>>>>>>> origin/8.x "type": "library", "installation-source": "dist", "autoload": { diff --git a/core/vendor/kriswallsmith/assetic/CHANGELOG-1.1.md b/core/vendor/kriswallsmith/assetic/CHANGELOG-1.1.md index a5a8640..8bcf8bb 100644 --- a/core/vendor/kriswallsmith/assetic/CHANGELOG-1.1.md +++ b/core/vendor/kriswallsmith/assetic/CHANGELOG-1.1.md @@ -1,3 +1,11 @@ +1.1.2 (July 18, 2013) +------------------- + + * Fixed deep mtime on asset collections + * `CallablesFilter` now implements `DependencyExtractorInterface` + * Fixed detection of "partial" children in subfolders in `SassFilter` + * Restored `PathUtils` for BC + 1.1.1 (June 1, 2013) -------------------- diff --git a/core/vendor/kriswallsmith/assetic/src/Assetic/Asset/AssetCollection.php b/core/vendor/kriswallsmith/assetic/src/Assetic/Asset/AssetCollection.php index 6cfa3e8..d115d62 100644 --- a/core/vendor/kriswallsmith/assetic/src/Assetic/Asset/AssetCollection.php +++ b/core/vendor/kriswallsmith/assetic/src/Assetic/Asset/AssetCollection.php @@ -128,6 +128,7 @@ public function getFilters() public function clearFilters() { $this->filters->clear(); + $this->clones = new \SplObjectStorage(); } public function load(FilterInterface $additionalFilter = null) diff --git a/core/vendor/kriswallsmith/assetic/src/Assetic/Factory/LazyAssetManager.php b/core/vendor/kriswallsmith/assetic/src/Assetic/Factory/LazyAssetManager.php index 5f8fe3f..b47db2e 100644 --- a/core/vendor/kriswallsmith/assetic/src/Assetic/Factory/LazyAssetManager.php +++ b/core/vendor/kriswallsmith/assetic/src/Assetic/Factory/LazyAssetManager.php @@ -11,6 +11,7 @@ namespace Assetic\Factory; +use Assetic\Asset\AssetCollectionInterface; use Assetic\Asset\AssetInterface; use Assetic\AssetManager; use Assetic\Factory\Loader\FormulaLoaderInterface; @@ -206,34 +207,38 @@ public function isDebug() public function getLastModified(AssetInterface $asset) { - $mtime = $asset->getLastModified(); - if (!$filters = $asset->getFilters()) { - return $mtime; - } - - // prepare load path - $sourceRoot = $asset->getSourceRoot(); - $sourcePath = $asset->getSourcePath(); - $loadPath = $sourceRoot && $sourcePath ? dirname($sourceRoot.'/'.$sourcePath) : null; - - $prevFilters = array(); - foreach ($filters as $filter) { - $prevFilters[] = $filter; + $mtime = 0; + foreach ($asset instanceof AssetCollectionInterface ? $asset : array($asset) as $leaf) { + $mtime = max($mtime, $leaf->getLastModified()); - if (!$filter instanceof DependencyExtractorInterface) { + if (!$filters = $leaf->getFilters()) { continue; } - // extract children from asset after running all preceeding filters - $clone = clone $asset; - $clone->clearFilters(); - foreach (array_slice($prevFilters, 0, -1) as $prevFilter) { - $clone->ensureFilter($prevFilter); - } - $clone->load(); - - foreach ($filter->getChildren($this->factory, $clone->getContent(), $loadPath) as $child) { - $mtime = max($mtime, $this->getLastModified($child)); + // prepare load path + $sourceRoot = $leaf->getSourceRoot(); + $sourcePath = $leaf->getSourcePath(); + $loadPath = $sourceRoot && $sourcePath ? dirname($sourceRoot.'/'.$sourcePath) : null; + + $prevFilters = array(); + foreach ($filters as $filter) { + $prevFilters[] = $filter; + + if (!$filter instanceof DependencyExtractorInterface) { + continue; + } + + // extract children from leaf after running all preceeding filters + $clone = clone $leaf; + $clone->clearFilters(); + foreach (array_slice($prevFilters, 0, -1) as $prevFilter) { + $clone->ensureFilter($prevFilter); + } + $clone->load(); + + foreach ($filter->getChildren($this->factory, $clone->getContent(), $loadPath) as $child) { + $mtime = max($mtime, $this->getLastModified($child)); + } } } diff --git a/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/CallablesFilter.php b/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/CallablesFilter.php index 25413b0..fafa52e 100644 --- a/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/CallablesFilter.php +++ b/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/CallablesFilter.php @@ -12,25 +12,29 @@ namespace Assetic\Filter; use Assetic\Asset\AssetInterface; +use Assetic\Factory\AssetFactory; /** * A filter that wraps callables. * * @author Kris Wallsmith */ -class CallablesFilter implements FilterInterface +class CallablesFilter implements FilterInterface, DependencyExtractorInterface { private $loader; private $dumper; + private $extractor; /** * @param callable|null $loader * @param callable|null $dumper + * @param callable|null $extractor */ - public function __construct($loader = null, $dumper = null) + public function __construct($loader = null, $dumper = null, $extractor = null) { $this->loader = $loader; $this->dumper = $dumper; + $this->extractor = $extractor; } public function filterLoad(AssetInterface $asset) @@ -46,4 +50,14 @@ public function filterDump(AssetInterface $asset) $callable($asset); } } + + public function getChildren(AssetFactory $factory, $content, $loadPath = null) + { + if (null !== $callable = $this->extractor) { + return $callable($factory, $content, $loadPath); + } + + return array(); + } + } diff --git a/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/LessFilter.php b/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/LessFilter.php index 4acd38f..37c7567 100644 --- a/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/LessFilter.php +++ b/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/LessFilter.php @@ -161,8 +161,8 @@ public function filterDump(AssetInterface $asset) } /** - * @todo support for @import-once - * @todo support for @import (less) "lib.css" + * @todo support for import-once + * @todo support for import (less) "lib.css" */ public function getChildren(AssetFactory $factory, $content, $loadPath = null) { diff --git a/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/Sass/SassFilter.php b/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/Sass/SassFilter.php index 24e618d..a97e1a8 100644 --- a/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/Sass/SassFilter.php +++ b/core/vendor/kriswallsmith/assetic/src/Assetic/Filter/Sass/SassFilter.php @@ -204,14 +204,14 @@ public function getChildren(AssetFactory $factory, $content, $loadPath = null) if (pathinfo($reference, PATHINFO_EXTENSION)) { $needles = array( $reference, - '_'.$reference, + self::partialize($reference), ); } else { $needles = array( $reference.'.scss', $reference.'.sass', - '_'.$reference.'.scss', - '_'.$reference.'.sass', + self::partialize($reference).'.scss', + self::partialize($reference).'.sass', ); } @@ -233,4 +233,21 @@ public function getChildren(AssetFactory $factory, $content, $loadPath = null) return $children; } + + private static function partialize($reference) + { + $parts = pathinfo($reference); + + if ('.' === $parts['dirname']) { + $partial = '_'.$parts['filename']; + } else { + $partial = $parts['dirname'].DIRECTORY_SEPARATOR.'_'.$parts['filename']; + } + + if (isset($parts['extension'])) { + $partial .= '.'.$parts['extension']; + } + + return $partial; + } } diff --git a/core/vendor/kriswallsmith/assetic/src/Assetic/Util/PathUtils.php b/core/vendor/kriswallsmith/assetic/src/Assetic/Util/PathUtils.php new file mode 100644 index 0000000..4b11b11 --- /dev/null +++ b/core/vendor/kriswallsmith/assetic/src/Assetic/Util/PathUtils.php @@ -0,0 +1,20 @@ +stdin = $stdin; $this->setTimeout($timeout); + $this->useFileHandles = defined('PHP_WINDOWS_VERSION_BUILD'); $this->enhanceWindowsCompatibility = true; $this->enhanceSigchildCompatibility = !defined('PHP_WINDOWS_VERSION_BUILD') && $this->isSigchildEnabled(); $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); @@ -237,18 +238,15 @@ public function start($callback = null) } } - $this->process = proc_open($commandline, $descriptors, $this->pipes, $this->cwd, $this->env, $this->options); + $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); if (!is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; - foreach ($this->pipes as $pipe) { - stream_set_blocking($pipe, false); - } - - $this->writePipes(); + $this->processPipes->unblock(); + $this->processPipes->write(false, $this->stdin); $this->updateStatus(false); $this->checkTimeout(); } @@ -263,8 +261,8 @@ public function start($callback = null) * * @return Process The new process * - * @throws \RuntimeException When process can't be launch or is stopped - * @throws \RuntimeException When process is already running + * @throws RuntimeException When process can't be launch or is stopped + * @throws RuntimeException When process is already running * * @see start() */ @@ -291,8 +289,8 @@ public function restart($callback = null) * * @return integer The exitcode of the process * - * @throws \RuntimeException When process timed out - * @throws \RuntimeException When process stopped after receiving signal + * @throws RuntimeException When process timed out + * @throws RuntimeException When process stopped after receiving signal */ public function wait($callback = null) { @@ -300,22 +298,15 @@ public function wait($callback = null) if (null !== $callback) { $this->callback = $this->buildCallback($callback); } - while ($this->pipes || (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles)) { - $this->checkTimeout(); - $this->readPipes(true); - } - $this->updateStatus(false); - if ($this->processInformation['signaled']) { - if ($this->isSigchildEnabled()) { - throw new RuntimeException('The process has been signaled.'); - } - throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); - } + do { + $this->checkTimeout(); + $running = defined('PHP_WINDOWS_VERSION_BUILD') ? $this->isRunning() : $this->processPipes->hasOpenHandles(); + $close = !defined('PHP_WINDOWS_VERSION_BUILD') || !$running;; + $this->readPipes(true, $close); + } while ($running); - $time = 0; - while ($this->isRunning() && $time < 1000000) { - $time += 1000; + while ($this->isRunning()) { usleep(1000); } @@ -384,7 +375,7 @@ public function signal($signal) */ public function getOutput() { - $this->readPipes(false); + $this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true); return $this->stdout; } @@ -416,7 +407,7 @@ public function getIncrementalOutput() */ public function getErrorOutput() { - $this->readPipes(false); + $this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true); return $this->stderr; } @@ -642,9 +633,13 @@ public function stop($timeout = 10, $signal = null) $this->signal($signal ?: SIGKILL); } } + } - $this->updateStatus(false); + $this->updateStatus(false); + if ($this->processInformation['running']) { + $this->close(); } + $this->status = self::STATUS_TERMINATED; return $this->exitcode; @@ -951,38 +946,10 @@ public function checkTimeout() */ private function getDescriptors() { - //Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. - //Workaround for this problem is to use temporary files instead of pipes on Windows platform. - //@see https://bugs.php.net/bug.php?id=51800 - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $this->fileHandles = array( - self::STDOUT => tmpfile(), - ); - if (false === $this->fileHandles[self::STDOUT]) { - throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); - } - $this->readBytes = array( - self::STDOUT => 0, - ); - - return array(array('pipe', 'r'), $this->fileHandles[self::STDOUT], array('pipe', 'w')); - } - - if ($this->tty) { - $descriptors = array( - array('file', '/dev/tty', 'r'), - array('file', '/dev/tty', 'w'), - array('file', '/dev/tty', 'w'), - ); - } else { - $descriptors = array( - array('pipe', 'r'), // stdin - array('pipe', 'w'), // stdout - array('pipe', 'w'), // stderr - ); - } + $this->processPipes = new ProcessPipes($this->useFileHandles); + $descriptors = $this->processPipes->getDescriptors(); - if ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors = array_merge($descriptors, array(array('pipe', 'w'))); @@ -1033,10 +1000,11 @@ protected function updateStatus($blocking) return; } - $this->readPipes($blocking); - $this->processInformation = proc_get_status($this->process); $this->captureExitCode(); + + $this->readPipes($blocking, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true); + if (!$this->processInformation['running']) { $this->close(); $this->status = self::STATUS_TERMINATED; @@ -1061,160 +1029,23 @@ protected function isSigchildEnabled() } /** - * Handles the windows file handles fallbacks. - * - * @param Boolean $closeEmptyHandles if true, handles that are empty will be assumed closed - */ - private function processFileHandles($closeEmptyHandles = false) - { - $fh = $this->fileHandles; - foreach ($fh as $type => $fileHandle) { - fseek($fileHandle, $this->readBytes[$type]); - $data = fread($fileHandle, 8192); - if (strlen($data) > 0) { - $this->readBytes[$type] += strlen($data); - call_user_func($this->callback, $type == 1 ? self::OUT : self::ERR, $data); - } - if (false === $data || ($closeEmptyHandles && '' === $data && feof($fileHandle))) { - fclose($fileHandle); - unset($this->fileHandles[$type]); - } - } - } - - /** - * Returns true if a system call has been interrupted. - * - * @return Boolean - */ - private function hasSystemCallBeenInterrupted() - { - $lastError = error_get_last(); - - // stream_select returns false when the `select` system call is interrupted by an incoming signal - return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); - } - - /** * Reads pipes, executes callback. * * @param Boolean $blocking Whether to use blocking calls or not. */ - private function readPipes($blocking) - { - if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles) { - $this->processFileHandles(!$this->pipes); - } - - if ($this->pipes) { - $r = $this->pipes; - $w = null; - $e = null; - - // let's have a look if something changed in streams - if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(self::TIMEOUT_PRECISION * 1E6) : 0)) { - // if a system call has been interrupted, forget about it, let's try again - // otherwise, an error occured, let's reset pipes - if (!$this->hasSystemCallBeenInterrupted()) { - $this->pipes = array(); - } - - return; - } - - // nothing has changed - if (0 === $n) { - return; - } - - $this->processReadPipes($r); - } - } - - /** - * Writes data to pipes. - * - * @param Boolean $blocking Whether to use blocking calls or not. - */ - private function writePipes() + private function readPipes($blocking, $close) { - if ($this->tty) { - $this->status = self::STATUS_TERMINATED; - - return; - } - - if (null === $this->stdin) { - fclose($this->pipes[0]); - unset($this->pipes[0]); - - return; - } - - $writePipes = array($this->pipes[0]); - unset($this->pipes[0]); - $stdinLen = strlen($this->stdin); - $stdinOffset = 0; - - while ($writePipes) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $this->processFileHandles(); - } - - $r = $this->pipes; - $w = $writePipes; - $e = null; - - if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(static::TIMEOUT_PRECISION * 1E6) : 0)) { - // if a system call has been interrupted, forget about it, let's try again - if ($this->hasSystemCallBeenInterrupted()) { - continue; - } - break; - } - - // nothing has changed, let's wait until the process is ready - if (0 === $n) { - continue; - } - - if ($w) { - $written = fwrite($writePipes[0], (binary) substr($this->stdin, $stdinOffset), 8192); - if (false !== $written) { - $stdinOffset += $written; - } - if ($stdinOffset >= $stdinLen) { - fclose($writePipes[0]); - $writePipes = null; - } - } - - $this->processReadPipes($r); + if ($close) { + $result = $this->processPipes->readAndCloseHandles($blocking); + } else { + $result = $this->processPipes->read($blocking); } - } - /** - * Processes read pipes, executes callback on it. - * - * @param array $pipes - */ - private function processReadPipes(array $pipes) - { - foreach ($pipes as $pipe) { - $type = array_search($pipe, $this->pipes); - $data = fread($pipe, 8192); - - if (strlen($data) > 0) { - // last exit code is output and caught to work around --enable-sigchild - if (3 == $type) { - $this->fallbackExitcode = (int) $data; - } else { - call_user_func($this->callback, $type == 1 ? self::OUT : self::ERR, $data); - } - } - if (false === $data || feof($pipe)) { - fclose($pipe); - unset($this->pipes[$type]); + foreach ($result as $type => $data) { + if (3 == $type) { + $this->fallbackExitcode = (int) $data; + } else { + call_user_func($this->callback, $type === self::STDOUT ? self::OUT : self::ERR, $data); } } } @@ -1237,13 +1068,9 @@ private function captureExitCode() */ private function close() { - foreach ($this->pipes as $pipe) { - fclose($pipe); - } - - $this->pipes = null; $exitcode = -1; + $this->processPipes->close(); if (is_resource($this->process)) { $exitcode = proc_close($this->process); } @@ -1258,13 +1085,6 @@ private function close() $this->exitcode = 128 + $this->processInformation['termsig']; } - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - foreach ($this->fileHandles as $fileHandle) { - fclose($fileHandle); - } - $this->fileHandles = array(); - } - return $this->exitcode; } @@ -1280,11 +1100,8 @@ private function resetProcessData() $this->processInformation = null; $this->stdout = null; $this->stderr = null; - $this->pipes = null; $this->process = null; $this->status = self::STATUS_READY; - $this->fileHandles = null; - $this->readBytes = null; $this->incrementalOutputOffset = 0; $this->incrementalErrorOutputOffset = 0; } diff --git a/core/vendor/symfony/process/Symfony/Component/Process/ProcessPipes.php b/core/vendor/symfony/process/Symfony/Component/Process/ProcessPipes.php new file mode 100644 index 0000000..0c57d42 --- /dev/null +++ b/core/vendor/symfony/process/Symfony/Component/Process/ProcessPipes.php @@ -0,0 +1,305 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * ProcessPipes manages descriptors and pipes for the use of proc_open. + */ +class ProcessPipes +{ + /** @var array */ + public $pipes = array(); + /** @var array */ + private $fileHandles = array(); + /** @var array */ + private $readBytes = array(); + /** @var Boolean */ + private $useFiles; + + public function __construct($useFiles = false) + { + $this->useFiles = (Boolean) $useFiles; + + // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. + // Workaround for this problem is to use temporary files instead of pipes on Windows platform. + // + // Please note that this work around prevents hanging but + // another issue occurs : In some race conditions, some data may be + // lost or corrupted. + // + // @see https://bugs.php.net/bug.php?id=51800 + if ($this->useFiles) { + $this->fileHandles = array( + Process::STDOUT => tmpfile(), + Process::STDERR => tmpfile(), + ); + if (false === $this->fileHandles[Process::STDOUT]) { + throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + if (false === $this->fileHandles[Process::STDERR]) { + throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + $this->readBytes = array( + Process::STDOUT => 0, + Process::STDERR => 0, + ); + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * Sets non-blocking mode on pipes. + */ + public function unblock() + { + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + } + + /** + * Closes file handles and pipes. + */ + public function close() + { + $this->closeUnixPipes(); + foreach ($this->fileHandles as $offset => $handle) { + fclose($handle); + } + $this->fileHandles = array(); + } + + /** + * Closes unix pipes. + * + * Nothing happens in case file handles are used. + */ + public function closeUnixPipes() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = array(); + } + + /** + * Returns an array of descriptors for the use of proc_open. + * + * @return array + */ + public function getDescriptors() + { + if ($this->useFiles) { + return array( + array('pipe', 'r'), + $this->fileHandles[Process::STDOUT], + $this->fileHandles[Process::STDERR], + ); + } + + return array( + array('pipe', 'r'), // stdin + array('pipe', 'w'), // stdout + array('pipe', 'w'), // stderr + ); + } + + /** + * Reads data in file handles and pipes. + * + * @param Boolean $blocking Whether to use blocking calls or not. + * + * @return array An array of read data indexed by their fd. + */ + public function read($blocking) + { + return array_replace($this->readStreams($blocking), $this->readFileHandles()); + } + + /** + * Reads data in file handles and pipes, closes them if EOF is reached. + * + * @param Boolean $blocking Whether to use blocking calls or not. + * + * @return array An array of read data indexed by their fd. + */ + public function readAndCloseHandles($blocking) + { + return array_replace($this->readStreams($blocking, true), $this->readFileHandles(true)); + } + + /** + * Returns if the current state has open file handles or pipes. + * + * @return Boolean + */ + public function hasOpenHandles() + { + if ($this->useFiles) { + return (Boolean) $this->fileHandles; + } + + return (Boolean) $this->pipes; + } + + /** + * Writes stdin data. + * + * @param Boolean $blocking Whether to use blocking calls or not. + * @param string $stdin The data to write. + */ + public function write($blocking, $stdin) + { + if (null === $stdin) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + + return; + } + + $writePipes = array($this->pipes[0]); + unset($this->pipes[0]); + $stdinLen = strlen($stdin); + $stdinOffset = 0; + + while ($writePipes) { + $r = null; + $w = $writePipes; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(Process::TIMEOUT_PRECISION * 1E6) : 0)) { + // if a system call has been interrupted, forget about it, let's try again + if ($this->hasSystemCallBeenInterrupted()) { + continue; + } + break; + } + + // nothing has changed, let's wait until the process is ready + if (0 === $n) { + continue; + } + + if ($w) { + $written = fwrite($writePipes[0], (binary) substr($stdin, $stdinOffset), 8192); + if (false !== $written) { + $stdinOffset += $written; + } + if ($stdinOffset >= $stdinLen) { + fclose($writePipes[0]); + $writePipes = null; + } + } + } + } + + /** + * Reads data in file handles. + * + * @return array An array of read data indexed by their fd. + */ + private function readFileHandles($close = false) + { + $read = array(); + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, 16392)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * Reads data in file pipes streams. + * + * @param Boolean $blocking Whether to use blocking calls or not. + * + * @return array An array of read data indexed by their fd. + */ + private function readStreams($blocking, $close = false) + { + $read = array(); + + $r = $this->pipes; + $w = null; + $e = null; + + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(Process::TIMEOUT_PRECISION * 1E6) : 0)) { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occured, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = array(); + } + + return $read; + } + + // nothing has changed + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + $type = array_search($pipe, $this->pipes); + $data = fread($pipe, 8192); + + if (strlen($data) > 0) { + $read[$type] = $data; + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + + return $read; + } + + /** + * Returns true if a system call has been interrupted. + * + * @return Boolean + */ + private function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + // stream_select returns false when the `select` system call is interrupted by an incoming signal + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } +} diff --git a/core/vendor/symfony/process/Symfony/Component/Process/README.md b/core/vendor/symfony/process/Symfony/Component/Process/README.md index 7b9f307..9bcc656 100644 --- a/core/vendor/symfony/process/Symfony/Component/Process/README.md +++ b/core/vendor/symfony/process/Symfony/Component/Process/README.md @@ -43,5 +43,5 @@ Resources You can run the unit tests with the following command: $ cd path/to/Symfony/Component/XXX/ - $ composer.phar install --dev + $ composer.phar install $ phpunit diff --git a/core/vendor/symfony/process/Symfony/Component/Process/Tests/AbstractProcessTest.php b/core/vendor/symfony/process/Symfony/Component/Process/Tests/AbstractProcessTest.php index d0228f0..15c309a 100644 --- a/core/vendor/symfony/process/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/core/vendor/symfony/process/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -69,17 +69,16 @@ public function testCallbacksAreExecutedWithStart() { $data = ''; - $process = $this->getProcess('echo "foo";sleep 1;echo "foo"'); + $process = $this->getProcess('echo foo && php -r "sleep(1);" && echo foo'); $process->start(function ($type, $buffer) use (&$data) { $data .= $buffer; }); - $start = microtime(true); while ($process->isRunning()) { usleep(10000); } - $this->assertEquals("foo\nfoo\n", $data); + $this->assertEquals(2, preg_match_all('/foo/', $data, $matches)); } /** @@ -102,10 +101,6 @@ public function testProcessResponses($expected, $getter, $code) */ public function testProcessPipes($code, $size) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $this->markTestSkipped('Test hangs on Windows & PHP due to https://bugs.php.net/bug.php?id=60120 and https://bugs.php.net/bug.php?id=51800'); - } - $expected = str_repeat(str_repeat('*', 1024), $size) . '!'; $expectedLength = (1024 * $size) + 1; @@ -119,6 +114,12 @@ public function testProcessPipes($code, $size) public function chainedCommandsOutputProvider() { + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + return array( + array("2 \r\n2\r\n", '&&', '2') + ); + } + return array( array("1\n1\n", ';', '1'), array("2\n2\n", '&&', '2'), @@ -131,10 +132,6 @@ public function chainedCommandsOutputProvider() */ public function testChainedCommandsOutput($expected, $operator, $input) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $this->markTestSkipped('Does it work on windows ?'); - } - $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input)); $process->run(); $this->assertEquals($expected, $process->getOutput()); @@ -173,7 +170,7 @@ public function testGetIncrementalErrorOutput() public function testGetOutput() { - $p = new Process(sprintf('php -r %s', escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}'))); + $p = new Process(sprintf('php -r %s', escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++; usleep(500); }'))); $p->run(); $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches)); @@ -308,7 +305,7 @@ public function testIsSuccessful() public function testIsSuccessfulOnlyAfterTerminated() { - $process = $this->getProcess('sleep 1'); + $process = $this->getProcess('php -r "sleep(1);"'); $process->start(); while ($process->isRunning()) { $this->assertFalse($process->isSuccessful()); @@ -441,7 +438,7 @@ public function testPhpDeadlock() public function testRunProcessWithTimeout() { $timeout = 0.5; - $process = $this->getProcess('sleep 3'); + $process = $this->getProcess('php -r "sleep(3);"'); $process->setTimeout($timeout); $start = microtime(true); try { @@ -459,7 +456,7 @@ public function testCheckTimeoutOnStartedProcess() { $timeout = 0.5; $precision = 100000; - $process = $this->getProcess('sleep 3'); + $process = $this->getProcess('php -r "sleep(3);"'); $process->setTimeout($timeout); $start = microtime(true); @@ -480,6 +477,21 @@ public function testCheckTimeoutOnStartedProcess() $this->assertFalse($process->isSuccessful()); } + public function testStartAfterATimeout() + { + $process = $this->getProcess('php -r "while(true) {echo \'\'; usleep(1000); }"'); + $process->setTimeout(0.1); + try { + $process->run(); + $this->fail('An exception should have been raised.'); + } catch (\Exception $e) { + + } + $process->start(); + usleep(10000); + $process->stop(); + } + public function testGetPid() { $process = $this->getProcess('php -r "sleep(1);"'); @@ -597,11 +609,18 @@ public function pipesCodeProvider() { $variations = array( 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);', - 'include \''.__DIR__.'/ProcessTestHelper.php\';', + 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';', ); + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650 + $sizes = array(1, 2, 4, 8); + } else { + $sizes = array(1, 16, 64, 1024, 4096); + } + $codes = array(); - foreach (array(1, 16, 64, 1024, 4096) as $size) { + foreach ($sizes as $size) { foreach ($variations as $code) { $codes[] = array($code, $size); } diff --git a/core/vendor/symfony/process/Symfony/Component/Process/Tests/ProcessTestHelper.php b/core/vendor/symfony/process/Symfony/Component/Process/Tests/PipeStdinInStdoutStdErrStreamSelect.php similarity index 83% rename from core/vendor/symfony/process/Symfony/Component/Process/Tests/ProcessTestHelper.php rename to core/vendor/symfony/process/Symfony/Component/Process/Tests/PipeStdinInStdoutStdErrStreamSelect.php index 25cfb41..cdc7525 100644 --- a/core/vendor/symfony/process/Symfony/Component/Process/Tests/ProcessTestHelper.php +++ b/core/vendor/symfony/process/Symfony/Component/Process/Tests/PipeStdinInStdoutStdErrStreamSelect.php @@ -8,9 +8,9 @@ $read = array(STDIN); $write = array(STDOUT, STDERR); -stream_set_blocking(STDIN, false); -stream_set_blocking(STDOUT, false); -stream_set_blocking(STDERR, false); +stream_set_blocking(STDIN, 0); +stream_set_blocking(STDOUT, 0); +stream_set_blocking(STDERR, 0); $out = $err = ''; while ($read || $write) { @@ -26,7 +26,7 @@ } if (in_array(STDOUT, $w) && strlen($out) > 0) { - $written = fwrite(STDOUT, (binary) $out, 1024); + $written = fwrite(STDOUT, (binary) $out, 32768); if (false === $written) { die(ERR_WRITE_FAILED); } @@ -37,7 +37,7 @@ } if (in_array(STDERR, $w) && strlen($err) > 0) { - $written = fwrite(STDERR, (binary) $err, 1024); + $written = fwrite(STDERR, (binary) $err, 32768); if (false === $written) { die(ERR_WRITE_FAILED); } @@ -48,7 +48,7 @@ } if ($r) { - $str = fread(STDIN, 1024); + $str = fread(STDIN, 32768); if (false !== $str) { $out .= $str; $err .= $str;