1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
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://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View\Helper;
16:
17: use Cake\Core\Configure;
18: use Cake\Core\Plugin;
19: use Cake\Routing\Router;
20: use Cake\Utility\Inflector;
21: use Cake\View\Helper;
22:
23: /**
24: * UrlHelper class for generating URLs.
25: */
26: class UrlHelper extends Helper
27: {
28:
29: /**
30: * Returns a URL based on provided parameters.
31: *
32: * ### Options:
33: *
34: * - `escape`: If false, the URL will be returned unescaped, do only use if it is manually
35: * escaped afterwards before being displayed.
36: * - `fullBase`: If true, the full base URL will be prepended to the result
37: *
38: * @param string|array|null $url Either a relative string URL like `/products/view/23` or
39: * an array of URL parameters. Using an array for URLs will allow you to leverage
40: * the reverse routing features of CakePHP.
41: * @param array|bool $options Array of options; bool `full` for BC reasons.
42: * @return string Full translated URL with base path.
43: */
44: public function build($url = null, $options = false)
45: {
46: $defaults = [
47: 'fullBase' => false,
48: 'escape' => true,
49: ];
50: if (!is_array($options)) {
51: $options = ['fullBase' => $options];
52: }
53: $options += $defaults;
54:
55: /** @var string $url */
56: $url = Router::url($url, $options['fullBase']);
57: if ($options['escape']) {
58: /** @var string $url */
59: $url = h($url);
60: }
61:
62: return $url;
63: }
64:
65: /**
66: * Generates URL for given image file.
67: *
68: * Depending on options passed provides full URL with domain name. Also calls
69: * `Helper::assetTimestamp()` to add timestamp to local files.
70: *
71: * @param string|array $path Path string or URL array
72: * @param array $options Options array. Possible keys:
73: * `fullBase` Return full URL with domain name
74: * `pathPrefix` Path prefix for relative URLs
75: * `plugin` False value will prevent parsing path as a plugin
76: * `timestamp` Overrides the value of `Asset.timestamp` in Configure.
77: * Set to false to skip timestamp generation.
78: * Set to true to apply timestamps when debug is true. Set to 'force' to always
79: * enable timestamping regardless of debug value.
80: * @return string Generated URL
81: */
82: public function image($path, array $options = [])
83: {
84: $pathPrefix = Configure::read('App.imageBaseUrl');
85:
86: return $this->assetUrl($path, $options + compact('pathPrefix'));
87: }
88:
89: /**
90: * Generates URL for given CSS file.
91: *
92: * Depending on options passed provides full URL with domain name. Also calls
93: * `Helper::assetTimestamp()` to add timestamp to local files.
94: *
95: * @param string|array $path Path string or URL array
96: * @param array $options Options array. Possible keys:
97: * `fullBase` Return full URL with domain name
98: * `pathPrefix` Path prefix for relative URLs
99: * `ext` Asset extension to append
100: * `plugin` False value will prevent parsing path as a plugin
101: * `timestamp` Overrides the value of `Asset.timestamp` in Configure.
102: * Set to false to skip timestamp generation.
103: * Set to true to apply timestamps when debug is true. Set to 'force' to always
104: * enable timestamping regardless of debug value.
105: * @return string Generated URL
106: */
107: public function css($path, array $options = [])
108: {
109: $pathPrefix = Configure::read('App.cssBaseUrl');
110: $ext = '.css';
111:
112: return $this->assetUrl($path, $options + compact('pathPrefix', 'ext'));
113: }
114:
115: /**
116: * Generates URL for given javascript file.
117: *
118: * Depending on options passed provides full URL with domain name. Also calls
119: * `Helper::assetTimestamp()` to add timestamp to local files.
120: *
121: * @param string|array $path Path string or URL array
122: * @param array $options Options array. Possible keys:
123: * `fullBase` Return full URL with domain name
124: * `pathPrefix` Path prefix for relative URLs
125: * `ext` Asset extension to append
126: * `plugin` False value will prevent parsing path as a plugin
127: * `timestamp` Overrides the value of `Asset.timestamp` in Configure.
128: * Set to false to skip timestamp generation.
129: * Set to true to apply timestamps when debug is true. Set to 'force' to always
130: * enable timestamping regardless of debug value.
131: * @return string Generated URL
132: */
133: public function script($path, array $options = [])
134: {
135: $pathPrefix = Configure::read('App.jsBaseUrl');
136: $ext = '.js';
137:
138: return $this->assetUrl($path, $options + compact('pathPrefix', 'ext'));
139: }
140:
141: /**
142: * Generates URL for given asset file.
143: *
144: * Depending on options passed provides full URL with domain name. Also calls
145: * `Helper::assetTimestamp()` to add timestamp to local files.
146: *
147: * ### Options:
148: *
149: * - `fullBase` Boolean true or a string (e.g. https://example) to
150: * return full URL with protocol and domain name.
151: * - `pathPrefix` Path prefix for relative URLs
152: * - `ext` Asset extension to append
153: * - `plugin` False value will prevent parsing path as a plugin
154: * - `timestamp` Overrides the value of `Asset.timestamp` in Configure.
155: * Set to false to skip timestamp generation.
156: * Set to true to apply timestamps when debug is true. Set to 'force' to always
157: * enable timestamping regardless of debug value.
158: *
159: * @param string|array $path Path string or URL array
160: * @param array $options Options array.
161: * @return string Generated URL
162: */
163: public function assetUrl($path, array $options = [])
164: {
165: if (is_array($path)) {
166: return $this->build($path, !empty($options['fullBase']));
167: }
168: // data URIs only require HTML escaping
169: if (preg_match('/^data:[a-z]+\/[a-z]+;/', $path)) {
170: return h($path);
171: }
172: if (strpos($path, '://') !== false || preg_match('/^[a-z]+:/i', $path)) {
173: return ltrim($this->build($path), '/');
174: }
175: if (!array_key_exists('plugin', $options) || $options['plugin'] !== false) {
176: list($plugin, $path) = $this->_View->pluginSplit($path, false);
177: }
178: if (!empty($options['pathPrefix']) && $path[0] !== '/') {
179: $path = $options['pathPrefix'] . $path;
180: }
181: if (!empty($options['ext']) &&
182: strpos($path, '?') === false &&
183: substr($path, -strlen($options['ext'])) !== $options['ext']
184: ) {
185: $path .= $options['ext'];
186: }
187: if (preg_match('|^([a-z0-9]+:)?//|', $path)) {
188: return $this->build($path);
189: }
190: if (isset($plugin)) {
191: $path = Inflector::underscore($plugin) . '/' . $path;
192: }
193:
194: $optionTimestamp = null;
195: if (array_key_exists('timestamp', $options)) {
196: $optionTimestamp = $options['timestamp'];
197: }
198: $webPath = $this->assetTimestamp($this->webroot($path), $optionTimestamp);
199:
200: $path = $this->_encodeUrl($webPath);
201:
202: if (!empty($options['fullBase'])) {
203: $fullBaseUrl = is_string($options['fullBase']) ? $options['fullBase'] : Router::fullBaseUrl();
204: $path = rtrim($fullBaseUrl, '/') . '/' . ltrim($path, '/');
205: }
206:
207: return $path;
208: }
209:
210: /**
211: * Encodes a URL for use in HTML attributes.
212: *
213: * @param string $url The URL to encode.
214: * @return string The URL encoded for both URL & HTML contexts.
215: */
216: protected function _encodeUrl($url)
217: {
218: $path = parse_url($url, PHP_URL_PATH);
219: $parts = array_map('rawurldecode', explode('/', $path));
220: $parts = array_map('rawurlencode', $parts);
221: $encoded = implode('/', $parts);
222:
223: /** @var string $url */
224: $url = h(str_replace($path, $encoded, $url));
225:
226: return $url;
227: }
228:
229: /**
230: * Adds a timestamp to a file based resource based on the value of `Asset.timestamp` in
231: * Configure. If Asset.timestamp is true and debug is true, or Asset.timestamp === 'force'
232: * a timestamp will be added.
233: *
234: * @param string $path The file path to timestamp, the path must be inside WWW_ROOT
235: * @param bool|string $timestamp If set will overrule the value of `Asset.timestamp` in Configure.
236: * @return string Path with a timestamp added, or not.
237: */
238: public function assetTimestamp($path, $timestamp = null)
239: {
240: if ($timestamp === null) {
241: $timestamp = Configure::read('Asset.timestamp');
242: }
243: $timestampEnabled = $timestamp === 'force' || ($timestamp === true && Configure::read('debug'));
244: if ($timestampEnabled && strpos($path, '?') === false) {
245: $filepath = preg_replace(
246: '/^' . preg_quote($this->_View->getRequest()->getAttribute('webroot'), '/') . '/',
247: '',
248: urldecode($path)
249: );
250: $webrootPath = WWW_ROOT . str_replace('/', DIRECTORY_SEPARATOR, $filepath);
251: if (file_exists($webrootPath)) {
252: return $path . '?' . filemtime($webrootPath);
253: }
254: $segments = explode('/', ltrim($filepath, '/'));
255: $plugin = Inflector::camelize($segments[0]);
256: if (Plugin::isLoaded($plugin)) {
257: unset($segments[0]);
258: $pluginPath = Plugin::path($plugin) . 'webroot' . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $segments);
259: if (file_exists($pluginPath)) {
260: return $path . '?' . filemtime($pluginPath);
261: }
262: }
263: }
264:
265: return $path;
266: }
267:
268: /**
269: * Checks if a file exists when theme is used, if no file is found default location is returned
270: *
271: * @param string $file The file to create a webroot path to.
272: * @return string Web accessible path to file.
273: */
274: public function webroot($file)
275: {
276: $request = $this->_View->getRequest();
277:
278: $asset = explode('?', $file);
279: $asset[1] = isset($asset[1]) ? '?' . $asset[1] : null;
280: $webPath = $request->getAttribute('webroot') . $asset[0];
281: $file = $asset[0];
282:
283: if (!empty($this->_View->getTheme())) {
284: $file = trim($file, '/');
285: $theme = $this->_inflectThemeName($this->_View->getTheme()) . '/';
286:
287: if (DIRECTORY_SEPARATOR === '\\') {
288: $file = str_replace('/', '\\', $file);
289: }
290:
291: if (file_exists(Configure::read('App.wwwRoot') . $theme . $file)) {
292: $webPath = $request->getAttribute('webroot') . $theme . $asset[0];
293: } else {
294: $themePath = Plugin::path($this->_View->getTheme());
295: $path = $themePath . 'webroot/' . $file;
296: if (file_exists($path)) {
297: $webPath = $request->getAttribute('webroot') . $theme . $asset[0];
298: }
299: }
300: }
301: if (strpos($webPath, '//') !== false) {
302: return str_replace('//', '/', $webPath . $asset[1]);
303: }
304:
305: return $webPath . $asset[1];
306: }
307:
308: /**
309: * Inflect the theme name to its underscored version.
310: *
311: * @param string $name Name of the theme which should be inflected.
312: * @return string Inflected name of the theme
313: */
314: protected function _inflectThemeName($name)
315: {
316: return Inflector::underscore($name);
317: }
318:
319: /**
320: * Event listeners.
321: *
322: * @return array
323: */
324: public function implementedEvents()
325: {
326: return [];
327: }
328: }
329: