diff --git a/app/Config/bootstrap.php b/app/Config/bootstrap.php index 4c54746488d..9d5a51c5f56 100644 --- a/app/Config/bootstrap.php +++ b/app/Config/bootstrap.php @@ -122,3 +122,25 @@ * CakePlugin::load('DebugKit'); //Loads a single plugin named DebugKit * */ + + +/** + * You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters: + * + * - AssetDispatcher filter will serve your asset files (css, images, js, etc) from your themes and plugins + * - CacheDispatcher filter will read the Cache.check configure variable and try to serve cached content generated from controllers + * + * Feel free to remove or add filters as you see fit for your application. A few examples: + * + * Configure::write('Dispatcher.filters', array( + * 'MyCacheFilter', // will use MyCacheFilter class from the Routing/Filter package in your app. + * 'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin. + * array('callbale' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch + * array('callbale' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch + * + * )); + */ +Configure::write('Dispatcher.filters', array( + 'AssetDispatcher', + 'CacheDispatcher' +)); diff --git a/lib/Cake/Console/Templates/skel/Config/bootstrap.php b/lib/Cake/Console/Templates/skel/Config/bootstrap.php index 78ac2896e0a..b2087767ed9 100644 --- a/lib/Cake/Console/Templates/skel/Config/bootstrap.php +++ b/lib/Cake/Console/Templates/skel/Config/bootstrap.php @@ -63,3 +63,24 @@ * CakePlugin::load('DebugKit'); //Loads a single plugin named DebugKit * */ + +/** + * You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters: + * + * - AssetDispatcher filter will serve your asset files (css, images, js, etc) from your themes and plugins + * - CacheDispatcher filter will read the Cache.check configure variable and try to serve cached content generated from controllers + * + * Feel free to remove or add filters as you see fit for your application. A few examples: + * + * Configure::write('Dispatcher.filters', array( + * 'MyCacheFilter', // will use MyCacheFilter class from the Routing/Filter package in your app. + * 'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin. + * array('callbale' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch + * array('callbale' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch + * + * )); + */ +Configure::write('Dispatcher.filters', array( + 'AssetDispatcher', + 'CacheDispatcher' +)); \ No newline at end of file diff --git a/lib/Cake/Routing/Dispatcher.php b/lib/Cake/Routing/Dispatcher.php index 3e77a456605..3e30711ced1 100644 --- a/lib/Cake/Routing/Dispatcher.php +++ b/lib/Cake/Routing/Dispatcher.php @@ -79,13 +79,7 @@ public function getEventManager() { * @return array **/ public function implementedEvents() { - return array( - 'Dispatcher.beforeDispatch' => array( - array('callable' => array($this, 'asset')), - array('callable' => array($this, 'cached')), - array('callable' => array($this, 'parseParams')), - ) - ); + return array('Dispatcher.beforeDispatch' => 'parseParams'); } /** @@ -283,141 +277,4 @@ protected function _loadRoutes() { include APP . 'Config' . DS . 'routes.php'; } -/** - * Checks whether the response was cached and set the body accordingly. - * - * @param CakeEvent $event containing the request and response object - * @return CakeResponse with cached content if found, null otherwise - */ - public function cached($event) { - $path = $event->data['request']->here(); - if (Configure::read('Cache.check') === true) { - if ($path == '/') { - $path = 'home'; - } - $path = strtolower(Inflector::slug($path)); - - $filename = CACHE . 'views' . DS . $path . '.php'; - - if (!file_exists($filename)) { - $filename = CACHE . 'views' . DS . $path . '_index.php'; - } - if (file_exists($filename)) { - $controller = null; - $view = new View($controller); - $result = $view->renderCache($filename, microtime(true)); - if ($result !== false) { - $event->data['response']->body($result); - return $event->data['response']; - } - } - } - } - -/** - * Checks if a requested asset exists and sends it to the browser - * - * @param CakeEvent $event containing the request and response object - * @return CakeResponse if the client is requesting a recognized asset, null otherwise - */ - public function asset($event) { - $url = $event->data['request']->url; - $response = $event->data['response']; - - if (strpos($url, '..') !== false || strpos($url, '.') === false) { - return; - } - - $filters = Configure::read('Asset.filter'); - $isCss = ( - strpos($url, 'ccss/') === 0 || - preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?statusCode(404); - $event->stopPropagation(); - return $response; - } elseif ($isCss) { - include WWW_ROOT . DS . $filters['css']; - $event->stopPropagation(); - return $response; - } elseif ($isJs) { - include WWW_ROOT . DS . $filters['js']; - $event->stopPropagation(); - return $response; - } - - $pathSegments = explode('.', $url); - $ext = array_pop($pathSegments); - $parts = explode('/', $url); - $assetFile = null; - - if ($parts[0] === 'theme') { - $themeName = $parts[1]; - unset($parts[0], $parts[1]); - $fileFragment = urldecode(implode(DS, $parts)); - $path = App::themePath($themeName) . 'webroot' . DS; - if (file_exists($path . $fileFragment)) { - $assetFile = $path . $fileFragment; - } - } else { - $plugin = Inflector::camelize($parts[0]); - if (CakePlugin::loaded($plugin)) { - unset($parts[0]); - $fileFragment = urldecode(implode(DS, $parts)); - $pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS; - if (file_exists($pluginWebroot . $fileFragment)) { - $assetFile = $pluginWebroot . $fileFragment; - } - } - } - - if ($assetFile !== null) { - $event->stopPropagation(); - $this->_deliverAsset($response, $assetFile, $ext); - return $response; - } - } - -/** - * Sends an asset file to the client - * - * @param CakeResponse $response The response object to use. - * @param string $assetFile Path to the asset file in the file system - * @param string $ext The extension of the file to determine its mime type - * @return void - */ - protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) { - ob_start(); - $compressionEnabled = Configure::read('Asset.compress') && $response->compress(); - if ($response->type($ext) == $ext) { - $contentType = 'application/octet-stream'; - $agent = env('HTTP_USER_AGENT'); - if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { - $contentType = 'application/octetstream'; - } - $response->type($contentType); - } - if (!$compressionEnabled) { - $response->header('Content-Length', filesize($assetFile)); - } - $response->cache(filemtime($assetFile)); - $response->send(); - ob_clean(); - if ($ext === 'css' || $ext === 'js') { - include $assetFile; - } else { - readfile($assetFile); - } - - if ($compressionEnabled) { - ob_end_flush(); - } - } - } diff --git a/lib/Cake/Routing/DispatcherFilter.php b/lib/Cake/Routing/DispatcherFilter.php index 057c1e525f4..ae8509799f4 100644 --- a/lib/Cake/Routing/DispatcherFilter.php +++ b/lib/Cake/Routing/DispatcherFilter.php @@ -23,7 +23,7 @@ * event listener with the ability to alter the request or response as needed before it is handled * by a controller or after the response body has already been built. * - * @package Cake.Event + * @package Cake.Routing */ abstract class DispatcherFilter implements CakeEventListener { diff --git a/lib/Cake/Routing/Filter/AssetDispatcher.php b/lib/Cake/Routing/Filter/AssetDispatcher.php new file mode 100644 index 00000000000..f1be060f21e --- /dev/null +++ b/lib/Cake/Routing/Filter/AssetDispatcher.php @@ -0,0 +1,156 @@ +data['request']->url; + $response = $event->data['response']; + + if (strpos($url, '..') !== false || strpos($url, '.') === false) { + return; + } + + if ($result = $this->_filterAsset($event)) { + $event->stopPropagation(); + return $result; + } + + $pathSegments = explode('.', $url); + $ext = array_pop($pathSegments); + $parts = explode('/', $url); + $assetFile = null; + + if ($parts[0] === 'theme') { + $themeName = $parts[1]; + unset($parts[0], $parts[1]); + $fileFragment = urldecode(implode(DS, $parts)); + $path = App::themePath($themeName) . 'webroot' . DS; + if (file_exists($path . $fileFragment)) { + $assetFile = $path . $fileFragment; + } + } else { + $plugin = Inflector::camelize($parts[0]); + if (CakePlugin::loaded($plugin)) { + unset($parts[0]); + $fileFragment = urldecode(implode(DS, $parts)); + $pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS; + if (file_exists($pluginWebroot . $fileFragment)) { + $assetFile = $pluginWebroot . $fileFragment; + } + } + } + + if ($assetFile !== null) { + $event->stopPropagation(); + $this->_deliverAsset($response, $assetFile, $ext); + return $response; + } + } + +/** + * Checks if the client is requeting a filtered asset and runs the corresponding + * filter if any is configured + * + * @param CakeEvent $event containing the request and response object + * @return CakeResponse if the client is requesting a recognized asset, null otherwise + */ + protected function _filterAsset($event) { + $url = $event->data['request']->url; + $response = $event->data['response']; + $filters = Configure::read('Asset.filter'); + $isCss = ( + strpos($url, 'ccss/') === 0 || + preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?statusCode(404); + return $response; + } elseif ($isCss) { + include WWW_ROOT . DS . $filters['css']; + return $response; + } elseif ($isJs) { + include WWW_ROOT . DS . $filters['js']; + return $response; + } + } + +/** + * Sends an asset file to the client + * + * @param CakeResponse $response The response object to use. + * @param string $assetFile Path to the asset file in the file system + * @param string $ext The extension of the file to determine its mime type + * @return void + */ + protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) { + ob_start(); + $compressionEnabled = Configure::read('Asset.compress') && $response->compress(); + if ($response->type($ext) == $ext) { + $contentType = 'application/octet-stream'; + $agent = env('HTTP_USER_AGENT'); + if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { + $contentType = 'application/octetstream'; + } + $response->type($contentType); + } + if (!$compressionEnabled) { + $response->header('Content-Length', filesize($assetFile)); + } + $response->cache(filemtime($assetFile)); + $response->send(); + ob_clean(); + if ($ext === 'css' || $ext === 'js') { + include $assetFile; + } else { + readfile($assetFile); + } + + if ($compressionEnabled) { + ob_end_flush(); + } + } + +} \ No newline at end of file diff --git a/lib/Cake/Routing/Filter/CacheDispatcher.php b/lib/Cake/Routing/Filter/CacheDispatcher.php new file mode 100644 index 00000000000..65387cb530a --- /dev/null +++ b/lib/Cake/Routing/Filter/CacheDispatcher.php @@ -0,0 +1,70 @@ +data['request']->here(); + if ($path == '/') { + $path = 'home'; + } + $path = strtolower(Inflector::slug($path)); + + $filename = CACHE . 'views' . DS . $path . '.php'; + + if (!file_exists($filename)) { + $filename = CACHE . 'views' . DS . $path . '_index.php'; + } + if (file_exists($filename)) { + $controller = null; + $view = new View($controller); + $result = $view->renderCache($filename, microtime(true)); + if ($result !== false) { + $event->data['response']->body($result); + return $event->data['response']; + } + } + } + +} \ No newline at end of file diff --git a/lib/Cake/Test/Case/AllRoutingTest.php b/lib/Cake/Test/Case/AllRoutingTest.php index 0564439a5d6..ebd6712b428 100644 --- a/lib/Cake/Test/Case/AllRoutingTest.php +++ b/lib/Cake/Test/Case/AllRoutingTest.php @@ -38,6 +38,7 @@ public static function suite() { $suite->addTestDirectory($libs . 'Routing'); $suite->addTestDirectory($libs . 'Routing' . DS . 'Route'); + $suite->addTestDirectory($libs . 'Routing' . DS . 'Filter'); return $suite; } } diff --git a/lib/Cake/Test/Case/Routing/DispatcherTest.php b/lib/Cake/Test/Case/Routing/DispatcherTest.php index f94793adf34..f3c3d685ca5 100644 --- a/lib/Cake/Test/Case/Routing/DispatcherTest.php +++ b/lib/Cake/Test/Case/Routing/DispatcherTest.php @@ -1357,6 +1357,7 @@ public function testAssets() { 'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS) )); CakePlugin::load(array('TestPlugin', 'TestPluginTwo')); + Configure::write('Dispatcher.filters', array('AssetDispatcher')); $Dispatcher = new TestDispatcher(); $response = $this->getMock('CakeResponse', array('_sendHeader')); @@ -1377,7 +1378,7 @@ public function testAssets() { } /** - * Data provider for asset() + * Data provider for asset filter * * - theme assets. * - plugin assets. @@ -1475,6 +1476,7 @@ public function testAsset($url, $file) { 'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS) )); CakePlugin::load(array('TestPlugin', 'PluginJs')); + Configure::write('Dispatcher.filters', array('AssetDispatcher')); $Dispatcher = new TestDispatcher(); $response = $this->getMock('CakeResponse', array('_sendHeader')); @@ -1503,57 +1505,13 @@ public function testMissingAssetProcessor404() { 'js' => '', 'css' => null )); + Configure::write('Dispatcher.filters', array('AssetDispatcher')); $request = new CakeRequest('ccss/cake.generic.css'); - $event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response')); - $this->assertSame($response, $Dispatcher->asset($event)); + $Dispatcher->dispatch($request, $response); $this->assertEquals('404', $response->statusCode()); - $this->assertTrue($event->isStopped()); } -/** - * test that asset filters work for theme and plugin assets - * - * @return void - */ - public function testAssetFilterForThemeAndPlugins() { - $Dispatcher = new TestDispatcher(); - $response = $this->getMock('CakeResponse', array('_sendHeader')); - Configure::write('Asset.filter', array( - 'js' => '', - 'css' => '' - )); - - $request = new CakeRequest('theme/test_theme/ccss/cake.generic.css'); - $event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response')); - $this->assertSame($response, $Dispatcher->asset($event)); - $this->assertTrue($event->isStopped()); - - $request = new CakeRequest('theme/test_theme/cjs/debug_kit.js'); - $event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response')); - $this->assertSame($response, $Dispatcher->asset($event)); - $this->assertTrue($event->isStopped()); - - $request = new CakeRequest('test_plugin/ccss/cake.generic.css'); - $event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response')); - $this->assertSame($response, $Dispatcher->asset($event)); - $this->assertTrue($event->isStopped()); - - $request = new CakeRequest('test_plugin/cjs/debug_kit.js'); - $event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response')); - $this->assertSame($response, $Dispatcher->asset($event)); - $this->assertTrue($event->isStopped()); - - $request = new CakeRequest('css/ccss/debug_kit.css'); - $event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response')); - $this->assertNull($Dispatcher->asset($event)); - $this->assertFalse($event->isStopped()); - - $request = new CakeRequest('js/cjs/debug_kit.js'); - $event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response')); - $this->assertNull($Dispatcher->asset($event)); - $this->assertFalse($event->isStopped()); - } /** * Data provider for cached actions. @@ -1606,8 +1564,11 @@ public function testFullPageCachingDispatch($url) { $dispatcher->dispatch($request, $response); $out = $response->body(); - $event = new CakeEvent('DispatcherTest', $dispatcher, array('request' => $request, 'response' => $response)); - $response = $dispatcher->cached($event); + Configure::write('Dispatcher.filters', array('CacheDispatcher')); + $request = new CakeRequest($url); + $response = $this->getMock('CakeResponse', array('send')); + $dispatcher = new TestDispatcher(); + $dispatcher->dispatch($request, $response); $cached = $response->body(); $cached = preg_replace('//', '', $cached); diff --git a/lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php b/lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php new file mode 100644 index 00000000000..593619aa4ff --- /dev/null +++ b/lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php @@ -0,0 +1,78 @@ + + * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The Open Group Test Suite License + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests + * @package Cake.Test.Case.Routing.Filter + * @since CakePHP(tm) v 2.2 + * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + */ + +App::uses('AssetDispatcher', 'Routing/Filter'); +App::uses('CakeEvent', 'Event'); +App::uses('CakeResponse', 'Network'); + +class AssetDispatcherTest extends CakeTestCase { + +/** + * tearDown method + * + * @return void + */ + public function tearDown() { + Configure::write('Dispatcher.filters', array()); + } + +/** + * test that asset filters work for theme and plugin assets + * + * @return void + */ + public function testAssetFilterForThemeAndPlugins() { + $filter = new AssetDispatcher(); + $response = $this->getMock('CakeResponse', array('_sendHeader')); + Configure::write('Asset.filter', array( + 'js' => '', + 'css' => '' + )); + + $request = new CakeRequest('theme/test_theme/ccss/cake.generic.css'); + $event = new CakeEvent('DispatcherTest', $this, compact('request', 'response')); + $this->assertSame($response, $filter->beforeDispatch($event)); + $this->assertTrue($event->isStopped()); + + $request = new CakeRequest('theme/test_theme/cjs/debug_kit.js'); + $event = new CakeEvent('DispatcherTest', $this, compact('request', 'response')); + $this->assertSame($response, $filter->beforeDispatch($event)); + $this->assertTrue($event->isStopped()); + + $request = new CakeRequest('test_plugin/ccss/cake.generic.css'); + $event = new CakeEvent('DispatcherTest', $this, compact('request', 'response')); + $this->assertSame($response, $filter->beforeDispatch($event)); + $this->assertTrue($event->isStopped()); + + $request = new CakeRequest('test_plugin/cjs/debug_kit.js'); + $event = new CakeEvent('DispatcherTest', $this, compact('request', 'response')); + $this->assertSame($response, $filter->beforeDispatch($event)); + $this->assertTrue($event->isStopped()); + + $request = new CakeRequest('css/ccss/debug_kit.css'); + $event = new CakeEvent('DispatcherTest', $this, compact('request', 'response')); + $this->assertNull($filter->beforeDispatch($event)); + $this->assertFalse($event->isStopped()); + + $request = new CakeRequest('js/cjs/debug_kit.js'); + $event = new CakeEvent('DispatcherTest', $this, compact('request', 'response')); + $this->assertNull($filter->beforeDispatch($event)); + $this->assertFalse($event->isStopped()); + } +} \ No newline at end of file