1<?php
2/**
3 * CakePHP(tm) Tests <https://book.cakephp.org/view/1196/Testing>
4 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5 *
6 * Licensed under The MIT License
7 * For full copyright and license information, please see the LICENSE.txt
8 * Redistributions of files must retain the above copyright notice.
9 *
10 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11 * @link          https://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests
12 * @package       Cake.Test.Case.Routing.Filter
13 * @since         CakePHP(tm) v 2.2
14 * @license       https://opensource.org/licenses/mit-license.php MIT License
15 */
16
17App::uses('AssetDispatcher', 'Routing/Filter');
18App::uses('CakeEvent', 'Event');
19App::uses('CakeResponse', 'Network');
20
21/**
22 * AssetDispatcherTest
23 *
24 * @package       Cake.Test.Case.Routing.Filter
25 */
26class AssetDispatcherTest extends CakeTestCase {
27
28/**
29 * tearDown method
30 *
31 * @return void
32 */
33	public function tearDown() {
34		parent::tearDown();
35		Configure::write('Dispatcher.filters', array());
36	}
37
38/**
39 * test that asset filters work for theme and plugin assets
40 *
41 * @return void
42 * @triggers DispatcherTest $this, compact('request', 'response')
43 * @triggers DispatcherTest $this, compact('request', 'response')
44 * @triggers DispatcherTest $this, compact('request', 'response')
45 * @triggers DispatcherTest $this, compact('request', 'response')
46 * @triggers DispatcherTest $this, compact('request', 'response')
47 * @triggers DispatcherTest $this, compact('request', 'response')
48 */
49	public function testAssetFilterForThemeAndPlugins() {
50		$filter = new AssetDispatcher();
51		$response = $this->getMock('CakeResponse', array('_sendHeader'));
52		Configure::write('Asset.filter', array(
53			'js' => '',
54			'css' => ''
55		));
56		App::build(array(
57			'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS),
58			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
59		), App::RESET);
60
61		$request = new CakeRequest('theme/test_theme/ccss/cake.generic.css');
62		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
63		$this->assertSame($response, $filter->beforeDispatch($event));
64		$this->assertTrue($event->isStopped());
65
66		$request = new CakeRequest('theme/test_theme/cjs/debug_kit.js');
67		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
68		$this->assertSame($response, $filter->beforeDispatch($event));
69		$this->assertTrue($event->isStopped());
70
71		$request = new CakeRequest('test_plugin/ccss/cake.generic.css');
72		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
73		$this->assertSame($response, $filter->beforeDispatch($event));
74		$this->assertTrue($event->isStopped());
75
76		$request = new CakeRequest('test_plugin/cjs/debug_kit.js');
77		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
78		$this->assertSame($response, $filter->beforeDispatch($event));
79		$this->assertTrue($event->isStopped());
80
81		$request = new CakeRequest('css/ccss/debug_kit.css');
82		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
83		$this->assertNull($filter->beforeDispatch($event));
84		$this->assertFalse($event->isStopped());
85
86		$request = new CakeRequest('js/cjs/debug_kit.js');
87		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
88		$this->assertNull($filter->beforeDispatch($event));
89		$this->assertFalse($event->isStopped());
90	}
91
92/**
93 * AssetDispatcher should not 404 extensions that could be handled
94 * by Routing.
95 *
96 * @return void
97 * @triggers DispatcherTest $this, compact('request', 'response')
98 */
99	public function testNoHandleRoutedExtension() {
100		$filter = new AssetDispatcher();
101		$response = $this->getMock('CakeResponse', array('_sendHeader'));
102		Configure::write('Asset.filter', array(
103			'js' => '',
104			'css' => ''
105		));
106		App::build(array(
107			'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS),
108			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
109		), App::RESET);
110		Router::parseExtensions('json');
111		Router::connect('/test_plugin/api/v1/:action', array('controller' => 'api'));
112		CakePlugin::load('TestPlugin');
113
114		$request = new CakeRequest('test_plugin/api/v1/forwarding.json');
115		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
116		$this->assertNull($filter->beforeDispatch($event));
117		$this->assertFalse($event->isStopped(), 'Events for routed extensions should not be stopped');
118	}
119
120/**
121 * Tests that $response->checkNotModified() is called and bypasses
122 * file dispatching
123 *
124 * @return void
125 * @triggers DispatcherTest $this, compact('request', 'response')
126 * @triggers DispatcherTest $this, compact('request', 'response')
127 */
128	public function testNotModified() {
129		$filter = new AssetDispatcher();
130		Configure::write('Asset.filter', array(
131			'js' => '',
132			'css' => ''
133		));
134		App::build(array(
135			'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS),
136			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
137		));
138		$time = filemtime(App::themePath('TestTheme') . 'webroot' . DS . 'img' . DS . 'cake.power.gif');
139		$time = new DateTime('@' . $time);
140
141		$response = $this->getMock('CakeResponse', array('send', 'checkNotModified'));
142		$request = new CakeRequest('theme/test_theme/img/cake.power.gif');
143
144		$response->expects($this->once())->method('checkNotModified')
145			->with($request)
146			->will($this->returnValue(true));
147		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
148
149		ob_start();
150		$this->assertSame($response, $filter->beforeDispatch($event));
151		ob_end_clean();
152		$this->assertEquals(200, $response->statusCode());
153		$this->assertEquals($time->format('D, j M Y H:i:s') . ' GMT', $response->modified());
154
155		$response = $this->getMock('CakeResponse', array('_sendHeader', 'checkNotModified'));
156		$request = new CakeRequest('theme/test_theme/img/cake.power.gif');
157		$response->expects($this->once())->method('checkNotModified')
158			->with($request)
159			->will($this->returnValue(true));
160		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
161		$this->assertSame($response, $filter->beforeDispatch($event));
162		$this->assertEquals($time->format('D, j M Y H:i:s') . ' GMT', $response->modified());
163	}
164
165/**
166 * Test that no exceptions are thrown for //index.php type URLs.
167 *
168 * @return void
169 * @triggers Dispatcher.beforeRequest $this, compact('request', 'response')
170 */
171	public function test404OnDoubleSlash() {
172		$filter = new AssetDispatcher();
173
174		$response = $this->getMock('CakeResponse', array('_sendHeader'));
175		$request = new CakeRequest('//index.php');
176		$event = new CakeEvent('Dispatcher.beforeRequest', $this, compact('request', 'response'));
177
178		$this->assertNull($filter->beforeDispatch($event));
179		$this->assertFalse($event->isStopped());
180	}
181
182/**
183 * Test that attempts to traverse directories are prevented.
184 *
185 * @return void
186 * @triggers Dispatcher.beforeRequest $this, compact('request', 'response')
187 */
188	public function test404OnDoubleDot() {
189		App::build(array(
190			'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS),
191			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
192		), App::RESET);
193		$response = $this->getMock('CakeResponse', array('_sendHeader'));
194		$request = new CakeRequest('theme/test_theme/../../../../../../VERSION.txt');
195		$event = new CakeEvent('Dispatcher.beforeRequest', $this, compact('request', 'response'));
196		$filter = new AssetDispatcher();
197		$this->assertNull($filter->beforeDispatch($event));
198		$this->assertFalse($event->isStopped());
199	}
200
201/**
202 * Test that attempts to traverse directories with urlencoded paths fail.
203 *
204 * @return void
205 * @triggers Dispatcher.beforeRequest $this, compact('request', 'response')
206 */
207	public function test404OnDoubleDotEncoded() {
208		App::build(array(
209			'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS),
210			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
211		), App::RESET);
212
213		$response = $this->getMock('CakeResponse', array('_sendHeader', 'send'));
214		$request = new CakeRequest('theme/test_theme/%2e./%2e./%2e./%2e./%2e./%2e./VERSION.txt');
215		$event = new CakeEvent('Dispatcher.beforeRequest', $this, compact('request', 'response'));
216
217		$response->expects($this->never())->method('send');
218
219		$filter = new AssetDispatcher();
220		$this->assertNull($filter->beforeDispatch($event));
221		$this->assertFalse($event->isStopped());
222	}
223
224/**
225 * Test asset content length is unset
226 *
227 * If content length is unset, then the webserver can figure it out.
228 *
229 * @outputBuffering enabled
230 * @return void
231 */
232	public function testAssetContentLength() {
233		Router::reload();
234		Configure::write('Dispatcher.filters', array('AssetDispatcher'));
235		App::build(array(
236			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
237		));
238
239		$url = 'theme/test_theme/css/test_asset.css';
240		$file = 'View/Themed/TestTheme/webroot/css/test_asset.css';
241
242		$request = new CakeRequest($url);
243		$response = $this->getMock('CakeResponse', array('_sendHeader', 'send'));
244		$event = new CakeEvent('Dispatcher.beforeRequest', $this, compact('request', 'response'));
245
246		$filter = new AssetDispatcher();
247		$filter->beforeDispatch($event);
248		$result = ob_get_clean();
249
250		$path = CAKE . 'Test' . DS . 'test_app' . DS . str_replace('/', DS, $file);
251		$file = file_get_contents($path);
252		$this->assertEquals($file, $result);
253
254		$headers = $response->header();
255		$this->assertFalse($headers['Content-Length']);
256	}
257
258}
259