Browse Source

Doh, actaully add UrlHelper to repo.

ADmad 11 years ago
parent
commit
62bd8232af
2 changed files with 471 additions and 0 deletions
  1. 185 0
      src/View/Helper/UrlHelper.php
  2. 286 0
      tests/TestCase/View/Helper/UrlHelperTest.php

+ 185 - 0
src/View/Helper/UrlHelper.php

@@ -0,0 +1,185 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\View\Helper;
+
+use Cake\Core\Configure;
+use Cake\Core\Plugin;
+use Cake\Routing\Router;
+use Cake\Utility\Inflector;
+use Cake\View\Helper;
+
+/**
+ * UrlHelper class for generating urls.
+ */
+class UrlHelper extends Helper {
+
+/**
+ * Finds URL for specified action.
+ *
+ * Returns a URL pointing at the provided parameters.
+ *
+ * @param string|array $url Either a relative string url like `/products/view/23` or
+ *    an array of URL parameters. Using an array for URLs will allow you to leverage
+ *    the reverse routing features of CakePHP.
+ * @param bool $full If true, the full base URL will be prepended to the result
+ * @return string Full translated URL with base path.
+ * @link http://book.cakephp.org/2.0/en/views/helpers.html
+ */
+	public function url($url = null, $full = false) {
+		return h(Router::url($url, $full));
+	}
+
+/**
+ * Generate URL for given asset file. Depending on options passed provides full URL with domain name.
+ * Also calls Helper::assetTimestamp() to add timestamp to local files
+ *
+ * @param string|array $path Path string or URL array
+ * @param array $options Options array. Possible keys:
+ *   `fullBase` Return full URL with domain name
+ *   `pathPrefix` Path prefix for relative URLs
+ *   `ext` Asset extension to append
+ *   `plugin` False value will prevent parsing path as a plugin
+ * @return string Generated URL
+ */
+	public function assetUrl($path, array $options = array()) {
+		if (is_array($path)) {
+			return $this->url($path, !empty($options['fullBase']));
+		}
+		if (strpos($path, '://') !== false) {
+			return $path;
+		}
+		if (!array_key_exists('plugin', $options) || $options['plugin'] !== false) {
+			list($plugin, $path) = $this->_View->pluginSplit($path, false);
+		}
+		if (!empty($options['pathPrefix']) && $path[0] !== '/') {
+			$path = $options['pathPrefix'] . $path;
+		}
+		if (
+			!empty($options['ext']) &&
+			strpos($path, '?') === false &&
+			substr($path, -strlen($options['ext'])) !== $options['ext']
+		) {
+			$path .= $options['ext'];
+		}
+		if (preg_match('|^([a-z0-9]+:)?//|', $path)) {
+			return $path;
+		}
+		if (isset($plugin)) {
+			$path = Inflector::underscore($plugin) . '/' . $path;
+		}
+		$path = $this->_encodeUrl($this->assetTimestamp($this->webroot($path)));
+
+		if (!empty($options['fullBase'])) {
+			$path = rtrim(Router::fullBaseUrl(), '/') . '/' . ltrim($path, '/');
+		}
+		return $path;
+	}
+
+/**
+ * Encodes a URL for use in HTML attributes.
+ *
+ * @param string $url The URL to encode.
+ * @return string The URL encoded for both URL & HTML contexts.
+ */
+	protected function _encodeUrl($url) {
+		$path = parse_url($url, PHP_URL_PATH);
+		$parts = array_map('rawurldecode', explode('/', $path));
+		$parts = array_map('rawurlencode', $parts);
+		$encoded = implode('/', $parts);
+		return h(str_replace($path, $encoded, $url));
+	}
+
+/**
+ * Adds a timestamp to a file based resource based on the value of `Asset.timestamp` in
+ * Configure. If Asset.timestamp is true and debug is true, or Asset.timestamp === 'force'
+ * a timestamp will be added.
+ *
+ * @param string $path The file path to timestamp, the path must be inside WWW_ROOT
+ * @return string Path with a timestamp added, or not.
+ */
+	public function assetTimestamp($path) {
+		$stamp = Configure::read('Asset.timestamp');
+		$timestampEnabled = $stamp === 'force' || ($stamp === true && Configure::read('debug'));
+		if ($timestampEnabled && strpos($path, '?') === false) {
+			$filepath = preg_replace(
+				'/^' . preg_quote($this->request->webroot, '/') . '/',
+				'',
+				urldecode($path)
+			);
+			$webrootPath = WWW_ROOT . str_replace('/', DS, $filepath);
+			if (file_exists($webrootPath)) {
+				//@codingStandardsIgnoreStart
+				return $path . '?' . @filemtime($webrootPath);
+				//@codingStandardsIgnoreEnd
+			}
+			$segments = explode('/', ltrim($filepath, '/'));
+			$plugin = Inflector::camelize($segments[0]);
+			if (Plugin::loaded($plugin)) {
+				unset($segments[0]);
+				$pluginPath = Plugin::path($plugin) . 'webroot' . DS . implode(DS, $segments);
+				//@codingStandardsIgnoreStart
+				return $path . '?' . @filemtime($pluginPath);
+				//@codingStandardsIgnoreEnd
+			}
+		}
+		return $path;
+	}
+
+/**
+ * Checks if a file exists when theme is used, if no file is found default location is returned
+ *
+ * @param string $file The file to create a webroot path to.
+ * @return string Web accessible path to file.
+ */
+	public function webroot($file) {
+		$asset = explode('?', $file);
+		$asset[1] = isset($asset[1]) ? '?' . $asset[1] : null;
+		$webPath = $this->request->webroot . $asset[0];
+		$file = $asset[0];
+
+		if (!empty($this->theme)) {
+			$file = trim($file, '/');
+			$theme = Inflector::underscore($this->theme) . '/';
+
+			if (DS === '\\') {
+				$file = str_replace('/', '\\', $file);
+			}
+
+			if (file_exists(Configure::read('App.www_root') . $theme . $file)) {
+				$webPath = $this->request->webroot . $theme . $asset[0];
+			} else {
+				$themePath = Plugin::path($this->theme);
+				$path = $themePath . 'webroot/' . $file;
+				if (file_exists($path)) {
+					$webPath = $this->request->webroot . $theme . $asset[0];
+				}
+			}
+		}
+		if (strpos($webPath, '//') !== false) {
+			return str_replace('//', '/', $webPath . $asset[1]);
+		}
+		return $webPath . $asset[1];
+	}
+
+/**
+ * Event listeners.
+ *
+ * @return array
+ */
+	public function implementedEvents() {
+		return [];
+	}
+
+}

+ 286 - 0
tests/TestCase/View/Helper/UrlHelperTest.php

@@ -0,0 +1,286 @@
+<?php
+/**
+ * CakePHP(tm) Tests <http://book.cakephp.org/2.0/en/development/testing.html>
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://book.cakephp.org/2.0/en/development/testing.html CakePHP(tm) Tests
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\View;
+
+use Cake\Core\Configure;
+use Cake\Core\Plugin;
+use Cake\Network\Request;
+use Cake\Routing\Router;
+use Cake\TestSuite\TestCase;
+use Cake\View\Helper\UrlHelper;
+use Cake\View\View;
+
+/**
+ * HelperTest class
+ *
+ */
+class HelperTest extends TestCase {
+
+/**
+ * setUp method
+ *
+ * @return void
+ */
+	public function setUp() {
+		parent::setUp();
+
+		Router::reload();
+		$this->View = new View();
+		$this->Helper = new UrlHelper($this->View);
+		$this->Helper->request = new Request();
+
+		Configure::write('App.namespace', 'TestApp');
+		Plugin::load(['TestTheme']);
+	}
+
+/**
+ * tearDown method
+ *
+ * @return void
+ */
+	public function tearDown() {
+		parent::tearDown();
+		Configure::delete('Asset');
+
+		Plugin::unload();
+		unset($this->Helper, $this->View);
+	}
+
+/**
+ * Ensure HTML escaping of URL params. So link addresses are valid and not exploited
+ *
+ * @return void
+ */
+	public function testUrlConversion() {
+		Router::connect('/:controller/:action/*');
+
+		$result = $this->Helper->url('/controller/action/1');
+		$this->assertEquals('/controller/action/1', $result);
+
+		$result = $this->Helper->url('/controller/action/1?one=1&two=2');
+		$this->assertEquals('/controller/action/1?one=1&amp;two=2', $result);
+
+		$result = $this->Helper->url(array('controller' => 'posts', 'action' => 'index', 'page' => '1" onclick="alert(\'XSS\');"'));
+		$this->assertEquals("/posts/index?page=1%22+onclick%3D%22alert%28%27XSS%27%29%3B%22", $result);
+
+		$result = $this->Helper->url('/controller/action/1/param:this+one+more');
+		$this->assertEquals('/controller/action/1/param:this+one+more', $result);
+
+		$result = $this->Helper->url('/controller/action/1/param:this%20one%20more');
+		$this->assertEquals('/controller/action/1/param:this%20one%20more', $result);
+
+		$result = $this->Helper->url('/controller/action/1/param:%7Baround%20here%7D%5Bthings%5D%5Bare%5D%24%24');
+		$this->assertEquals('/controller/action/1/param:%7Baround%20here%7D%5Bthings%5D%5Bare%5D%24%24', $result);
+
+		$result = $this->Helper->url(array(
+			'controller' => 'posts', 'action' => 'index', 'param' => '%7Baround%20here%7D%5Bthings%5D%5Bare%5D%24%24'
+		));
+		$this->assertEquals("/posts/index?param=%257Baround%2520here%257D%255Bthings%255D%255Bare%255D%2524%2524", $result);
+
+		$result = $this->Helper->url(array(
+			'controller' => 'posts', 'action' => 'index', 'page' => '1',
+			'?' => array('one' => 'value', 'two' => 'value', 'three' => 'purple')
+		));
+		$this->assertEquals("/posts/index?page=1&amp;one=value&amp;two=value&amp;three=purple", $result);
+	}
+
+/**
+ * test assetTimestamp application
+ *
+ * @return void
+ */
+	public function testAssetTimestamp() {
+		Configure::write('Foo.bar', 'test');
+		Configure::write('Asset.timestamp', false);
+		$result = $this->Helper->assetTimestamp(Configure::read('App.cssBaseUrl') . 'cake.generic.css');
+		$this->assertEquals(Configure::read('App.cssBaseUrl') . 'cake.generic.css', $result);
+
+		Configure::write('Asset.timestamp', true);
+		Configure::write('debug', false);
+
+		$result = $this->Helper->assetTimestamp('/%3Cb%3E/cake.generic.css');
+		$this->assertEquals('/%3Cb%3E/cake.generic.css', $result);
+
+		$result = $this->Helper->assetTimestamp(Configure::read('App.cssBaseUrl') . 'cake.generic.css');
+		$this->assertEquals(Configure::read('App.cssBaseUrl') . 'cake.generic.css', $result);
+
+		Configure::write('Asset.timestamp', true);
+		Configure::write('debug', true);
+		$result = $this->Helper->assetTimestamp(Configure::read('App.cssBaseUrl') . 'cake.generic.css');
+		$this->assertRegExp('/' . preg_quote(Configure::read('App.cssBaseUrl') . 'cake.generic.css?', '/') . '[0-9]+/', $result);
+
+		Configure::write('Asset.timestamp', 'force');
+		Configure::write('debug', false);
+		$result = $this->Helper->assetTimestamp(Configure::read('App.cssBaseUrl') . 'cake.generic.css');
+		$this->assertRegExp('/' . preg_quote(Configure::read('App.cssBaseUrl') . 'cake.generic.css?', '/') . '[0-9]+/', $result);
+
+		$result = $this->Helper->assetTimestamp(Configure::read('App.cssBaseUrl') . 'cake.generic.css?someparam');
+		$this->assertEquals(Configure::read('App.cssBaseUrl') . 'cake.generic.css?someparam', $result);
+
+		$this->Helper->request->webroot = '/some/dir/';
+		$result = $this->Helper->assetTimestamp('/some/dir/' . Configure::read('App.cssBaseUrl') . 'cake.generic.css');
+		$this->assertRegExp('/' . preg_quote(Configure::read('App.cssBaseUrl') . 'cake.generic.css?', '/') . '[0-9]+/', $result);
+	}
+
+/**
+ * test assetUrl application
+ *
+ * @return void
+ */
+	public function testAssetUrl() {
+		Router::connect('/:controller/:action/*');
+
+		$this->Helper->webroot = '';
+		$result = $this->Helper->assetUrl(array(
+				'controller' => 'js',
+				'action' => 'post',
+				'_ext' => 'js'
+			),
+			array('fullBase' => true)
+		);
+		$this->assertEquals(Router::fullBaseUrl() . '/js/post.js', $result);
+
+		$result = $this->Helper->assetUrl('foo.jpg', array('pathPrefix' => 'img/'));
+		$this->assertEquals('img/foo.jpg', $result);
+
+		$result = $this->Helper->assetUrl('foo.jpg', array('fullBase' => true));
+		$this->assertEquals(Router::fullBaseUrl() . '/foo.jpg', $result);
+
+		$result = $this->Helper->assetUrl('style', array('ext' => '.css'));
+		$this->assertEquals('style.css', $result);
+
+		$result = $this->Helper->assetUrl('dir/sub dir/my image', array('ext' => '.jpg'));
+		$this->assertEquals('dir/sub%20dir/my%20image.jpg', $result);
+
+		$result = $this->Helper->assetUrl('foo.jpg?one=two&three=four');
+		$this->assertEquals('foo.jpg?one=two&amp;three=four', $result);
+
+		$result = $this->Helper->assetUrl('dir/big+tall/image', array('ext' => '.jpg'));
+		$this->assertEquals('dir/big%2Btall/image.jpg', $result);
+	}
+
+/**
+ * Test assetUrl with no rewriting.
+ *
+ * @return void
+ */
+	public function testAssetUrlNoRewrite() {
+		$this->Helper->request->addPaths(array(
+			'base' => '/cake_dev/index.php',
+			'webroot' => '/cake_dev/app/webroot/',
+			'here' => '/cake_dev/index.php/tasks',
+		));
+		$result = $this->Helper->assetUrl('img/cake.icon.png', array('fullBase' => true));
+		$expected = Configure::read('App.fullBaseUrl') . '/cake_dev/app/webroot/img/cake.icon.png';
+		$this->assertEquals($expected, $result);
+	}
+
+/**
+ * Test assetUrl with plugins.
+ *
+ * @return void
+ */
+	public function testAssetUrlPlugin() {
+		$this->Helper->webroot = '';
+		Plugin::load('TestPlugin');
+
+		$result = $this->Helper->assetUrl('TestPlugin.style', array('ext' => '.css'));
+		$this->assertEquals('test_plugin/style.css', $result);
+
+		$result = $this->Helper->assetUrl('TestPlugin.style', array('ext' => '.css', 'plugin' => false));
+		$this->assertEquals('TestPlugin.style.css', $result);
+
+		Plugin::unload('TestPlugin');
+	}
+
+/**
+ * test assetUrl and Asset.timestamp = force
+ *
+ * @return void
+ */
+	public function testAssetUrlTimestampForce() {
+		$this->Helper->webroot = '';
+		Configure::write('Asset.timestamp', 'force');
+
+		$result = $this->Helper->assetUrl('cake.generic.css', array('pathPrefix' => Configure::read('App.cssBaseUrl')));
+		$this->assertRegExp('/' . preg_quote(Configure::read('App.cssBaseUrl') . 'cake.generic.css?', '/') . '[0-9]+/', $result);
+	}
+
+/**
+ * test assetTimestamp with plugins and themes
+ *
+ * @return void
+ */
+	public function testAssetTimestampPluginsAndThemes() {
+		Configure::write('Asset.timestamp', 'force');
+		Plugin::load(array('TestPlugin'));
+
+		$result = $this->Helper->assetTimestamp('/test_plugin/css/test_plugin_asset.css');
+		$this->assertRegExp('#/test_plugin/css/test_plugin_asset.css\?[0-9]+$#', $result, 'Missing timestamp plugin');
+
+		$result = $this->Helper->assetTimestamp('/test_plugin/css/i_dont_exist.css');
+		$this->assertRegExp('#/test_plugin/css/i_dont_exist.css\?$#', $result, 'No error on missing file');
+
+		$result = $this->Helper->assetTimestamp('/test_theme/js/theme.js');
+		$this->assertRegExp('#/test_theme/js/theme.js\?[0-9]+$#', $result, 'Missing timestamp theme');
+
+		$result = $this->Helper->assetTimestamp('/test_theme/js/non_existant.js');
+		$this->assertRegExp('#/test_theme/js/non_existant.js\?$#', $result, 'No error on missing file');
+	}
+
+/**
+ * Test generating paths with webroot().
+ *
+ * @return void
+ */
+	public function testWebrootPaths() {
+		$this->Helper->request->webroot = '/';
+		$result = $this->Helper->webroot('/img/cake.power.gif');
+		$expected = '/img/cake.power.gif';
+		$this->assertEquals($expected, $result);
+
+		$this->Helper->theme = 'TestTheme';
+
+		$result = $this->Helper->webroot('/img/cake.power.gif');
+		$expected = '/test_theme/img/cake.power.gif';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Helper->webroot('/img/test.jpg');
+		$expected = '/test_theme/img/test.jpg';
+		$this->assertEquals($expected, $result);
+
+		$webRoot = Configure::read('App.www_root');
+		Configure::write('App.www_root', TEST_APP . 'TestApp/webroot/');
+
+		$result = $this->Helper->webroot('/img/cake.power.gif');
+		$expected = '/test_theme/img/cake.power.gif';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Helper->webroot('/img/test.jpg');
+		$expected = '/test_theme/img/test.jpg';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Helper->webroot('/img/cake.icon.gif');
+		$expected = '/img/cake.icon.gif';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Helper->webroot('/img/cake.icon.gif?some=param');
+		$expected = '/img/cake.icon.gif?some=param';
+		$this->assertEquals($expected, $result);
+
+		Configure::write('App.www_root', $webRoot);
+	}
+
+}