├── 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 | }
--------------------------------------------------------------------------------