├── cache └── empty ├── tests ├── tmp │ └── empty ├── Munee │ ├── Mocks │ │ ├── MockRequest.php │ │ └── MockAssetType.php │ └── Cases │ │ ├── Asset │ │ └── RegistryTest.php │ │ ├── UtilsTest.php │ │ ├── RequestTest.php │ │ └── ResponseTest.php └── bootstrap.php ├── .gitignore ├── .travis.yml ├── phpunit.xml.dist ├── src └── Munee │ ├── ErrorException.php │ ├── Asset │ ├── NotFoundException.php │ ├── Type │ │ ├── CompilationException.php │ │ ├── JavaScript.php │ │ ├── Image.php │ │ └── Css.php │ ├── Filter.php │ ├── HeaderSetter.php │ ├── Filter │ │ ├── Image │ │ │ ├── Negative.php │ │ │ ├── Colorize.php │ │ │ ├── Grayscale.php │ │ │ └── Resize.php │ │ ├── JavaScript │ │ │ ├── Minify.php │ │ │ └── Packer.php │ │ └── Css │ │ │ └── Minify.php │ ├── Registry.php │ └── Type.php │ ├── Dispatcher.php │ ├── Response.php │ ├── Utils.php │ └── Request.php ├── README.md ├── LICENSE ├── composer.json └── config └── bootstrap.php /cache/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tmp/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | composer.phar 4 | cache/* 5 | vendor/* 6 | webroot/* 7 | !empty -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | 10 | matrix: 11 | allow_failures: 12 | - php: 7.0 13 | 14 | before_script: 15 | - composer install 16 | 17 | script: 18 | - phpunit 19 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | ./tests/Munee/ 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Munee/ErrorException.php: -------------------------------------------------------------------------------- 1 | ext = 'foo'; 26 | } 27 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | content = 'foo'; 25 | } 26 | 27 | /** 28 | * Override function to set test header 29 | */ 30 | public function getHeaders() 31 | { 32 | header("Content-Type: text/test"); 33 | } 34 | 35 | /** 36 | * Override function to return an arbitrary timestamp 37 | * 38 | * @return int 39 | */ 40 | public function getLastModifiedDate() 41 | { 42 | return 123456789; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Munee/Asset/Filter.php: -------------------------------------------------------------------------------- 1 | allowedParams; 33 | } 34 | 35 | /** 36 | * A Sub-Class uses this method to manipulate the image based on the params passed in 37 | * 38 | * @param string $originalFile 39 | * @param array $arguments 40 | * @param array $typeOptions 41 | */ 42 | abstract public function doFilter($originalFile, $arguments, $typeOptions); 43 | } -------------------------------------------------------------------------------- /src/Munee/Asset/HeaderSetter.php: -------------------------------------------------------------------------------- 1 | array( 28 | 'regex' => 'true|false|t|f|yes|no|y|n', 29 | 'cast' => 'string' 30 | ) 31 | ); 32 | 33 | /** 34 | * Turn an image Negative 35 | * 36 | * @param string $file 37 | * @param array $arguments 38 | * @param array $typeOptions 39 | * 40 | * @return void 41 | */ 42 | public function doFilter($file, $arguments, $typeOptions) 43 | { 44 | $Imagine = new Imagine(); 45 | $image = $Imagine->open($file); 46 | $image->effects()->negative(); 47 | $image->save($file); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Munee/Asset/Filter/JavaScript/Minify.php: -------------------------------------------------------------------------------- 1 | array( 28 | 'regex' => 'true|false|t|f|yes|no|y|n', 29 | 'default' => 'false', 30 | 'cast' => 'boolean' 31 | ) 32 | ); 33 | 34 | /** 35 | * JavaScript Minification 36 | * 37 | * @param string $file 38 | * @param array $arguments 39 | * @param array $javaScriptOptions 40 | * 41 | * @return void 42 | */ 43 | public function doFilter($file, $arguments, $javaScriptOptions) 44 | { 45 | if (! $arguments['minify']) { 46 | return; 47 | } 48 | 49 | file_put_contents($file, Minifier::minify(file_get_contents($file))); 50 | } 51 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meenie/Munee", 3 | "description": "Standalone PHP 5.3 Asset Optimisation & Manipulation - On-The-Fly Image Resizing, On-the-fly LESS, SCSS, CoffeeScript Compiling, CSS & JavaScript Combining/Minifying, and Smart Client Side and Server Side Caching", 4 | "keywords": ["php","less","scss","coffeescript","cache","asset optimisation","asset optimization","asset management","css","javascript","lessphp","scssphp","image manipulation"], 5 | "homepage": "http://mun.ee", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Cody Lundquist", 10 | "email": "cody.lundquist@gmail.com", 11 | "role": "Developer" 12 | } 13 | ], 14 | "autoload": { 15 | "psr-4": { 16 | "Munee\\": "src/Munee" 17 | }, 18 | "files": ["config/bootstrap.php"] 19 | }, 20 | "require": { 21 | "php": ">=5.3.2", 22 | "oyejorge/less.php": "1.7.0.5", 23 | "leafo/scssphp": "0.1.1", 24 | "tedivm/jshrink": "1.0.1", 25 | "imagine/imagine": "0.6.2", 26 | "coffeescript/coffeescript": "1.3.1", 27 | "meenie/javascript-packer": "1.1", 28 | "tubalmartin/cssmin": "~2.4", 29 | "sabberworm/php-css-parser": "7.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Munee/Asset/Type/JavaScript.php: -------------------------------------------------------------------------------- 1 | response->headerController->headerField('Content-Type', 'text/javascript'); 27 | } 28 | 29 | 30 | /** 31 | * Callback method called before filters are run 32 | * 33 | * Overriding to run the file through CoffeeScript compiler if it has a .coffee extension 34 | * 35 | * @param string $originalFile 36 | * @param string $cacheFile 37 | */ 38 | protected function beforeFilter($originalFile, $cacheFile) 39 | { 40 | if ('coffee' == pathinfo($originalFile, PATHINFO_EXTENSION)) { 41 | $coffeeScript = CoffeeScript\Compiler::compile(file_get_contents($originalFile), array('header' => false)); 42 | file_put_contents($cacheFile, $coffeeScript); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Munee/Asset/Filter/Image/Colorize.php: -------------------------------------------------------------------------------- 1 | array( 29 | 'regex' => '[A-Fa-f0-9]{3}$|^[A-Fa-f0-9]{6}', 30 | 'cast' => 'string' 31 | ) 32 | ); 33 | 34 | /** 35 | * Colorize an image 36 | * 37 | * @param string $file 38 | * @param array $arguments 39 | * @param array $typeOptions 40 | * 41 | * @return void 42 | */ 43 | public function doFilter($file, $arguments, $typeOptions) 44 | { 45 | $Imagine = new Imagine(); 46 | $image = $Imagine->open($file); 47 | $colour = $image->palette()->color('#' . $arguments['colorize']); 48 | $image->effects()->colorize($colour); 49 | $image->save($file); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Munee/Asset/Filter/Image/Grayscale.php: -------------------------------------------------------------------------------- 1 | array( 28 | 'regex' => 'true|false|t|f|yes|no|y|n', 29 | 'default' => 'false', 30 | 'cast' => 'boolean' 31 | ) 32 | ); 33 | 34 | /** 35 | * Turn an image Grayscale 36 | * 37 | * @param string $file 38 | * @param array $arguments 39 | * @param array $typeOptions 40 | * 41 | * @return void 42 | */ 43 | public function doFilter($file, $arguments, $typeOptions) 44 | { 45 | if (! $arguments['grayscale']) { 46 | return; 47 | } 48 | 49 | $Imagine = new Imagine(); 50 | $image = $Imagine->open($file); 51 | $image->effects()->grayscale(); 52 | $image->save($file); 53 | } 54 | } -------------------------------------------------------------------------------- /src/Munee/Asset/Filter/Css/Minify.php: -------------------------------------------------------------------------------- 1 | array( 29 | 'regex' => 'true|false|t|f|yes|no|y|n', 30 | 'default' => 'false', 31 | 'cast' => 'boolean' 32 | ) 33 | ); 34 | 35 | /** 36 | * CSS Minification 37 | * 38 | * @param string $file 39 | * @param array $arguments 40 | * @param array $cssOptions 41 | * 42 | * @return void 43 | */ 44 | public function doFilter($file, $arguments, $cssOptions) 45 | { 46 | if (! $arguments['minify']) { 47 | return; 48 | } 49 | 50 | $content = file_get_contents($file); 51 | $compressor = new CSSmin(); 52 | 53 | if (Utils::isSerialized($content, $content)) { 54 | $content['compiled'] = $compressor->run($content['compiled']); 55 | $content = serialize($content); 56 | } else { 57 | $content = $compressor->run($content); 58 | } 59 | 60 | file_put_contents($file, $content); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Munee/Asset/Filter/JavaScript/Packer.php: -------------------------------------------------------------------------------- 1 | array( 28 | 'regex' => 'true|false|t|f|yes|no|y|n', 29 | 'default' => 'false', 30 | 'cast' => 'boolean' 31 | ) 32 | ); 33 | 34 | /** 35 | * Default options for the Packer library 36 | * 37 | * @var array 38 | */ 39 | protected $_defaultPackerOptions = array( 40 | 'encoding' => 62, 41 | 'fastDecode' => true, 42 | 'specialChars' => false 43 | ); 44 | 45 | /** 46 | * JavaScript Packer 47 | * 48 | * @param string $file 49 | * @param array $arguments 50 | * @param array $javaScriptOptions 51 | * 52 | * @return void 53 | */ 54 | public function doFilter($file, $arguments, $javaScriptOptions) 55 | { 56 | $userOptions = isset($javaScriptOptions['packer']) ? $javaScriptOptions['packer'] : array(); 57 | $options = array_merge($this->_defaultPackerOptions, $userOptions); 58 | 59 | if (! $arguments['packer']) { 60 | return; 61 | } 62 | 63 | $content = file_get_contents($file); 64 | $packer = new JavaScriptPacker($content, $options['encoding'], $options['fastDecode'], $options['specialChars']); 65 | file_put_contents($file, $packer->pack()); 66 | } 67 | } -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertSame(get_class($CheckMockAssetType), get_class($MockAssetType)); 50 | } 51 | 52 | /** 53 | * Make sure we are getting ErrorExceptions when extensions are not supported 54 | */ 55 | public function testExtensionNotSupported() 56 | { 57 | $this->setExpectedException('Munee\ErrorException'); 58 | Registry::getSupportedExtensions('nope'); 59 | } 60 | 61 | /** 62 | * Lets see if we get an exception when an extension is not registered 63 | */ 64 | public function testExtensionNotRegistered() 65 | { 66 | $MockRequest = new MockRequest(); 67 | $MockRequest->ext = 'nope'; 68 | $this->setExpectedException('Munee\ErrorException'); 69 | Registry::getClass($MockRequest); 70 | } 71 | 72 | /** 73 | * Check to make sure all extensions are supported when registered 74 | */ 75 | public function testSupportedExtensions() 76 | { 77 | $checkExtensions = array('foo', 'bar'); 78 | $supportedExtensions = Registry::getSupportedExtensions('bar'); 79 | 80 | $this->assertSame($checkExtensions, $supportedExtensions); 81 | } 82 | 83 | /** 84 | * Make sure we can un-register extensions 85 | */ 86 | public function testUnRegisterExtension() 87 | { 88 | Registry::unRegister('bar'); 89 | 90 | $checkExtensions = array('foo'); 91 | $supportedExtensions = Registry::getSupportedExtensions('foo'); 92 | 93 | $this->assertSame($checkExtensions, $supportedExtensions); 94 | } 95 | } -------------------------------------------------------------------------------- /src/Munee/Asset/Registry.php: -------------------------------------------------------------------------------- 1 | ext, $registered['extensions'])) { 73 | return $registered['resolve']($Request); 74 | } 75 | } 76 | 77 | throw new ErrorException("The following extension is not handled: {$Request->ext}"); 78 | } 79 | 80 | /** 81 | * Get Supported Extensions 82 | * 83 | * @param string $extension 84 | * 85 | * @return array 86 | * 87 | * @throws ErrorException 88 | */ 89 | public static function getSupportedExtensions($extension) 90 | { 91 | foreach (static::$_registry as $registered) { 92 | if (in_array($extension, (array) $registered['extensions'])) { 93 | return $registered['extensions']; 94 | } 95 | } 96 | 97 | throw new ErrorException("The following extension is not handled: {$extension}"); 98 | } 99 | } -------------------------------------------------------------------------------- /tests/Munee/Cases/UtilsTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(is_dir($testDir)); 27 | 28 | Utils::createDir($testDir); 29 | $this->assertTrue(is_dir($testDir)); 30 | 31 | Utils::removeDir(WEBROOT . DS . 'test'); 32 | } 33 | 34 | /** 35 | * Lets test all types of serialisation! 36 | */ 37 | public function testIsSerialized() 38 | { 39 | $notSerializedString = 'nope'; 40 | $this->assertFalse(Utils::isSerialized($notSerializedString)); 41 | 42 | $notString = 42; 43 | $this->assertFalse(Utils::isSerialized($notString)); 44 | 45 | $wrongQuotesSerial = "s:1:'foo';"; 46 | $this->assertFalse(Utils::isSerialized($wrongQuotesSerial)); 47 | $malformedStringSerial = 's:1:"foo";'; 48 | $this->assertFalse(Utils::isSerialized($malformedStringSerial)); 49 | $correctStringSerial = 's:3:"foo";'; 50 | $this->assertTrue(Utils::isSerialized($correctStringSerial)); 51 | 52 | $malformedArraySerial = 'a:1:{s:1:"foo";}'; 53 | $this->assertFalse(Utils::isSerialized($malformedArraySerial)); 54 | $correctArraySerial = 'a:1:{s:3:"foo";s:3:"bar";}'; 55 | $this->assertTrue(Utils::isSerialized($correctArraySerial)); 56 | 57 | $malformedObjectSerial = 'O8:"stdClass":0:{}'; 58 | $this->assertFalse(Utils::isSerialized($malformedObjectSerial)); 59 | $malformedObjectSerial = 'O:::"stdClass":0:{}'; 60 | $this->assertFalse(Utils::isSerialized($malformedObjectSerial)); 61 | $correctObjectSerial = 'O:8:"stdClass":0:{}'; 62 | $this->assertTrue(Utils::isSerialized($correctObjectSerial)); 63 | 64 | $malformedNullSerial = 'N:'; 65 | $this->assertFalse(Utils::isSerialized($malformedNullSerial)); 66 | $correctNullSerial = 'N;'; 67 | $this->assertTrue(Utils::isSerialized($correctNullSerial)); 68 | 69 | $isTrue = false; 70 | $serializedTrue = 'b:1;'; 71 | $this->assertTrue(Utils::isSerialized($serializedTrue, $isTrue)); 72 | $this->assertTrue($isTrue); 73 | 74 | $isFalse = false; 75 | $serializedFalse = 'b:0;'; 76 | $this->assertTrue(Utils::isSerialized($serializedFalse, $isFalse)); 77 | $this->assertFalse($isFalse); 78 | 79 | $checkArray = array('foo' => 'bar'); 80 | $testArray = array(); 81 | Utils::isSerialized('a:1:{s:3:"foo";s:3:"bar";}', $testArray); 82 | $this->assertSame($checkArray, $testArray); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Munee/Dispatcher.php: -------------------------------------------------------------------------------- 1 | true, 25 | 'maxAge' => 0 26 | ); 27 | 28 | /** 29 | * 1) Initialise the Request 30 | * 2) Grab the AssetType based on the request and initialise it 31 | * 3) Instantiate the Response class, set the headers, and then return the content 32 | * 33 | * Rap everything in a Try/Catch block for error handling 34 | * 35 | * @param Request $Request 36 | * @param array $options 37 | * 38 | * @return string 39 | * 40 | * @catch NotFoundException 41 | * @catch ErrorException 42 | */ 43 | public static function run(Request $Request, $options = array()) 44 | { 45 | try { 46 | /** 47 | * Merge in default options 48 | */ 49 | $options = array_merge(self::$defaultOptions, $options); 50 | /** 51 | * Set the header controller. Can be overwritten by the dispatcher options 52 | */ 53 | if ( 54 | isset($options['headerController']) && 55 | $options['headerController'] instanceof Asset\HeaderSetter 56 | ) { 57 | $headerController = $options['headerController']; 58 | } else { 59 | $headerController = new Asset\HeaderSetter; 60 | } 61 | /** 62 | * Initialise the Request 63 | */ 64 | $Request->init(); 65 | /** 66 | * Grab the correct AssetType 67 | */ 68 | $AssetType = Asset\Registry::getClass($Request); 69 | /** 70 | * Initialise the AssetType 71 | */ 72 | $AssetType->init(); 73 | /** 74 | * Create a response 75 | */ 76 | $Response = new Response($AssetType); 77 | $Response->setHeaderController($headerController); 78 | /** 79 | * Set the headers if told to do so 80 | */ 81 | if ($options['setHeaders']) { 82 | /** 83 | * Set the headers. 84 | */ 85 | $Response->setHeaders($options['maxAge']); 86 | } 87 | /** 88 | * If the content hasn't been modified return null so only headers are sent 89 | * otherwise return the content 90 | */ 91 | return $Response->notModified ? null : $Response->render(); 92 | } catch (Asset\NotFoundException $e) { 93 | if (isset($headerController) && $headerController instanceof Asset\HeaderSetter) { 94 | $headerController->statusCode('HTTP/1.0', 404, 'Not Found'); 95 | $headerController->headerField('Status', '404 Not Found'); 96 | } 97 | 98 | return 'Not Found Error: ' . static::getErrors($e); 99 | } catch (Asset\Type\CompilationException $e) { 100 | if (isset($AssetType) && $AssetType instanceof Asset\Type) { 101 | $AssetType->cleanUpAfterError(); 102 | } 103 | 104 | return 'Compilation Error: ' . static::getErrors($e); 105 | } catch (ErrorException $e) { 106 | return 'Error: ' . static::getErrors($e); 107 | } 108 | } 109 | 110 | /** 111 | * Grabs all of the Exception messages in a chain 112 | * 113 | * @param \Exception $e 114 | * 115 | * @return string 116 | */ 117 | protected static function getErrors (\Exception $e) { 118 | $errors = $e->getMessage(); 119 | while ($e = $e->getPrevious()) { 120 | $errors .= "
" . $e->getMessage(); 121 | } 122 | 123 | return $errors; 124 | } 125 | } -------------------------------------------------------------------------------- /src/Munee/Response.php: -------------------------------------------------------------------------------- 1 | assetType = $AssetType; 57 | 58 | $AssetType->setResponse($this); 59 | } 60 | 61 | /** 62 | * Set controller for setting headers. 63 | * 64 | * @param object $headerController 65 | * 66 | * @return self 67 | * 68 | * @throws ErrorException 69 | */ 70 | public function setHeaderController($headerController) 71 | { 72 | if(! $headerController instanceof Asset\HeaderSetter) { 73 | throw new ErrorException('Header controller must be an instance of Asset\HeaderSetter.'); 74 | } 75 | 76 | $this->headerController = $headerController; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Set Headers for Response 83 | * 84 | * @param integer $maxAge - Used with the cache-control to tell the browser how long it should wait before revalidating 85 | * 86 | * @return self 87 | * 88 | * @throws ErrorException 89 | */ 90 | public function setHeaders($maxAge) 91 | { 92 | $lastModifiedDate = $this->assetType->getLastModifiedDate(); 93 | $eTag = md5($lastModifiedDate . $this->assetType->getContent()); 94 | $checkModifiedSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? 95 | $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false; 96 | $checkETag = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? 97 | $_SERVER['HTTP_IF_NONE_MATCH'] : false; 98 | 99 | if ( 100 | ($checkModifiedSince && strtotime($checkModifiedSince) == $lastModifiedDate) || 101 | $checkETag == $eTag 102 | ) { 103 | $this->headerController->statusCode('HTTP/1.1', 304, 'Not Modified'); 104 | $this->notModified = true; 105 | } else { 106 | // We don't want the browser to handle any cache, Munee will handle that. 107 | $this->headerController->headerField('Cache-Control', 'max-age=' . $maxAge . ', must-revalidate'); 108 | $this->headerController->headerField('Last-Modified', gmdate('D, d M Y H:i:s', $lastModifiedDate) . ' GMT'); 109 | $this->headerController->headerField('ETag', $eTag); 110 | $this->assetType->getHeaders(); 111 | } 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Returns the Asset Types content. 118 | * It will try and use Gzip to compress the content and save bandwidth 119 | * 120 | * @return string 121 | */ 122 | public function render() 123 | { 124 | $content = $this->assetType->getContent(); 125 | /** 126 | * Do not use ob_gzhandler() if zlib.output_compression ini option is set 127 | * This will gzip the output twice and the text will be garbled 128 | */ 129 | if (@ini_get('zlib.output_compression')) { 130 | $ret = $content; 131 | } else if (! $ret = ob_gzhandler($content, PHP_OUTPUT_HANDLER_START | PHP_OUTPUT_HANDLER_END)) { 132 | $ret = $content; 133 | } 134 | 135 | return $ret; 136 | } 137 | } -------------------------------------------------------------------------------- /src/Munee/Utils.php: -------------------------------------------------------------------------------- 1 | setExpectedException('Munee\ErrorException'); 53 | $Request->init(); 54 | } 55 | 56 | /** 57 | * Constructor Test 58 | */ 59 | public function testConstructor() 60 | { 61 | $Request = new Request(array('foo' => 'bar')); 62 | $this->assertSame(array('foo' => 'bar'), $Request->options); 63 | } 64 | 65 | /** 66 | * Make sure files are parsed properly and the extension is set 67 | */ 68 | public function testInit() 69 | { 70 | $_GET = array( 71 | 'files' => '/js/foo.js,/js/bar.js' 72 | ); 73 | 74 | $Request = new Request(); 75 | 76 | $Request->init(); 77 | 78 | $this->assertSame('js', $Request->ext); 79 | $this->assertSame(array(WEBROOT . '/js/foo.js', WEBROOT . '/js/bar.js'), $Request->files); 80 | } 81 | 82 | /** 83 | * Make sure all passed in files can be handled by the asset type class 84 | */ 85 | public function testExtensionNotSupported() 86 | { 87 | $_GET = array( 88 | 'files' => '/js/foo.jpg,/js/bar.js' 89 | ); 90 | 91 | $Request = new Request(); 92 | 93 | $this->setExpectedException('Munee\ErrorException'); 94 | $Request->init(); 95 | } 96 | 97 | /** 98 | * Make sure they can not go above webroot when requesting a file 99 | */ 100 | public function testGoingAboveWebroot() 101 | { 102 | $_GET = array( 103 | 'files' => '/../..././js/bad.js,/js/bar.js' 104 | ); 105 | 106 | $Request = new Request(); 107 | 108 | $Request->init(); 109 | $this->assertSame(array(WEBROOT . '/js/bad.js', WEBROOT . '/js/bar.js'), $Request->files); 110 | } 111 | 112 | /** 113 | * Make legacy code is still being supported 114 | */ 115 | public function testLegacyCode() 116 | { 117 | $_GET = array( 118 | 'files' => '/minify/js/foo.js' 119 | ); 120 | 121 | $Request = new Request(); 122 | 123 | $Request->init(); 124 | 125 | $this->assertSame(array(WEBROOT . '/js/foo.js'), $Request->files); 126 | $this->assertSame(array('minify' => 'true'), $Request->getRawParams()); 127 | } 128 | 129 | /** 130 | * Make sure you are getting the correct raw parameters 131 | */ 132 | public function testGetRawParams() 133 | { 134 | $_GET = array( 135 | 'files' => '/js/foo.js,/js/bar.js', 136 | 'minify' => 'true', 137 | 'notAllowedParam' => 'foo' 138 | ); 139 | 140 | $Request = new Request(); 141 | 142 | $Request->init(); 143 | $rawParams = $Request->getRawParams(); 144 | $this->assertSame(array('minify' => 'true', 'notAllowedParam' => 'foo'), $rawParams); 145 | } 146 | 147 | /** 148 | * Make sure the Parameter Parser is doing it's job correctly 149 | */ 150 | public function testParseParams() 151 | { 152 | $_GET = array( 153 | 'files' => '/js/foo.js,/js/bar.js', 154 | 'foo' => 'true', 155 | 'b' => 'ba[42]', 156 | 'notAllowedParam' => 'foo' 157 | 158 | ); 159 | 160 | $Request = new Request(); 161 | 162 | $Request->init(); 163 | 164 | $this->assertEquals(array(), $Request->params); 165 | 166 | $allowedParams = array( 167 | 'foo' => array( 168 | 'alias' => 'f', 169 | 'default' => 'false', 170 | 'cast' => 'boolean' 171 | ), 172 | 'bar' => array( 173 | 'alias' => 'b', 174 | 'arguments' => array( 175 | 'baz' => array( 176 | 'alias' => 'ba', 177 | 'regex' => '\d+', 178 | 'cast' => 'integer', 179 | 'default' => 24 180 | ), 181 | ) 182 | ), 183 | 'qux' => array( 184 | 'default' => 'no' 185 | ) 186 | ); 187 | 188 | $Request->parseParams($allowedParams); 189 | $assertedParams = array( 190 | 'foo' => true, 191 | 'bar' => array( 192 | 'baz' => 42 193 | ), 194 | 'qux' => 'no' 195 | ); 196 | 197 | $this->assertSame($assertedParams, $Request->params); 198 | } 199 | 200 | /** 201 | * Make sure the param validation is working properly 202 | */ 203 | public function testWrongParamValue() 204 | { 205 | $_GET = array( 206 | 'files' => '/js/foo.js', 207 | 'foo' => 'not good' 208 | ); 209 | 210 | $Request = new Request(); 211 | 212 | $Request->init(); 213 | 214 | $allowedParams = array( 215 | 'foo' => array( 216 | 'alias' => 'f', 217 | 'regex' => 'true|false', 218 | 'default' => 'false', 219 | 'cast' => 'boolean' 220 | ) 221 | ); 222 | 223 | $this->setExpectedException('Munee\ErrorException'); 224 | $Request->parseParams($allowedParams); 225 | } 226 | } -------------------------------------------------------------------------------- /src/Munee/Asset/Filter/Image/Resize.php: -------------------------------------------------------------------------------- 1 | array( 32 | 'arguments' => array( 33 | 'width' => array( 34 | 'alias' => 'w', 35 | 'regex' => '\d+', 36 | 'cast' => 'integer' 37 | ), 38 | 'height' => array( 39 | 'alias' => 'h', 40 | 'regex' => '\d+', 41 | 'cast' => 'integer' 42 | ), 43 | 'quality' => array( 44 | 'alias' => array('q', 'qlty', 'jpeg_quality'), 45 | 'regex' => '\d{1,2}(?!\d)|100', 46 | 'default' => 75, 47 | 'cast' => 'integer' 48 | ), 49 | 'exact' => array( 50 | 'alias' => 'e', 51 | 'regex' => 'true|false|t|f|yes|no|y|n', 52 | 'default' => 'false', 53 | 'cast' => 'boolean' 54 | ), 55 | 'stretch' => array( 56 | 'alias' => 's', 57 | 'regex' => 'true|false|t|f|yes|no|y|n', 58 | 'default' => 'false', 59 | 'cast' => 'boolean' 60 | ), 61 | 'fill' => array( 62 | 'alias' => 'f', 63 | 'regex' => 'true|false|t|f|yes|no|y|n', 64 | 'default' => 'false', 65 | 'cast' => 'boolean' 66 | ), 67 | 'fillColour' => array( 68 | 'alias' => array( 69 | 'fc', 70 | 'fillColor', 71 | 'fillcolor', 72 | 'fill_color', 73 | 'fill-color', 74 | 'fillcolour', 75 | 'fill_colour', 76 | 'fill-colour' 77 | ), 78 | 'regex' => '[A-Fa-f0-9]{3}$|^[A-Fa-f0-9]{6}', 79 | 'default' => 'ffffff', 80 | 'cast' => 'string' 81 | ) 82 | ) 83 | ) 84 | ); 85 | 86 | /** 87 | * Use Imagine to resize an image and return it's new path 88 | * 89 | * @param string $originalImage 90 | * @param array $arguments 91 | * @param array $imageOptions 92 | * 93 | * @return void 94 | * 95 | * @throws ErrorException 96 | */ 97 | public function doFilter($originalImage, $arguments, $imageOptions) 98 | { 99 | // Need at least a height or a width 100 | if (empty($arguments['height']) && empty($arguments['width'])) { 101 | throw new ErrorException('You must set at least the height (h) or the width (w)'); 102 | } 103 | switch (strtolower($imageOptions['imageProcessor'])) { 104 | case 'gd': 105 | $Imagine = new \Imagine\Gd\Imagine(); 106 | break; 107 | case 'imagick': 108 | $Imagine = new \Imagine\Imagick\Imagine(); 109 | break; 110 | case 'gmagick': 111 | $Imagine = new \Imagine\Gmagick\Imagine(); 112 | break; 113 | default: 114 | throw new ErrorException('Unsupported imageProcessor config value: ' . $imageOptions['imageProcessor']); 115 | } 116 | $image = $Imagine->open($originalImage); 117 | 118 | $size = $image->getSize(); 119 | $originalWidth = $size->getWidth(); 120 | $originalHeight = $size->getHeight(); 121 | $width = $originalWidth; 122 | $height = $originalHeight; 123 | 124 | if (! empty($arguments['height'])) { 125 | if ($originalHeight > $arguments['height'] || $arguments['stretch']) { 126 | $height = $arguments['height']; 127 | } 128 | } 129 | if (! empty($arguments['width'])) { 130 | if ($originalWidth > $arguments['width'] || $arguments['stretch']) { 131 | $width = $arguments['width']; 132 | } 133 | } 134 | 135 | /** 136 | * Prevent from someone from creating huge images 137 | */ 138 | if ($width > $imageOptions['maxAllowedResizeWidth']) { 139 | $width = $imageOptions['maxAllowedResizeWidth']; 140 | } 141 | 142 | if ($height > $imageOptions['maxAllowedResizeHeight']) { 143 | $height = $imageOptions['maxAllowedResizeHeight']; 144 | } 145 | 146 | $mode = $arguments['exact'] ? 147 | ImageInterface::THUMBNAIL_OUTBOUND : 148 | ImageInterface::THUMBNAIL_INSET; 149 | 150 | $newSize = new Box($width, $height); 151 | 152 | $newImage = $image->thumbnail($newSize, $mode); 153 | if ($arguments['fill']) { 154 | $adjustedSize = $newImage->getSize(); 155 | $canvasWidth = isset($arguments['width']) ? $arguments['width'] : $adjustedSize->getWidth(); 156 | $canvasHeight = isset($arguments['height']) ? $arguments['height'] : $adjustedSize->getHeight(); 157 | /** 158 | * Prevent from someone from creating huge images 159 | */ 160 | if ($canvasWidth > $imageOptions['maxAllowedResizeWidth']) { 161 | $canvasWidth = $imageOptions['maxAllowedResizeWidth']; 162 | } 163 | 164 | if ($canvasHeight > $imageOptions['maxAllowedResizeHeight']) { 165 | $canvasHeight = $imageOptions['maxAllowedResizeHeight']; 166 | } 167 | 168 | $palette = new RGB(); 169 | $canvas = $Imagine->create( 170 | new Box($canvasWidth, $canvasHeight), 171 | $palette->color($arguments['fillColour']) 172 | ); 173 | 174 | // Put image in the middle of the canvas 175 | $newImage = $canvas->paste($newImage, new Point( 176 | (int) (($canvasWidth - $adjustedSize->getWidth()) / 2), 177 | (int) (($canvasHeight - $adjustedSize->getHeight()) / 2) 178 | )); 179 | } 180 | 181 | $newImage->save($originalImage, array('jpeg_quality' => $arguments['quality'])); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /tests/Munee/Cases/ResponseTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('Munee\ErrorException'); 33 | new Response(new \stdClass()); 34 | } 35 | 36 | /** 37 | * Test the initial request from a browser. Meaning they haven't requested the file before 38 | * 39 | * @runInSeparateProcess 40 | */ 41 | public function testNonCachedResponse() 42 | { 43 | $Response = new Response(new MockAssetType()); 44 | $Response->setHeaderController(new HeaderSetter()); 45 | $Response->setHeaders(0); 46 | 47 | $checkHeaders = array(); 48 | $checkHeaders['Cache-Control'] = 'max-age=0, must-revalidate'; 49 | $checkHeaders['Content-Type'] = 'text/test'; 50 | $checkHeaders['Last-Modified'] = gmdate('D, d M Y H:i:s', $this->lastModifiedTime) . ' GMT'; 51 | // ETag is MD5 Hash of the content + last modified date 52 | $checkHeaders['ETag'] = '00403b660c8f869d9f50c429f6dceb72'; 53 | 54 | $setHeaders = $this->getHeaders(); 55 | 56 | $this->assertSame($checkHeaders['Cache-Control'], $setHeaders['Cache-Control']); 57 | unset($setHeaders['Cache-Control']); 58 | $this->assertContains($checkHeaders['Content-Type'], $setHeaders['Content-Type']); 59 | unset($setHeaders['Content-Type']); 60 | $this->assertSame($checkHeaders['Last-Modified'], $setHeaders['Last-Modified']); 61 | unset($setHeaders['Last-Modified']); 62 | $this->assertSame($checkHeaders['ETag'], $setHeaders['ETag']); 63 | unset($setHeaders['ETag']); 64 | 65 | $this->assertSame(0, count($setHeaders)); 66 | 67 | $this->assertFalse($Response->notModified); 68 | 69 | header_remove('Cache-Control'); 70 | header_remove('Last-Modified'); 71 | header_remove('ETag'); 72 | header_remove('Content-Type'); 73 | } 74 | 75 | /** 76 | * Test the subsequent request by setting the correct $_SERVER variables and returning a 77 | * 304 response. Unfortunately, xdebug_get_headers() does not return header response codes 78 | * so I am testing a variable instead. 79 | * 80 | * @runInSeparateProcess 81 | */ 82 | public function testCachedResponse() 83 | { 84 | $_SERVER['HTTP_IF_MODIFIED_SINCE'] = gmdate('D, d M Y H:i:s', $this->lastModifiedTime) . ' GMT'; 85 | $_SERVER['HTTP_IF_NONE_MATCH'] = '00403b660c8f869d9f50c429f6dceb72'; 86 | 87 | $Response = new Response(new MockAssetType()); 88 | $Response->setHeaderController(new HeaderSetter()); 89 | $Response->setHeaders(0); 90 | 91 | $checkHeaders = array(); 92 | $setHeaders = $this->getHeaders(); 93 | 94 | $this->assertSame($checkHeaders, $setHeaders); 95 | 96 | $this->assertSame(0, count($setHeaders)); 97 | $this->assertTrue($Response->notModified); 98 | 99 | unset($_SERVER['HTTP_IF_MODIFIED_SINCE']); 100 | unset($_SERVER['HTTP_IF_NONE_MATCH']); 101 | } 102 | 103 | /** 104 | * Test to see if what the browser request headers has is expired and return a new response 105 | * with new caching headers. 106 | * 107 | * @runInSeparateProcess 108 | */ 109 | public function testExpiredRequestResponse() 110 | { 111 | $expiredTime = $this->lastModifiedTime - 100; // removing 100 seconds 112 | $_SERVER['HTTP_IF_MODIFIED_SINCE'] = gmdate('D, d M Y H:i:s', $expiredTime) . ' GMT'; 113 | $_SERVER['HTTP_IF_NONE_MATCH'] = '00000000000000000000000000000000'; 114 | 115 | $Response = new Response(new MockAssetType()); 116 | $Response->setHeaderController(new HeaderSetter()); 117 | $Response->setHeaders(0); 118 | 119 | $checkHeaders = array(); 120 | $checkHeaders['Cache-Control'] = 'max-age=0, must-revalidate'; 121 | $checkHeaders['Content-Type'] = 'text/test'; 122 | $checkHeaders['Last-Modified'] = gmdate('D, d M Y H:i:s', $this->lastModifiedTime) . ' GMT'; 123 | // ETag is MD5 Hash of the content + last modified date 124 | $checkHeaders['ETag'] = '00403b660c8f869d9f50c429f6dceb72'; 125 | 126 | $setHeaders = $this->getHeaders(); 127 | 128 | $this->assertSame($checkHeaders['Cache-Control'], $setHeaders['Cache-Control']); 129 | unset($setHeaders['Cache-Control']); 130 | $this->assertContains($checkHeaders['Content-Type'], $setHeaders['Content-Type']); 131 | unset($setHeaders['Content-Type']); 132 | $this->assertSame($checkHeaders['Last-Modified'], $setHeaders['Last-Modified']); 133 | unset($setHeaders['Last-Modified']); 134 | $this->assertSame($checkHeaders['ETag'], $setHeaders['ETag']); 135 | unset($setHeaders['ETag']); 136 | 137 | $this->assertSame(0, count($setHeaders)); 138 | 139 | $this->assertFalse($Response->notModified); 140 | 141 | header_remove('Cache-Control'); 142 | header_remove('Last-Modified'); 143 | header_remove('ETag'); 144 | header_remove('Content-Type'); 145 | } 146 | 147 | /** 148 | * Make sure rendering is working properly 149 | */ 150 | public function testRender() 151 | { 152 | $Response = new Response(new MockAssetType()); 153 | 154 | $this->assertSame('foo', $Response->render()); 155 | } 156 | 157 | /** 158 | * Helper function to get the current headers set and weed out the ones that don't have a value 159 | * 160 | * @return array 161 | */ 162 | protected function getHeaders() 163 | { 164 | $rawHeaders = xdebug_get_headers(); 165 | $ret = array(); 166 | foreach ($rawHeaders as $header) { 167 | $headerParts = explode(':', $header, 2); 168 | if (2 == count($headerParts)) { 169 | if ($headerParts[0] === 'Content-type') { 170 | // xdebug incompatible naming 171 | $headerParts[0] = 'Content-Type'; 172 | } 173 | $ret[$headerParts[0]] = trim($headerParts[1]); 174 | } elseif (isset($ret[$headerParts[0]])) { 175 | // If a header param is empty, make sure others with the same name are not set as well 176 | unset($ret[$headerParts[0]]); 177 | } 178 | } 179 | 180 | return $ret; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Munee/Asset/Type/Image.php: -------------------------------------------------------------------------------- 1 | 3, 30 | // Number of seconds - default is 5 minutes 31 | 'allowedFiltersTimeLimit' => 300, 32 | // Should the referrer be checked for security 33 | 'checkReferrer' => true, 34 | // Use a placeholder for missing images? 35 | 'placeholders' => false, 36 | 'maxAllowedResizeWidth' => 1920, 37 | 'maxAllowedResizeHeight' => 1080, 38 | /** 39 | * Can easily change which image processor to use. Values can be: 40 | * GD - Default 41 | * Imagick 42 | * Gmagick 43 | */ 44 | 'imageProcessor' => 'GD' 45 | ); 46 | 47 | /** 48 | * Stores the specific placeholder that will be used for this requested asset, if any. 49 | * 50 | * @var bool 51 | */ 52 | protected $placeholder = false; 53 | 54 | /** 55 | * Checks to see if cache exists and is the latest, if it does, return it 56 | * 57 | * Extra security checks for images 58 | * 59 | * @param string $originalFile 60 | * @param string $cacheFile 61 | * 62 | * @return bool|string 63 | */ 64 | protected function checkCache($originalFile, $cacheFile) 65 | { 66 | if (! $return = parent::checkCache($originalFile, $cacheFile)) { 67 | /** 68 | * If using the placeholder when the original file doesn't exist 69 | * and it has already been cached, return the cached contents. 70 | * Also make sure the placeholder hasn't been modified since being cached. 71 | */ 72 | $this->placeholder = $this->parsePlaceholders($originalFile); 73 | if (! file_exists($originalFile) && $this->placeholder) { 74 | return parent::checkCache($this->placeholder, $cacheFile); 75 | } 76 | } 77 | 78 | return $return; 79 | } 80 | 81 | /** 82 | * Overwrite the _setupFile function so placeholder images can be shown instead of broken images 83 | * 84 | * 85 | * @param string $originalFile 86 | * @param string $cacheFile 87 | */ 88 | protected function setupFile($originalFile, $cacheFile) 89 | { 90 | if (count($this->filters) > 0) { 91 | $this->checkNumberOfAllowedFilters($cacheFile); 92 | if ($this->options['checkReferrer']) { 93 | $this->checkReferrer(); 94 | } 95 | } 96 | 97 | if (! file_exists($originalFile)) { 98 | // If we are using a placeholder and that exists, use it! 99 | if ($this->placeholder && file_exists($this->placeholder)) { 100 | $originalFile = $this->placeholder; 101 | } 102 | } 103 | 104 | parent::setupFile($originalFile, $cacheFile); 105 | } 106 | 107 | /** 108 | * Set additional headers just for an Image 109 | */ 110 | public function getHeaders() 111 | { 112 | switch ($this->request->ext) { 113 | case 'jpg': 114 | case 'jpeg': 115 | $this->response->headerController->headerField('Content-Type', 'image/jpeg'); 116 | break; 117 | case 'png': 118 | $this->response->headerController->headerField('Content-Type', 'image/png'); 119 | break; 120 | case 'gif': 121 | $this->response->headerController->headerField('Content-Type', 'image/gif'); 122 | break; 123 | } 124 | } 125 | 126 | /** 127 | * Check to make sure the referrer domain is the same as the domain where the image exists. 128 | * 129 | * @throws ErrorException 130 | */ 131 | protected function checkReferrer() 132 | { 133 | if (! isset($_SERVER['HTTP_REFERER'])) { 134 | throw new ErrorException('Direct image manipulation is not allowed.'); 135 | } 136 | 137 | $referrer = preg_replace('%^https?://%', '', $_SERVER['HTTP_REFERER']); 138 | if (! preg_match("%^{$_SERVER['SERVER_NAME']}%", $referrer)) { 139 | throw new ErrorException('Referrer does not match the correct domain.'); 140 | } 141 | } 142 | 143 | /** 144 | * Check number of allowed resizes within a set time limit 145 | * 146 | * @param string $cacheFile 147 | * 148 | * @throws ErrorException 149 | */ 150 | protected function checkNumberOfAllowedFilters($cacheFile) 151 | { 152 | $pathInfo = pathinfo($cacheFile); 153 | $fileNameHash = preg_replace('%-.*$%', '', $pathInfo['filename']); 154 | // Grab all the similar files 155 | $cachedImages = glob($pathInfo['dirname'] . DS . $fileNameHash . '*'); 156 | 157 | if (! is_array($cachedImages)) { 158 | $cachedImages = array(); 159 | } 160 | 161 | // Loop through and remove the ones that are older than the time limit 162 | foreach ($cachedImages as $k => $image) { 163 | if (filemtime($image) < time() - $this->options['allowedFiltersTimeLimit']) { 164 | unset($cachedImages[$k]); 165 | } 166 | } 167 | 168 | // Check and see if we've reached the maximum allowed resizes within the current time limit. 169 | if (count($cachedImages) >= $this->options['numberOfAllowedFilters']) { 170 | throw new ErrorException('You cannot create anymore resizes/manipulations at this time.'); 171 | } 172 | } 173 | 174 | /** 175 | * Checks the 'placeholders' Request Option to see if placeholders should be used for missing images 176 | * It uses a wildcard syntax (*) to see which placeholder should be used for a particular set of images. 177 | * 178 | * @param string $file 179 | * 180 | * @return boolean|string 181 | * 182 | * @throws ErrorException 183 | */ 184 | protected function parsePlaceholders($file) 185 | { 186 | $ret = false; 187 | if (! empty($this->options['placeholders'])) { 188 | // If it's a string, use the image for all missing images. 189 | if (is_string($this->options['placeholders'])) { 190 | $this->options['placeholders'] = array('*' => $this->options['placeholders']); 191 | } 192 | 193 | foreach ($this->options['placeholders'] as $path => $placeholder) { 194 | // Setup path for regex 195 | $escapedWebroot = preg_quote($this->request->webroot); 196 | $regex = '^' . $escapedWebroot . str_replace(array('*', $this->request->webroot), array('.*?', ''), $path) . '$'; 197 | if (preg_match("%{$regex}%", $file)) { 198 | if ('http' == substr($placeholder, 0, 4)) { 199 | $ret = $this->getImageByUrl($placeholder); 200 | } else { 201 | $ret = $placeholder; 202 | } 203 | break; 204 | } 205 | } 206 | } 207 | 208 | return $ret; 209 | } 210 | 211 | /** 212 | * Grabs an image by URL from another server 213 | * 214 | * @param string $url 215 | * 216 | * @return string 217 | */ 218 | protected function getImageByUrl($url) 219 | { 220 | $cacheFolder = MUNEE_CACHE . DS . 'placeholders'; 221 | Utils::createDir($cacheFolder); 222 | $requestOptions = serialize($this->request->options); 223 | $originalFile = array_shift($this->request->files); 224 | 225 | $fileName = $cacheFolder . DS . md5($url) . '-' . md5($requestOptions . $originalFile); 226 | if (! file_exists($fileName)) { 227 | file_put_contents($fileName, file_get_contents($url)); 228 | } 229 | 230 | return $fileName; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/Munee/Asset/Type/Css.php: -------------------------------------------------------------------------------- 1 | false, 34 | 'scssifyAllCss' => false 35 | ); 36 | 37 | /** 38 | * Set additional headers just for CSS 39 | */ 40 | public function getHeaders() 41 | { 42 | $this->response->headerController->headerField('Content-Type', 'text/css'); 43 | } 44 | 45 | /** 46 | * Checks to see if cache exists and is the latest, if it does, return it 47 | * It also checks to see if this is LESS cache and makes sure all imported files are the latest 48 | * 49 | * @param string $originalFile 50 | * @param string $cacheFile 51 | * 52 | * @return bool|string|array 53 | */ 54 | protected function checkCache($originalFile, $cacheFile) 55 | { 56 | if (! $ret = parent::checkCache($originalFile, $cacheFile)) { 57 | return false; 58 | } 59 | 60 | if (Utils::isSerialized($ret, $ret)) { 61 | // Go through each file and make sure none of them have changed 62 | foreach ($ret['files'] as $file => $lastModified) { 63 | if (filemtime($file) > $lastModified) { 64 | return false; 65 | } 66 | } 67 | 68 | $ret = serialize($ret); 69 | } 70 | 71 | return $ret; 72 | } 73 | 74 | /** 75 | * Callback method called before filters are run 76 | * 77 | * Overriding to run the file through LESS/SCSS if need be. 78 | * Also want to fix any relative paths for images. 79 | * 80 | * @param string $originalFile 81 | * @param string $cacheFile 82 | * 83 | * @throws CompilationException 84 | */ 85 | protected function beforeFilter($originalFile, $cacheFile) 86 | { 87 | if ($this->isLess($originalFile)) { 88 | $less = new lessc(); 89 | try { 90 | $compiledLess = $less->cachedCompile($originalFile); 91 | } catch (\Exception $e) { 92 | throw new CompilationException('Error in LESS Compiler', 0, $e); 93 | } 94 | $compiledLess['compiled'] = $this->fixRelativePaths($compiledLess['compiled'], $originalFile); 95 | file_put_contents($cacheFile, serialize($compiledLess)); 96 | } elseif ($this->isScss($originalFile)) { 97 | $scss = new ScssCompiler(); 98 | $scss->addImportPath(pathinfo($originalFile, PATHINFO_DIRNAME)); 99 | try { 100 | $compiled = $scss->compile(file_get_contents($originalFile)); 101 | } catch (\Exception $e) { 102 | throw new CompilationException('Error in SCSS Compiler', 0, $e); 103 | } 104 | 105 | $content = compact('compiled'); 106 | $parsedFiles = $scss->getParsedFiles(); 107 | $parsedFiles[] = $originalFile; 108 | foreach ($parsedFiles as $file) { 109 | $content['files'][$file] = filemtime($file); 110 | } 111 | 112 | $content['compiled'] = $this->fixRelativePaths($content['compiled'], $originalFile); 113 | file_put_contents($cacheFile, serialize($content)); 114 | } else { 115 | $content = file_get_contents($originalFile); 116 | file_put_contents($cacheFile, $this->fixRelativePaths($content, $originalFile)); 117 | } 118 | } 119 | 120 | /** 121 | * Callback method called after the content is collected and/or cached 122 | * Check if the content is serialized. If it is, we have LESS cache 123 | * and we want to return whats in the `compiled` array key 124 | * 125 | * @param string $content 126 | * 127 | * @return string 128 | */ 129 | protected function afterGetFileContent($content) 130 | { 131 | if (Utils::isSerialized($content, $content)) { 132 | $content = $content['compiled']; 133 | } 134 | 135 | return $content; 136 | } 137 | 138 | /** 139 | * Check if it's a LESS file or if we should run all CSS through LESS 140 | * 141 | * @param string $file 142 | * 143 | * @return boolean 144 | */ 145 | protected function isLess($file) 146 | { 147 | return 'less' == pathinfo($file, PATHINFO_EXTENSION) || $this->options['lessifyAllCss']; 148 | } 149 | 150 | /** 151 | * Check if it's a SCSS file or if we should run all CSS through SCSS 152 | * 153 | * @param string $file 154 | * 155 | * @return boolean 156 | */ 157 | protected function isScss($file) 158 | { 159 | return 'scss' == pathinfo($file, PATHINFO_EXTENSION) || $this->options['scssifyAllCss']; 160 | } 161 | 162 | /** 163 | * Use CssParser to go through and convert all relative paths to absolute 164 | * 165 | * @param string $content 166 | * @param string $originalFile 167 | * 168 | * @return string 169 | */ 170 | protected function fixRelativePaths($content, $originalFile) 171 | { 172 | $cssParserSettings = CssSettings::create()->withMultibyteSupport(false); 173 | $cssParser = new CssParser($content, $cssParserSettings); 174 | $cssDocument = $cssParser->parse(); 175 | 176 | $cssBlocks = $cssDocument->getAllValues(); 177 | 178 | $this->fixUrls($cssBlocks, $originalFile); 179 | 180 | return $cssDocument->render(); 181 | } 182 | 183 | /** 184 | * Recursively go through the CSS Blocks and update relative links to absolute 185 | * 186 | * @param $cssBlocks 187 | * @param $originalFile 188 | * @throws CompilationException 189 | */ 190 | protected function fixUrls($cssBlocks, $originalFile) { 191 | foreach ($cssBlocks as $cssBlock) { 192 | if ($cssBlock instanceof Import) { 193 | $this->fixUrls($cssBlock->atRuleArgs(), $originalFile); 194 | } else { 195 | if (! $cssBlock instanceof URL) { 196 | continue; 197 | } 198 | 199 | $originalUrl = $cssBlock->getURL()->getString(); 200 | $url = $this->relativeToAbsolute($originalUrl, $originalFile); 201 | $cssBlock->getURL()->setString($url); 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * Convert the passed in url from relative to absolute taking care not to convert urls that are already 208 | * absolute, point to a different domain/protocol, or are base64 encoded "data:image" strings. 209 | * It will also prefix a url with the munee dispatcher file URL if *not* using URL Rewrites (.htaccess). 210 | * 211 | * @param $originalUrl 212 | * @param $originalFile 213 | * 214 | * @return string 215 | * @throws CompilationException 216 | */ 217 | protected function relativeToAbsolute($originalUrl, $originalFile) 218 | { 219 | $webroot = $this->request->webroot; 220 | $url = $originalUrl; 221 | if ( 222 | $originalUrl[0] !== '/' && 223 | strpos($originalUrl, '://') === false && 224 | strpos($originalUrl, 'data:image') === false 225 | ) { 226 | $basePath = SUB_FOLDER . str_replace($webroot, '', dirname($originalFile)); 227 | $basePathParts = array_reverse(array_filter(explode('/', $basePath))); 228 | $numOfRecursiveDirs = substr_count($originalUrl, '../'); 229 | if ($numOfRecursiveDirs > count($basePathParts)) { 230 | throw new CompilationException( 231 | 'Error in stylesheet ' . $originalFile . 232 | '. The following URL goes above webroot: ' . $url . '' 233 | ); 234 | } 235 | 236 | $basePathParts = array_slice($basePathParts, $numOfRecursiveDirs); 237 | $basePath = implode('/', array_reverse($basePathParts)); 238 | 239 | if (! empty($basePath) && $basePath[0] != '/') { 240 | $basePath = '/' . $basePath; 241 | } 242 | 243 | $url = $basePath . '/' . $originalUrl; 244 | $url = str_replace(array('../', './'), '', $url); 245 | } 246 | 247 | // If not using URL Rewrite 248 | if (! MUNEE_USING_URL_REWRITE) { 249 | $dispatcherUrl = MUNEE_DISPATCHER_FILE . '?files='; 250 | // If url is not already pointing to munee dispatcher file, 251 | // isn't pointing to another domain/protocol, 252 | // and isn't using data:image 253 | if ( 254 | strpos($url, $dispatcherUrl) !== 0 && 255 | strpos($originalUrl, '://') === false && 256 | strpos($originalUrl, 'data:image') === false 257 | ) { 258 | $url = str_replace('?', '&', $url); 259 | $url = $dispatcherUrl . $url; 260 | } 261 | } 262 | 263 | return $url; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Munee/Asset/Type.php: -------------------------------------------------------------------------------- 1 | request = $Request; 89 | 90 | // Pull in filters based on the raw params that were passed in 91 | $rawParams = $Request->getRawParams(); 92 | $assetShortName = preg_replace('%^.*\\\\%','', get_class($this)); 93 | $allowedParams = array(); 94 | foreach (array_keys($rawParams) as $filterName) { 95 | $filterClass = 'Munee\\Asset\\Filter\\' . $assetShortName . '\\' . ucfirst($filterName); 96 | if (class_exists($filterClass)) { 97 | $Filter = new $filterClass(); 98 | $allowedParams += $Filter->getAllowedParams(); 99 | $this->filters[$filterName] = $Filter; 100 | } 101 | } 102 | 103 | // Parse the raw params based on a map of allowedParams for those filters 104 | $this->request->parseParams($allowedParams); 105 | 106 | $this->cacheDir = MUNEE_CACHE . DS . $assetShortName; 107 | 108 | $optionsKey = strtolower($assetShortName); 109 | // Set the AssetType options if someone were passed in through the Request Class 110 | if (isset($this->request->options[$optionsKey])) { 111 | $this->options = array_merge( 112 | $this->options, 113 | $this->request->options[$optionsKey] 114 | ); 115 | } 116 | 117 | // Create cache dir if needed 118 | Utils::createDir($this->cacheDir); 119 | } 120 | 121 | /** 122 | * Process all files in the request and set the content 123 | */ 124 | public function init() 125 | { 126 | $content = array(); 127 | foreach ($this->request->files as $file) { 128 | $cacheFile = $this->generateCacheFile($file); 129 | 130 | if (! $fileContent = $this->checkCache($file, $cacheFile)) { 131 | $this->setupFile($file, $cacheFile); 132 | $fileContent = $this->getFileContent($file, $cacheFile); 133 | } 134 | 135 | $content[] = $this->afterGetFileContent($fileContent); 136 | } 137 | 138 | $this->content = implode("\n", $content); 139 | } 140 | 141 | /** 142 | * Sets the Munee\Response class to the AssetType 143 | * 144 | * @param $Response 145 | * 146 | * @throws \Munee\ErrorException 147 | */ 148 | public function setResponse($Response) 149 | { 150 | if (! $Response instanceof Response) { 151 | throw new ErrorException('Response class must be an instance of Munee\Response'); 152 | } 153 | 154 | $this->response = $Response; 155 | } 156 | 157 | /** 158 | * Grabs the content for the Response class 159 | * 160 | * @return string 161 | */ 162 | public function getContent() 163 | { 164 | return $this->content; 165 | } 166 | 167 | /** 168 | * Return a this requests Last Modified Date. 169 | * 170 | * @return integer timestamp 171 | */ 172 | public function getLastModifiedDate() 173 | { 174 | return $this->lastModifiedDate; 175 | } 176 | 177 | /** 178 | * If an exception is handled this function will fire and clean up any files 179 | * that have been cached as they have not properly compiled. 180 | */ 181 | public function cleanUpAfterError() 182 | { 183 | foreach ($this->request->files as $file) { 184 | $cacheFile = $this->generateCacheFile($file); 185 | unlink($cacheFile); 186 | } 187 | } 188 | 189 | /** 190 | * Callback method called before filters are run 191 | * 192 | * @param string $originalFile 193 | * @param string $cacheFile 194 | */ 195 | protected function beforeFilter($originalFile, $cacheFile) {} 196 | 197 | /** 198 | * Callback function called after filters are run 199 | * 200 | * @param string $originalFile 201 | * @param string $cacheFile 202 | */ 203 | protected function afterFilter($originalFile, $cacheFile) {} 204 | 205 | /** 206 | * Callback function called after _getFileContent() is called 207 | * 208 | * @param string $content 209 | * 210 | * @return string 211 | */ 212 | protected function afterGetFileContent($content) 213 | { 214 | return $content; 215 | } 216 | 217 | /** 218 | * Checks to see if the file exists and then copies it to the cache folder for further manipulation 219 | * 220 | * @param $originalFile 221 | * @param $cacheFile 222 | * 223 | * @throws NotFoundException 224 | */ 225 | protected function setupFile($originalFile, $cacheFile) 226 | { 227 | // Check if the file exists 228 | if (! file_exists($originalFile)) { 229 | throw new NotFoundException('File does not exist: ' . str_replace($this->request->webroot, '', $originalFile)); 230 | } 231 | 232 | // Copy the original file to the cache location 233 | copy($originalFile, $cacheFile); 234 | } 235 | 236 | /** 237 | * Grab a files content but check to make sure it exists first 238 | * 239 | * @param string $originalFile 240 | * @param string $cacheFile 241 | * 242 | * @return string 243 | * 244 | * @throws NotFoundException 245 | */ 246 | protected function getFileContent($originalFile, $cacheFile) 247 | { 248 | $this->beforeFilter($originalFile, $cacheFile); 249 | // Run through each filter 250 | foreach ($this->filters as $filterName => $Filter) { 251 | $arguments = isset($this->request->params[$filterName]) ? 252 | $this->request->params[$filterName] : array(); 253 | if (! is_array($arguments)) { 254 | $arguments = array($filterName => $arguments); 255 | } 256 | // Do not minify if .min. is in the filename as it has already been minified 257 | if(strpos($originalFile, '.min.') !== FALSE) { 258 | $arguments['minify'] = false; 259 | } 260 | $Filter->doFilter($cacheFile, $arguments, $this->options); 261 | } 262 | 263 | $this->afterFilter($originalFile, $cacheFile); 264 | $this->lastModifiedDate = time(); 265 | 266 | return file_get_contents($cacheFile); 267 | } 268 | 269 | /** 270 | * Checks to see if cache exists and is the latest, if it does, return it 271 | * 272 | * @param string $originalFile 273 | * @param string $cacheFile 274 | * 275 | * @return bool|string 276 | */ 277 | protected function checkCache($originalFile, $cacheFile) 278 | { 279 | if (! file_exists($cacheFile) || ! file_exists($originalFile)) { 280 | return false; 281 | } 282 | 283 | $cacheFileLastModified = filemtime($cacheFile); 284 | if (filemtime($originalFile) > $cacheFileLastModified) { 285 | return false; 286 | } 287 | 288 | if ($this->lastModifiedDate < $cacheFileLastModified) { 289 | $this->lastModifiedDate = $cacheFileLastModified; 290 | } 291 | 292 | return file_get_contents($cacheFile); 293 | } 294 | 295 | /** 296 | * Generate File Name Hash based on filename, request params and request options 297 | * 298 | * @param string $file 299 | * 300 | * @return string 301 | */ 302 | protected function generateCacheFile($file) 303 | { 304 | $cacheSalt = serialize(array( 305 | $this->request->options, 306 | MUNEE_USING_URL_REWRITE, 307 | MUNEE_DISPATCHER_FILE 308 | )); 309 | $params = serialize($this->request->params); 310 | $ext = pathinfo($file, PATHINFO_EXTENSION); 311 | 312 | $fileHash = md5($file); 313 | $optionsHash = md5($params . $cacheSalt); 314 | 315 | $cacheDir = $this->cacheDir . DS . substr($fileHash, 0, 2); 316 | 317 | Utils::createDir($cacheDir); 318 | 319 | return $cacheDir . DS . substr($fileHash, 2) . '-' . $optionsHash . '.' . $ext; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/Munee/Request.php: -------------------------------------------------------------------------------- 1 | options = $options; 85 | $this->rawFiles = isset($_GET['files']) ? $_GET['files'] : ''; 86 | unset($_GET['files']); 87 | 88 | $this->rawParams = $_GET; 89 | } 90 | 91 | /** 92 | * Sets the document root. 93 | * 94 | * @param string $path 95 | * 96 | * @return object 97 | */ 98 | public function setWebroot($path) 99 | { 100 | $this->webroot = $path; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Sets either an individual _rawParams key - or overwrites the whole array. 107 | * 108 | * @param mixed $key 109 | * @param mixed $value 110 | * 111 | * @return object 112 | */ 113 | public function setRawParam($key, $value = null) 114 | { 115 | if (is_array($key)) { 116 | $this->rawParams = $key; 117 | } else { 118 | $this->rawParams[$key] = $value; 119 | } 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Returns the pre-parsed raw params 126 | * 127 | * @return array 128 | */ 129 | public function getRawParams() 130 | { 131 | return $this->rawParams; 132 | } 133 | 134 | /** 135 | * Sets the $rawFiles. 136 | * 137 | * @param string $files 138 | * 139 | * @return object 140 | */ 141 | public function setFiles($files) 142 | { 143 | $this->rawFiles = $files; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Parses the $rawFiles and does sanity checks 150 | * 151 | * @throws ErrorException 152 | * @throws Asset\NotFoundException 153 | */ 154 | public function init() 155 | { 156 | if (empty($this->rawFiles)) { 157 | throw new ErrorException('No file specified; make sure you are using the correct .htaccess rules.'); 158 | } 159 | 160 | // Handle legacy code for minifying 161 | if (preg_match('%^/minify/%', $this->rawFiles)) { 162 | $this->rawFiles = substr($this->rawFiles, 7); 163 | $this->setRawParam('minify', 'true'); 164 | } 165 | 166 | $this->ext = strtolower(pathinfo($this->rawFiles, PATHINFO_EXTENSION)); 167 | $supportedExtensions = Registry::getSupportedExtensions($this->ext); 168 | // Suppressing errors because Exceptions thrown in the callback cause Warnings. 169 | $webroot = $this->webroot; 170 | $this->files = @array_map(function($v) use ($supportedExtensions, $webroot) { 171 | // Make sure all the file extensions are supported 172 | if (! in_array(strtolower(pathinfo($v, PATHINFO_EXTENSION)), $supportedExtensions)) { 173 | throw new ErrorException('All requested files need to be: ' . implode(', ', $supportedExtensions)); 174 | } 175 | // Strip any parent directory slugs (../) - loop through until they are all gone 176 | $count = 1; 177 | while ($count > 0) { 178 | $v = preg_replace('%(/\\.\\.?|\\.\\.?/)%', '', $v, -1, $count); 179 | // If there is no slash prefix, add it back in 180 | if (substr($v, 0, 1) != '/') { 181 | $v = '/' . $v; 182 | } 183 | } 184 | 185 | // Remove sub-folder if in the path, it shouldn't be there. 186 | $v = str_replace(SUB_FOLDER, '', $v); 187 | 188 | return $webroot . $v; 189 | }, explode(',', $this->rawFiles)); 190 | } 191 | 192 | /** 193 | * Parse query string parameter arguments based on mapped allowed params 194 | * 195 | * @param array $allowedParams 196 | */ 197 | public function parseParams($allowedParams) 198 | { 199 | $this->allowedParams = $allowedParams; 200 | $this->setDefaultParams(); 201 | 202 | foreach ($this->rawParams as $checkParam => $value) { 203 | if (! $paramOptions = $this->getParamOptions($checkParam)) { 204 | continue; 205 | } 206 | 207 | $param = $paramOptions['param']; 208 | $options = $paramOptions['options']; 209 | 210 | $paramValue = $this->getParamValue($param, $options, $value); 211 | if (isset($this->params[$param]) && is_array($this->params[$param])) { 212 | $this->params[$param] = array_merge($this->params[$param], $paramValue); 213 | } else { 214 | $this->params[$param] = $paramValue; 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * Setup the default values for the allowed parameters 221 | */ 222 | protected function setDefaultParams() 223 | { 224 | foreach ($this->allowedParams as $param => $options) { 225 | $this->params[$param] = null; 226 | if (! empty($options['arguments'])) { 227 | $this->params[$param] = array(); 228 | foreach ($options['arguments'] as $arg => $opts) { 229 | if (! empty($opts['default'])) { 230 | $cast = ! empty($opts['cast']) ? $opts['cast'] : 'string'; 231 | $this->params[$param][$arg] = $this->castValue($cast, $opts['default']); 232 | } 233 | } 234 | } elseif (! empty($options['default'])) { 235 | $cast = ! empty($options['cast']) ? $options['cast'] : 'string'; 236 | $this->params[$param] = $this->castValue($cast, $options['default']); 237 | } 238 | } 239 | } 240 | 241 | 242 | /** 243 | * Grabs the params options taking into account any aliases 244 | * 245 | * @param $checkParam 246 | * 247 | * @return bool|array 248 | */ 249 | protected function getParamOptions($checkParam) 250 | { 251 | if (isset($this->allowedParams[$checkParam])) { 252 | return array('param' => $checkParam, 'options' => $this->allowedParams[$checkParam]); 253 | } else { 254 | foreach ($this->allowedParams as $param => $options) { 255 | if (! empty($options['alias']) && in_array($checkParam, (array) $options['alias'])) { 256 | return compact('param', 'options'); 257 | } 258 | } 259 | } 260 | 261 | return false; 262 | } 263 | 264 | 265 | /** 266 | * Grabs a value from a param by running it through supplied regex 267 | * 268 | * @param $param 269 | * @param $paramOptions 270 | * @param $value 271 | * 272 | * @return string|array 273 | * 274 | * @throws \Munee\ErrorException 275 | */ 276 | protected function getParamValue($param, $paramOptions, $value) 277 | { 278 | if (! empty($paramOptions['arguments'])) { 279 | $ret = array(); 280 | foreach ($paramOptions['arguments'] as $arg => $opts) { 281 | $p = $arg; 282 | if (! empty($opts['alias'])) { 283 | $alias = implode('|', (array) $opts['alias']); 284 | $p .= "|\\b{$alias}"; 285 | } 286 | $regex = "(\\b{$p})\\[(.*?)\\]"; 287 | if (preg_match("%{$regex}%", $value, $match)) { 288 | $ret[$arg] = $this->getParamValue($arg, $opts, $match[2]); 289 | } 290 | } 291 | 292 | return $ret; 293 | } else { 294 | // Using RegEx? 295 | if (! empty($paramOptions['regex'])) { 296 | if (! preg_match("%^(?:{$paramOptions['regex']})$%", $value)) { 297 | throw new ErrorException("'{$value}' is not a valid value for: {$param}"); 298 | } 299 | } 300 | 301 | $cast = ! empty($paramOptions['cast']) ? $paramOptions['cast'] : 'string'; 302 | 303 | return $this->castValue($cast, $value); 304 | } 305 | } 306 | 307 | 308 | /** 309 | * Helper function to cast values 310 | * 311 | * @param $cast 312 | * @param $value 313 | * 314 | * @return bool|int|string 315 | */ 316 | protected function castValue($cast, $value) 317 | { 318 | switch ($cast) { 319 | case 'integer'; 320 | $value = (integer) $value; 321 | break; 322 | case 'boolean'; 323 | $value = in_array($value, array('true', 't', 'yes', 'y')); 324 | break; 325 | case 'string': 326 | default: 327 | $value = (string) $value; 328 | } 329 | 330 | return $value; 331 | } 332 | } --------------------------------------------------------------------------------