diff --git a/framework/yii/console/controllers/AssetController.php b/framework/yii/console/controllers/AssetController.php index ca7896c..8e3de29 100644 --- a/framework/yii/console/controllers/AssetController.php +++ b/framework/yii/console/controllers/AssetController.php @@ -517,17 +517,77 @@ EOD */ public function combineCssFiles($inputFiles, $outputFile) { - // todo: adjust url() references in CSS files $content = ''; foreach ($inputFiles as $file) { $content .= "/*** BEGIN FILE: $file ***/\n" - . file_get_contents($file) + . $this->adjustCssUrl(file_get_contents($file), dirname($file), dirname($outputFile)) . "/*** END FILE: $file ***/\n"; } file_put_contents($outputFile, $content); } /** + * Adjusts CSS content allowing URL references pointing to the original resources. + * @param string $cssContent source CSS content. + * @param string $inputFilePath input CSS file name. + * @param string $outputFilePath output CSS file name. + * @return string adjusted CSS content. + */ + protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath) + { + $sharedPathParts = array(); + $inputFilePathParts = explode('/', $inputFilePath); + $inputFilePathPartsCount = count($inputFilePathParts); + $outputFilePathParts = explode('/', $outputFilePath); + $outputFilePathPartsCount = count($outputFilePathParts); + for ($i =0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) { + if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) { + $sharedPathParts[] = $inputFilePathParts[$i]; + } else { + break; + } + } + $sharedPath = implode('/', $sharedPathParts); + + $inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/'); + $outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/'); + $inputFileRelativePathParts = explode('/', $inputFileRelativePath); + $outputFileRelativePathParts = explode('/', $outputFileRelativePath); + + $callback = function($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) { + $fullMatch = $matches[0]; + $inputUrl = $matches[1]; + + if (preg_match('/https?:\/\//is', $inputUrl)) { + return $fullMatch; + } + + $outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..'); + $outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts); + + if (strpos($inputUrl, '/') !== false) { + $inputUrlParts = explode('/', $inputUrl); + foreach ($inputUrlParts as $key => $inputUrlPart) { + if ($inputUrlPart == '..') { + array_pop($outputUrlParts); + unset($inputUrlParts[$key]); + } + } + $outputUrlParts[] = implode('/', $inputUrlParts); + } else { + $outputUrlParts[] = $inputUrl; + } + $outputUrl = implode('/', $outputUrlParts); + + return str_replace($inputUrl, $outputUrl, $fullMatch); + }; + + $cssContent = preg_replace_callback('/url\(["\']?([^"]*)["\']?\)/is', $callback, $cssContent); + + return $cssContent; + } + + /** * Creates template of configuration file for [[actionCompress]]. * @param string $configFile output file name. */ diff --git a/framework/yii/jui/Accordion.php b/framework/yii/jui/Accordion.php index 1a83f28..f36c981 100644 --- a/framework/yii/jui/Accordion.php +++ b/framework/yii/jui/Accordion.php @@ -56,15 +56,38 @@ use yii\helpers\Html; class Accordion extends Widget { /** - * @var array list of collapsible sections. + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the container tag of this widget + */ + public $options = array(); + /** + * @var array list of collapsible items. Each item can be an array of the following structure: + * + * ~~~ + * array( + * 'header' => 'Item header', + * 'content' => 'Item content', + * // the HTML attributes of the item header container tag. This will overwrite "headerOptions". + * 'headerOptions' => array(), + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => array(), + * ) + * ~~~ */ public $items = array(); /** - * @var array list of individual collabsible section default options. + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. */ public $itemOptions = array(); /** - * @var array list of individual collabsible section header default options. + * @var array list of HTML attributes for the item header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "h3", the tag name of the item container tags. */ public $headerOptions = array(); @@ -83,7 +106,7 @@ class Accordion extends Widget } /** - * Renders collapsible sections as specified on [[items]]. + * Renders collapsible items as specified on [[items]]. * @return string the rendering result. * @throws InvalidConfigException. */ diff --git a/tests/unit/framework/console/controllers/AssetControllerTest.php b/tests/unit/framework/console/controllers/AssetControllerTest.php index d792c9e..db6d2a7 100644 --- a/tests/unit/framework/console/controllers/AssetControllerTest.php +++ b/tests/unit/framework/console/controllers/AssetControllerTest.php @@ -169,6 +169,23 @@ class AssetControllerTest extends TestCase } } + /** + * Invokes the asset controller method even if it is protected. + * @param string $methodName name of the method to be invoked. + * @param array $args method arguments. + * @return mixed method invoke result. + */ + protected function invokeAssetControllerMethod($methodName, array $args = array()) + { + $controller = $this->createAssetController(); + $controllerClassReflection = new ReflectionClass(get_class($controller)); + $methodReflection = $controllerClassReflection->getMethod($methodName); + $methodReflection->setAccessible(true); + $result = $methodReflection->invokeArgs($controller, $args); + $methodReflection->setAccessible(false); + return $result; + } + // Tests : public function testActionTemplate() @@ -237,4 +254,65 @@ class AssetControllerTest extends TestCase $this->assertContains($content, $compressedJsFileContent, "Source of '{$name}' is missing in combined file!"); } } + + /** + * Data provider for [[testAdjustCssUrl()]]. + * @return array test data. + */ + public function adjustCssUrlDataProvider() + { + return array( + array( + '.published-same-dir-class {background-image: url(published_same_dir.png);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.published-same-dir-class {background-image: url(../input/published_same_dir.png);}', + ), + array( + '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', + ), + array( + '.static-same-dir-class {background-image: url(\'static_same_dir.png\');}', + '/test/base/path/css', + '/test/base/path/assets/output', + '.static-same-dir-class {background-image: url(\'../../css/static_same_dir.png\');}', + ), + array( + '.static-relative-dir-class {background-image: url("../img/static_relative_dir.png");}', + '/test/base/path/css', + '/test/base/path/assets/output', + '.static-relative-dir-class {background-image: url("../../img/static_relative_dir.png");}', + ), + array( + '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', + ), + array( + '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', + ), + ); + } + + /** + * @dataProvider adjustCssUrlDataProvider + * + * @param $cssContent + * @param $inputFilePath + * @param $outputFilePath + * @param $expectedCssContent + */ + public function testAdjustCssUrl($cssContent, $inputFilePath, $outputFilePath, $expectedCssContent) + { + $adjustedCssContent = $this->invokeAssetControllerMethod('adjustCssUrl', array($cssContent, $inputFilePath, $outputFilePath)); + + $this->assertEquals($expectedCssContent, $adjustedCssContent, 'Unable to adjust CSS correctly!'); + } }