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 0.9.1
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\Http\Response;
19: use Cake\View\Helper;
20: use Cake\View\StringTemplateTrait;
21: use Cake\View\View;
22:
23: /**
24: * Html Helper class for easy use of HTML widgets.
25: *
26: * HtmlHelper encloses all methods needed while working with HTML pages.
27: *
28: * @property \Cake\View\Helper\UrlHelper $Url
29: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html
30: */
31: class HtmlHelper extends Helper
32: {
33: use StringTemplateTrait;
34:
35: /**
36: * List of helpers used by this helper
37: *
38: * @var array
39: */
40: public $helpers = ['Url'];
41:
42: /**
43: * Reference to the Response object
44: *
45: * @var \Cake\Http\Response
46: */
47: public $response;
48:
49: /**
50: * Default config for this class
51: *
52: * @var array
53: */
54: protected $_defaultConfig = [
55: 'templates' => [
56: 'meta' => '<meta{{attrs}}/>',
57: 'metalink' => '<link href="{{url}}"{{attrs}}/>',
58: 'link' => '<a href="{{url}}"{{attrs}}>{{content}}</a>',
59: 'mailto' => '<a href="mailto:{{url}}"{{attrs}}>{{content}}</a>',
60: 'image' => '<img src="{{url}}"{{attrs}}/>',
61: 'tableheader' => '<th{{attrs}}>{{content}}</th>',
62: 'tableheaderrow' => '<tr{{attrs}}>{{content}}</tr>',
63: 'tablecell' => '<td{{attrs}}>{{content}}</td>',
64: 'tablerow' => '<tr{{attrs}}>{{content}}</tr>',
65: 'block' => '<div{{attrs}}>{{content}}</div>',
66: 'blockstart' => '<div{{attrs}}>',
67: 'blockend' => '</div>',
68: 'tag' => '<{{tag}}{{attrs}}>{{content}}</{{tag}}>',
69: 'tagstart' => '<{{tag}}{{attrs}}>',
70: 'tagend' => '</{{tag}}>',
71: 'tagselfclosing' => '<{{tag}}{{attrs}}/>',
72: 'para' => '<p{{attrs}}>{{content}}</p>',
73: 'parastart' => '<p{{attrs}}>',
74: 'css' => '<link rel="{{rel}}" href="{{url}}"{{attrs}}/>',
75: 'style' => '<style{{attrs}}>{{content}}</style>',
76: 'charset' => '<meta charset="{{charset}}"/>',
77: 'ul' => '<ul{{attrs}}>{{content}}</ul>',
78: 'ol' => '<ol{{attrs}}>{{content}}</ol>',
79: 'li' => '<li{{attrs}}>{{content}}</li>',
80: 'javascriptblock' => '<script{{attrs}}>{{content}}</script>',
81: 'javascriptstart' => '<script>',
82: 'javascriptlink' => '<script src="{{url}}"{{attrs}}></script>',
83: 'javascriptend' => '</script>',
84: 'confirmJs' => '{{confirm}}'
85: ]
86: ];
87:
88: /**
89: * Breadcrumbs.
90: *
91: * @var array
92: * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
93: */
94: protected $_crumbs = [];
95:
96: /**
97: * Names of script & css files that have been included once
98: *
99: * @var array
100: */
101: protected $_includedAssets = [];
102:
103: /**
104: * Options for the currently opened script block buffer if any.
105: *
106: * @var array
107: */
108: protected $_scriptBlockOptions = [];
109:
110: /**
111: * Document type definitions
112: *
113: * @var string[]
114: */
115: protected $_docTypes = [
116: 'html4-strict' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
117: 'html4-trans' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
118: 'html4-frame' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
119: 'html5' => '<!DOCTYPE html>',
120: 'xhtml-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
121: 'xhtml-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
122: 'xhtml-frame' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
123: 'xhtml11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
124: ];
125:
126: /**
127: * Constructor
128: *
129: * ### Settings
130: *
131: * - `templates` Either a filename to a config containing templates.
132: * Or an array of templates to load. See Cake\View\StringTemplate for
133: * template formatting.
134: *
135: * ### Customizing tag sets
136: *
137: * Using the `templates` option you can redefine the tag HtmlHelper will use.
138: *
139: * @param \Cake\View\View $View The View this helper is being attached to.
140: * @param array $config Configuration settings for the helper.
141: */
142: public function __construct(View $View, array $config = [])
143: {
144: parent::__construct($View, $config);
145: $this->response = $this->_View->getResponse() ?: new Response();
146: }
147:
148: /**
149: * Adds a link to the breadcrumbs array.
150: *
151: * @param string $name Text for link
152: * @param string|array|null $link URL for link (if empty it won't be a link)
153: * @param array $options Link attributes e.g. ['id' => 'selected']
154: * @return $this
155: * @see \Cake\View\Helper\HtmlHelper::link() for details on $options that can be used.
156: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
157: * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
158: */
159: public function addCrumb($name, $link = null, array $options = [])
160: {
161: deprecationWarning(
162: 'HtmlHelper::addCrumb() is deprecated. ' .
163: 'Use the BreadcrumbsHelper instead.'
164: );
165:
166: $this->_crumbs[] = [$name, $link, $options];
167:
168: return $this;
169: }
170:
171: /**
172: * Returns a doctype string.
173: *
174: * Possible doctypes:
175: *
176: * - html4-strict: HTML4 Strict.
177: * - html4-trans: HTML4 Transitional.
178: * - html4-frame: HTML4 Frameset.
179: * - html5: HTML5. Default value.
180: * - xhtml-strict: XHTML1 Strict.
181: * - xhtml-trans: XHTML1 Transitional.
182: * - xhtml-frame: XHTML1 Frameset.
183: * - xhtml11: XHTML1.1.
184: *
185: * @param string $type Doctype to use.
186: * @return string|null Doctype string
187: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-doctype-tags
188: */
189: public function docType($type = 'html5')
190: {
191: if (isset($this->_docTypes[$type])) {
192: return $this->_docTypes[$type];
193: }
194:
195: return null;
196: }
197:
198: /**
199: * Creates a link to an external resource and handles basic meta tags
200: *
201: * Create a meta tag that is output inline:
202: *
203: * ```
204: * $this->Html->meta('icon', 'favicon.ico');
205: * ```
206: *
207: * Append the meta tag to custom view block "meta":
208: *
209: * ```
210: * $this->Html->meta('description', 'A great page', ['block' => true]);
211: * ```
212: *
213: * Append the meta tag to custom view block:
214: *
215: * ```
216: * $this->Html->meta('description', 'A great page', ['block' => 'metaTags']);
217: * ```
218: *
219: * Create a custom meta tag:
220: *
221: * ```
222: * $this->Html->meta(['property' => 'og:site_name', 'content' => 'CakePHP']);
223: * ```
224: *
225: * ### Options
226: *
227: * - `block` - Set to true to append output to view block "meta" or provide
228: * custom block name.
229: *
230: * @param string|array $type The title of the external resource, Or an array of attributes for a
231: * custom meta tag.
232: * @param string|array|null $content The address of the external resource or string for content attribute
233: * @param array $options Other attributes for the generated tag. If the type attribute is html,
234: * rss, atom, or icon, the mime-type is returned.
235: * @return string|null A completed `<link />` element, or null if the element was sent to a block.
236: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-meta-tags
237: */
238: public function meta($type, $content = null, array $options = [])
239: {
240: if (!is_array($type)) {
241: $types = [
242: 'rss' => ['type' => 'application/rss+xml', 'rel' => 'alternate', 'title' => $type, 'link' => $content],
243: 'atom' => ['type' => 'application/atom+xml', 'title' => $type, 'link' => $content],
244: 'icon' => ['type' => 'image/x-icon', 'rel' => 'icon', 'link' => $content],
245: 'keywords' => ['name' => 'keywords', 'content' => $content],
246: 'description' => ['name' => 'description', 'content' => $content],
247: 'robots' => ['name' => 'robots', 'content' => $content],
248: 'viewport' => ['name' => 'viewport', 'content' => $content],
249: 'canonical' => ['rel' => 'canonical', 'link' => $content],
250: 'next' => ['rel' => 'next', 'link' => $content],
251: 'prev' => ['rel' => 'prev', 'link' => $content],
252: 'first' => ['rel' => 'first', 'link' => $content],
253: 'last' => ['rel' => 'last', 'link' => $content]
254: ];
255:
256: if ($type === 'icon' && $content === null) {
257: $types['icon']['link'] = 'favicon.ico';
258: }
259:
260: if (isset($types[$type])) {
261: $type = $types[$type];
262: } elseif (!isset($options['type']) && $content !== null) {
263: if (is_array($content) && isset($content['_ext'])) {
264: $type = $types[$content['_ext']];
265: } else {
266: $type = ['name' => $type, 'content' => $content];
267: }
268: } elseif (isset($options['type'], $types[$options['type']])) {
269: $type = $types[$options['type']];
270: unset($options['type']);
271: } else {
272: $type = [];
273: }
274: }
275:
276: $options += $type + ['block' => null];
277: $out = null;
278:
279: if (isset($options['link'])) {
280: $options['link'] = $this->Url->assetUrl($options['link']);
281: if (isset($options['rel']) && $options['rel'] === 'icon') {
282: $out = $this->formatTemplate('metalink', [
283: 'url' => $options['link'],
284: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'link'])
285: ]);
286: $options['rel'] = 'shortcut icon';
287: }
288: $out .= $this->formatTemplate('metalink', [
289: 'url' => $options['link'],
290: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'link'])
291: ]);
292: } else {
293: $out = $this->formatTemplate('meta', [
294: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'type'])
295: ]);
296: }
297:
298: if (empty($options['block'])) {
299: return $out;
300: }
301: if ($options['block'] === true) {
302: $options['block'] = __FUNCTION__;
303: }
304: $this->_View->append($options['block'], $out);
305: }
306:
307: /**
308: * Returns a charset META-tag.
309: *
310: * @param string|null $charset The character set to be used in the meta tag. If empty,
311: * The App.encoding value will be used. Example: "utf-8".
312: * @return string A meta tag containing the specified character set.
313: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-charset-tags
314: */
315: public function charset($charset = null)
316: {
317: if (empty($charset)) {
318: $charset = strtolower(Configure::read('App.encoding'));
319: }
320:
321: return $this->formatTemplate('charset', [
322: 'charset' => !empty($charset) ? $charset : 'utf-8'
323: ]);
324: }
325:
326: /**
327: * Creates an HTML link.
328: *
329: * If $url starts with "http://" this is treated as an external link. Else,
330: * it is treated as a path to controller/action and parsed with the
331: * UrlHelper::build() method.
332: *
333: * If the $url is empty, $title is used instead.
334: *
335: * ### Options
336: *
337: * - `escape` Set to false to disable escaping of title and attributes.
338: * - `escapeTitle` Set to false to disable escaping of title. Takes precedence
339: * over value of `escape`)
340: * - `confirm` JavaScript confirmation message.
341: *
342: * @param string|array $title The content to be wrapped by `<a>` tags.
343: * Can be an array if $url is null. If $url is null, $title will be used as both the URL and title.
344: * @param string|array|null $url Cake-relative URL or array of URL parameters, or
345: * external URL (starts with http://)
346: * @param array $options Array of options and HTML attributes.
347: * @return string An `<a />` element.
348: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-links
349: */
350: public function link($title, $url = null, array $options = [])
351: {
352: $escapeTitle = true;
353: if ($url !== null) {
354: $url = $this->Url->build($url, $options);
355: unset($options['fullBase']);
356: } else {
357: $url = $this->Url->build($title);
358: $title = htmlspecialchars_decode($url, ENT_QUOTES);
359: $title = h(urldecode($title));
360: $escapeTitle = false;
361: }
362:
363: if (isset($options['escapeTitle'])) {
364: $escapeTitle = $options['escapeTitle'];
365: unset($options['escapeTitle']);
366: } elseif (isset($options['escape'])) {
367: $escapeTitle = $options['escape'];
368: }
369:
370: if ($escapeTitle === true) {
371: $title = h($title);
372: } elseif (is_string($escapeTitle)) {
373: $title = htmlentities($title, ENT_QUOTES, $escapeTitle);
374: }
375:
376: $templater = $this->templater();
377: $confirmMessage = null;
378: if (isset($options['confirm'])) {
379: $confirmMessage = $options['confirm'];
380: unset($options['confirm']);
381: }
382: if ($confirmMessage) {
383: $confirm = $this->_confirm($confirmMessage, 'return true;', 'return false;', $options);
384: $options['onclick'] = $templater->format('confirmJs', [
385: 'confirmMessage' => $this->_cleanConfirmMessage($confirmMessage),
386: 'confirm' => $confirm
387: ]);
388: }
389:
390: return $templater->format('link', [
391: 'url' => $url,
392: 'attrs' => $templater->formatAttributes($options),
393: 'content' => $title
394: ]);
395: }
396:
397: /**
398: * Creates a link element for CSS stylesheets.
399: *
400: * ### Usage
401: *
402: * Include one CSS file:
403: *
404: * ```
405: * echo $this->Html->css('styles.css');
406: * ```
407: *
408: * Include multiple CSS files:
409: *
410: * ```
411: * echo $this->Html->css(['one.css', 'two.css']);
412: * ```
413: *
414: * Add the stylesheet to view block "css":
415: *
416: * ```
417: * $this->Html->css('styles.css', ['block' => true]);
418: * ```
419: *
420: * Add the stylesheet to a custom block:
421: *
422: * ```
423: * $this->Html->css('styles.css', ['block' => 'layoutCss']);
424: * ```
425: *
426: * ### Options
427: *
428: * - `block` Set to true to append output to view block "css" or provide
429: * custom block name.
430: * - `once` Whether or not the css file should be checked for uniqueness. If true css
431: * files will only be included once, use false to allow the same
432: * css to be included more than once per request.
433: * - `plugin` False value will prevent parsing path as a plugin
434: * - `rel` Defaults to 'stylesheet'. If equal to 'import' the stylesheet will be imported.
435: * - `fullBase` If true the URL will get a full address for the css file.
436: *
437: * @param string|string[] $path The name of a CSS style sheet or an array containing names of
438: * CSS stylesheets. If `$path` is prefixed with '/', the path will be relative to the webroot
439: * of your application. Otherwise, the path will be relative to your CSS path, usually webroot/css.
440: * @param array $options Array of options and HTML arguments.
441: * @return string|null CSS `<link />` or `<style />` tag, depending on the type of link.
442: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#linking-to-css-files
443: */
444: public function css($path, array $options = [])
445: {
446: $options += ['once' => true, 'block' => null, 'rel' => 'stylesheet'];
447:
448: if (is_array($path)) {
449: $out = '';
450: foreach ($path as $i) {
451: $out .= "\n\t" . $this->css($i, $options);
452: }
453: if (empty($options['block'])) {
454: return $out . "\n";
455: }
456:
457: return null;
458: }
459:
460: if (strpos($path, '//') !== false) {
461: $url = $path;
462: } else {
463: $url = $this->Url->css($path, $options);
464: $options = array_diff_key($options, ['fullBase' => null, 'pathPrefix' => null]);
465: }
466:
467: if ($options['once'] && isset($this->_includedAssets[__METHOD__][$path])) {
468: return null;
469: }
470: unset($options['once']);
471: $this->_includedAssets[__METHOD__][$path] = true;
472: $templater = $this->templater();
473:
474: if ($options['rel'] === 'import') {
475: $out = $templater->format('style', [
476: 'attrs' => $templater->formatAttributes($options, ['rel', 'block']),
477: 'content' => '@import url(' . $url . ');',
478: ]);
479: } else {
480: $out = $templater->format('css', [
481: 'rel' => $options['rel'],
482: 'url' => $url,
483: 'attrs' => $templater->formatAttributes($options, ['rel', 'block']),
484: ]);
485: }
486:
487: if (empty($options['block'])) {
488: return $out;
489: }
490: if ($options['block'] === true) {
491: $options['block'] = __FUNCTION__;
492: }
493: $this->_View->append($options['block'], $out);
494: }
495:
496: /**
497: * Returns one or many `<script>` tags depending on the number of scripts given.
498: *
499: * If the filename is prefixed with "/", the path will be relative to the base path of your
500: * application. Otherwise, the path will be relative to your JavaScript path, usually webroot/js.
501: *
502: * ### Usage
503: *
504: * Include one script file:
505: *
506: * ```
507: * echo $this->Html->script('styles.js');
508: * ```
509: *
510: * Include multiple script files:
511: *
512: * ```
513: * echo $this->Html->script(['one.js', 'two.js']);
514: * ```
515: *
516: * Add the script file to a custom block:
517: *
518: * ```
519: * $this->Html->script('styles.js', ['block' => 'bodyScript']);
520: * ```
521: *
522: * ### Options
523: *
524: * - `block` Set to true to append output to view block "script" or provide
525: * custom block name.
526: * - `once` Whether or not the script should be checked for uniqueness. If true scripts will only be
527: * included once, use false to allow the same script to be included more than once per request.
528: * - `plugin` False value will prevent parsing path as a plugin
529: * - `fullBase` If true the url will get a full address for the script file.
530: *
531: * @param string|string[] $url String or array of javascript files to include
532: * @param array $options Array of options, and html attributes see above.
533: * @return string|null String of `<script />` tags or null if block is specified in options
534: * or if $once is true and the file has been included before.
535: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#linking-to-javascript-files
536: */
537: public function script($url, array $options = [])
538: {
539: $defaults = ['block' => null, 'once' => true];
540: $options += $defaults;
541:
542: if (is_array($url)) {
543: $out = '';
544: foreach ($url as $i) {
545: $out .= "\n\t" . $this->script($i, $options);
546: }
547: if (empty($options['block'])) {
548: return $out . "\n";
549: }
550:
551: return null;
552: }
553:
554: if (strpos($url, '//') === false) {
555: $url = $this->Url->script($url, $options);
556: $options = array_diff_key($options, ['fullBase' => null, 'pathPrefix' => null]);
557: }
558: if ($options['once'] && isset($this->_includedAssets[__METHOD__][$url])) {
559: return null;
560: }
561: $this->_includedAssets[__METHOD__][$url] = true;
562:
563: $out = $this->formatTemplate('javascriptlink', [
564: 'url' => $url,
565: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'once']),
566: ]);
567:
568: if (empty($options['block'])) {
569: return $out;
570: }
571: if ($options['block'] === true) {
572: $options['block'] = __FUNCTION__;
573: }
574: $this->_View->append($options['block'], $out);
575: }
576:
577: /**
578: * Wrap $script in a script tag.
579: *
580: * ### Options
581: *
582: * - `safe` (boolean) Whether or not the $script should be wrapped in `<![CDATA[ ]]>`.
583: * Defaults to `false`.
584: * - `block` Set to true to append output to view block "script" or provide
585: * custom block name.
586: *
587: * @param string $script The script to wrap
588: * @param array $options The options to use. Options not listed above will be
589: * treated as HTML attributes.
590: * @return string|null String or null depending on the value of `$options['block']`
591: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-inline-javascript-blocks
592: */
593: public function scriptBlock($script, array $options = [])
594: {
595: $options += ['safe' => false, 'block' => null];
596: if ($options['safe']) {
597: $script = "\n" . '//<![CDATA[' . "\n" . $script . "\n" . '//]]>' . "\n";
598: }
599: unset($options['safe']);
600:
601: $out = $this->formatTemplate('javascriptblock', [
602: 'attrs' => $this->templater()->formatAttributes($options, ['block']),
603: 'content' => $script
604: ]);
605:
606: if (empty($options['block'])) {
607: return $out;
608: }
609: if ($options['block'] === true) {
610: $options['block'] = 'script';
611: }
612: $this->_View->append($options['block'], $out);
613: }
614:
615: /**
616: * Begin a script block that captures output until HtmlHelper::scriptEnd()
617: * is called. This capturing block will capture all output between the methods
618: * and create a scriptBlock from it.
619: *
620: * ### Options
621: *
622: * - `safe` (boolean) Whether or not the $script should be wrapped in `<![CDATA[ ]]>`.
623: * See scriptBlock().
624: * - `block` Set to true to append output to view block "script" or provide
625: * custom block name.
626: *
627: * @param array $options Options for the code block.
628: * @return void
629: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-inline-javascript-blocks
630: */
631: public function scriptStart(array $options = [])
632: {
633: $this->_scriptBlockOptions = $options;
634: ob_start();
635: }
636:
637: /**
638: * End a Buffered section of JavaScript capturing.
639: * Generates a script tag inline or appends to specified view block depending on
640: * the settings used when the scriptBlock was started
641: *
642: * @return string|null Depending on the settings of scriptStart() either a script tag or null
643: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-inline-javascript-blocks
644: */
645: public function scriptEnd()
646: {
647: $buffer = ob_get_clean();
648: $options = $this->_scriptBlockOptions;
649: $this->_scriptBlockOptions = [];
650:
651: return $this->scriptBlock($buffer, $options);
652: }
653:
654: /**
655: * Builds CSS style data from an array of CSS properties
656: *
657: * ### Usage:
658: *
659: * ```
660: * echo $this->Html->style(['margin' => '10px', 'padding' => '10px'], true);
661: *
662: * // creates
663: * 'margin:10px;padding:10px;'
664: * ```
665: *
666: * @param array $data Style data array, keys will be used as property names, values as property values.
667: * @param bool $oneLine Whether or not the style block should be displayed on one line.
668: * @return string CSS styling data
669: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-css-programatically
670: */
671: public function style(array $data, $oneLine = true)
672: {
673: $out = [];
674: foreach ($data as $key => $value) {
675: $out[] = $key . ':' . $value . ';';
676: }
677: if ($oneLine) {
678: return implode(' ', $out);
679: }
680:
681: return implode("\n", $out);
682: }
683:
684: /**
685: * Returns the breadcrumb trail as a sequence of »-separated links.
686: *
687: * If `$startText` is an array, the accepted keys are:
688: *
689: * - `text` Define the text/content for the link.
690: * - `url` Define the target of the created link.
691: *
692: * All other keys will be passed to HtmlHelper::link() as the `$options` parameter.
693: *
694: * @param string $separator Text to separate crumbs.
695: * @param string|array|bool $startText This will be the first crumb, if false it defaults to first crumb in array. Can
696: * also be an array, see above for details.
697: * @return string|null Composed bread crumbs
698: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
699: * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
700: */
701: public function getCrumbs($separator = '»', $startText = false)
702: {
703: deprecationWarning(
704: 'HtmlHelper::getCrumbs() is deprecated. ' .
705: 'Use the BreadcrumbsHelper instead.'
706: );
707:
708: $crumbs = $this->_prepareCrumbs($startText);
709: if (!empty($crumbs)) {
710: $out = [];
711: foreach ($crumbs as $crumb) {
712: if (!empty($crumb[1])) {
713: $out[] = $this->link($crumb[0], $crumb[1], $crumb[2]);
714: } else {
715: $out[] = $crumb[0];
716: }
717: }
718:
719: return implode($separator, $out);
720: }
721:
722: return null;
723: }
724:
725: /**
726: * Returns breadcrumbs as a (x)html list
727: *
728: * This method uses HtmlHelper::tag() to generate list and its elements. Works
729: * similar to HtmlHelper::getCrumbs(), so it uses options which every
730: * crumb was added with.
731: *
732: * ### Options
733: *
734: * - `separator` Separator content to insert in between breadcrumbs, defaults to ''
735: * - `firstClass` Class for wrapper tag on the first breadcrumb, defaults to 'first'
736: * - `lastClass` Class for wrapper tag on current active page, defaults to 'last'
737: *
738: * @param array $options Array of HTML attributes to apply to the generated list elements.
739: * @param string|array|bool $startText This will be the first crumb, if false it defaults to first crumb in array. Can
740: * also be an array, see `HtmlHelper::getCrumbs` for details.
741: * @return string|null Breadcrumbs HTML list.
742: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
743: * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
744: */
745: public function getCrumbList(array $options = [], $startText = false)
746: {
747: deprecationWarning(
748: 'HtmlHelper::getCrumbList() is deprecated. ' .
749: 'Use the BreadcrumbsHelper instead.'
750: );
751:
752: $defaults = ['firstClass' => 'first', 'lastClass' => 'last', 'separator' => '', 'escape' => true];
753: $options += $defaults;
754: $firstClass = $options['firstClass'];
755: $lastClass = $options['lastClass'];
756: $separator = $options['separator'];
757: $escape = $options['escape'];
758: unset($options['firstClass'], $options['lastClass'], $options['separator'], $options['escape']);
759:
760: $crumbs = $this->_prepareCrumbs($startText, $escape);
761: if (empty($crumbs)) {
762: return null;
763: }
764:
765: $result = '';
766: $crumbCount = count($crumbs);
767: $ulOptions = $options;
768: foreach ($crumbs as $which => $crumb) {
769: $options = [];
770: if (empty($crumb[1])) {
771: $elementContent = $crumb[0];
772: } else {
773: $elementContent = $this->link($crumb[0], $crumb[1], $crumb[2]);
774: }
775: if (!$which && $firstClass !== false) {
776: $options['class'] = $firstClass;
777: } elseif ($which == $crumbCount - 1 && $lastClass !== false) {
778: $options['class'] = $lastClass;
779: }
780: if (!empty($separator) && ($crumbCount - $which >= 2)) {
781: $elementContent .= $separator;
782: }
783: $result .= $this->formatTemplate('li', [
784: 'content' => $elementContent,
785: 'attrs' => $this->templater()->formatAttributes($options)
786: ]);
787: }
788:
789: return $this->formatTemplate('ul', [
790: 'content' => $result,
791: 'attrs' => $this->templater()->formatAttributes($ulOptions)
792: ]);
793: }
794:
795: /**
796: * Prepends startText to crumbs array if set
797: *
798: * @param string|array|bool $startText Text to prepend
799: * @param bool $escape If the output should be escaped or not
800: * @return array Crumb list including startText (if provided)
801: * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
802: */
803: protected function _prepareCrumbs($startText, $escape = true)
804: {
805: deprecationWarning(
806: 'HtmlHelper::_prepareCrumbs() is deprecated. ' .
807: 'Use the BreadcrumbsHelper instead.'
808: );
809:
810: $crumbs = $this->_crumbs;
811: if ($startText) {
812: if (!is_array($startText)) {
813: $startText = [
814: 'url' => '/',
815: 'text' => $startText
816: ];
817: }
818: $startText += ['url' => '/', 'text' => __d('cake', 'Home')];
819: list($url, $text) = [$startText['url'], $startText['text']];
820: unset($startText['url'], $startText['text']);
821: array_unshift($crumbs, [$text, $url, $startText + ['escape' => $escape]]);
822: }
823:
824: return $crumbs;
825: }
826:
827: /**
828: * Creates a formatted IMG element.
829: *
830: * This method will set an empty alt attribute if one is not supplied.
831: *
832: * ### Usage:
833: *
834: * Create a regular image:
835: *
836: * ```
837: * echo $this->Html->image('cake_icon.png', ['alt' => 'CakePHP']);
838: * ```
839: *
840: * Create an image link:
841: *
842: * ```
843: * echo $this->Html->image('cake_icon.png', ['alt' => 'CakePHP', 'url' => 'https://cakephp.org']);
844: * ```
845: *
846: * ### Options:
847: *
848: * - `url` If provided an image link will be generated and the link will point at
849: * `$options['url']`.
850: * - `fullBase` If true the src attribute will get a full address for the image file.
851: * - `plugin` False value will prevent parsing path as a plugin
852: *
853: * @param string|array $path Path to the image file, relative to the app/webroot/img/ directory.
854: * @param array $options Array of HTML attributes. See above for special options.
855: * @return string completed img tag
856: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#linking-to-images
857: */
858: public function image($path, array $options = [])
859: {
860: $path = $this->Url->image($path, $options);
861: $options = array_diff_key($options, ['fullBase' => null, 'pathPrefix' => null]);
862:
863: if (!isset($options['alt'])) {
864: $options['alt'] = '';
865: }
866:
867: $url = false;
868: if (!empty($options['url'])) {
869: $url = $options['url'];
870: unset($options['url']);
871: }
872:
873: $templater = $this->templater();
874: $image = $templater->format('image', [
875: 'url' => $path,
876: 'attrs' => $templater->formatAttributes($options),
877: ]);
878:
879: if ($url) {
880: return $templater->format('link', [
881: 'url' => $this->Url->build($url),
882: 'attrs' => null,
883: 'content' => $image
884: ]);
885: }
886:
887: return $image;
888: }
889:
890: /**
891: * Returns a row of formatted and named TABLE headers.
892: *
893: * @param array $names Array of tablenames. Each tablename also can be a key that points to an array with a set
894: * of attributes to its specific tag
895: * @param array|null $trOptions HTML options for TR elements.
896: * @param array|null $thOptions HTML options for TH elements.
897: * @return string Completed table headers
898: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-table-headings
899: */
900: public function tableHeaders(array $names, array $trOptions = null, array $thOptions = null)
901: {
902: $out = [];
903: foreach ($names as $arg) {
904: if (!is_array($arg)) {
905: $out[] = $this->formatTemplate('tableheader', [
906: 'attrs' => $this->templater()->formatAttributes($thOptions),
907: 'content' => $arg
908: ]);
909: } else {
910: $out[] = $this->formatTemplate('tableheader', [
911: 'attrs' => $this->templater()->formatAttributes(current($arg)),
912: 'content' => key($arg)
913: ]);
914: }
915: }
916:
917: return $this->tableRow(implode(' ', $out), (array)$trOptions);
918: }
919:
920: /**
921: * Returns a formatted string of table rows (TR's with TD's in them).
922: *
923: * @param array|string $data Array of table data
924: * @param array|bool|null $oddTrOptions HTML options for odd TR elements if true useCount is used
925: * @param array|bool|null $evenTrOptions HTML options for even TR elements
926: * @param bool $useCount adds class "column-$i"
927: * @param bool $continueOddEven If false, will use a non-static $count variable,
928: * so that the odd/even count is reset to zero just for that call.
929: * @return string Formatted HTML
930: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-table-cells
931: */
932: public function tableCells($data, $oddTrOptions = null, $evenTrOptions = null, $useCount = false, $continueOddEven = true)
933: {
934: if (empty($data[0]) || !is_array($data[0])) {
935: $data = [$data];
936: }
937:
938: if ($oddTrOptions === true) {
939: $useCount = true;
940: $oddTrOptions = null;
941: }
942:
943: if ($evenTrOptions === false) {
944: $continueOddEven = false;
945: $evenTrOptions = null;
946: }
947:
948: if ($continueOddEven) {
949: static $count = 0;
950: } else {
951: $count = 0;
952: }
953:
954: $out = [];
955: foreach ($data as $line) {
956: $count++;
957: $cellsOut = $this->_renderCells($line, $useCount);
958: $opts = $count % 2 ? $oddTrOptions : $evenTrOptions;
959: $out[] = $this->tableRow(implode(' ', $cellsOut), (array)$opts);
960: }
961:
962: return implode("\n", $out);
963: }
964:
965: /**
966: * Renders cells for a row of a table.
967: *
968: * This is a helper method for tableCells(). Overload this method as you
969: * need to change the behavior of the cell rendering.
970: *
971: * @param array $line Line data to render.
972: * @param bool $useCount Renders the count into the row. Default is false.
973: * @return string[]
974: */
975: protected function _renderCells($line, $useCount = false)
976: {
977: $i = 0;
978: $cellsOut = [];
979: foreach ($line as $cell) {
980: $cellOptions = [];
981:
982: if (is_array($cell)) {
983: $cellOptions = $cell[1];
984: $cell = $cell[0];
985: }
986:
987: if ($useCount) {
988: $i += 1;
989: if (isset($cellOptions['class'])) {
990: $cellOptions['class'] .= ' column-' . $i;
991: } else {
992: $cellOptions['class'] = 'column-' . $i;
993: }
994: }
995:
996: $cellsOut[] = $this->tableCell($cell, $cellOptions);
997: }
998:
999: return $cellsOut;
1000: }
1001:
1002: /**
1003: * Renders a single table row (A TR with attributes).
1004: *
1005: * @param string $content The content of the row.
1006: * @param array $options HTML attributes.
1007: * @return string
1008: */
1009: public function tableRow($content, array $options = [])
1010: {
1011: return $this->formatTemplate('tablerow', [
1012: 'attrs' => $this->templater()->formatAttributes($options),
1013: 'content' => $content
1014: ]);
1015: }
1016:
1017: /**
1018: * Renders a single table cell (A TD with attributes).
1019: *
1020: * @param string $content The content of the cell.
1021: * @param array $options HTML attributes.
1022: * @return string
1023: */
1024: public function tableCell($content, array $options = [])
1025: {
1026: return $this->formatTemplate('tablecell', [
1027: 'attrs' => $this->templater()->formatAttributes($options),
1028: 'content' => $content
1029: ]);
1030: }
1031:
1032: /**
1033: * Returns a formatted block tag, i.e DIV, SPAN, P.
1034: *
1035: * ### Options
1036: *
1037: * - `escape` Whether or not the contents should be html_entity escaped.
1038: *
1039: * @param string $name Tag name.
1040: * @param string|null $text String content that will appear inside the div element.
1041: * If null, only a start tag will be printed
1042: * @param array $options Additional HTML attributes of the DIV tag, see above.
1043: * @return string The formatted tag element
1044: */
1045: public function tag($name, $text = null, array $options = [])
1046: {
1047: if (empty($name)) {
1048: return $text;
1049: }
1050: if (isset($options['escape']) && $options['escape']) {
1051: $text = h($text);
1052: unset($options['escape']);
1053: }
1054: if ($text === null) {
1055: $tag = 'tagstart';
1056: } else {
1057: $tag = 'tag';
1058: }
1059:
1060: return $this->formatTemplate($tag, [
1061: 'attrs' => $this->templater()->formatAttributes($options),
1062: 'tag' => $name,
1063: 'content' => $text,
1064: ]);
1065: }
1066:
1067: /**
1068: * Returns a formatted DIV tag for HTML FORMs.
1069: *
1070: * ### Options
1071: *
1072: * - `escape` Whether or not the contents should be html_entity escaped.
1073: *
1074: * @param string|null $class CSS class name of the div element.
1075: * @param string|null $text String content that will appear inside the div element.
1076: * If null, only a start tag will be printed
1077: * @param array $options Additional HTML attributes of the DIV tag
1078: * @return string The formatted DIV element
1079: */
1080: public function div($class = null, $text = null, array $options = [])
1081: {
1082: if (!empty($class)) {
1083: $options['class'] = $class;
1084: }
1085:
1086: return $this->tag('div', $text, $options);
1087: }
1088:
1089: /**
1090: * Returns a formatted P tag.
1091: *
1092: * ### Options
1093: *
1094: * - `escape` Whether or not the contents should be html_entity escaped.
1095: *
1096: * @param string $class CSS class name of the p element.
1097: * @param string $text String content that will appear inside the p element.
1098: * @param array $options Additional HTML attributes of the P tag
1099: * @return string The formatted P element
1100: */
1101: public function para($class, $text, array $options = [])
1102: {
1103: if (!empty($options['escape'])) {
1104: $text = h($text);
1105: }
1106: if ($class && !empty($class)) {
1107: $options['class'] = $class;
1108: }
1109: $tag = 'para';
1110: if ($text === null) {
1111: $tag = 'parastart';
1112: }
1113:
1114: return $this->formatTemplate($tag, [
1115: 'attrs' => $this->templater()->formatAttributes($options),
1116: 'content' => $text,
1117: ]);
1118: }
1119:
1120: /**
1121: * Returns an audio/video element
1122: *
1123: * ### Usage
1124: *
1125: * Using an audio file:
1126: *
1127: * ```
1128: * echo $this->Html->media('audio.mp3', ['fullBase' => true]);
1129: * ```
1130: *
1131: * Outputs:
1132: *
1133: * ```
1134: * <video src="http://www.somehost.com/files/audio.mp3">Fallback text</video>
1135: * ```
1136: *
1137: * Using a video file:
1138: *
1139: * ```
1140: * echo $this->Html->media('video.mp4', ['text' => 'Fallback text']);
1141: * ```
1142: *
1143: * Outputs:
1144: *
1145: * ```
1146: * <video src="/files/video.mp4">Fallback text</video>
1147: * ```
1148: *
1149: * Using multiple video files:
1150: *
1151: * ```
1152: * echo $this->Html->media(
1153: * ['video.mp4', ['src' => 'video.ogv', 'type' => "video/ogg; codecs='theora, vorbis'"]],
1154: * ['tag' => 'video', 'autoplay']
1155: * );
1156: * ```
1157: *
1158: * Outputs:
1159: *
1160: * ```
1161: * <video autoplay="autoplay">
1162: * <source src="/files/video.mp4" type="video/mp4"/>
1163: * <source src="/files/video.ogv" type="video/ogv; codecs='theora, vorbis'"/>
1164: * </video>
1165: * ```
1166: *
1167: * ### Options
1168: *
1169: * - `tag` Type of media element to generate, either "audio" or "video".
1170: * If tag is not provided it's guessed based on file's mime type.
1171: * - `text` Text to include inside the audio/video tag
1172: * - `pathPrefix` Path prefix to use for relative URLs, defaults to 'files/'
1173: * - `fullBase` If provided the src attribute will get a full address including domain name
1174: *
1175: * @param string|array $path Path to the video file, relative to the webroot/{$options['pathPrefix']} directory.
1176: * Or an array where each item itself can be a path string or an associate array containing keys `src` and `type`
1177: * @param array $options Array of HTML attributes, and special options above.
1178: * @return string Generated media element
1179: */
1180: public function media($path, array $options = [])
1181: {
1182: $options += [
1183: 'tag' => null,
1184: 'pathPrefix' => 'files/',
1185: 'text' => ''
1186: ];
1187:
1188: if (!empty($options['tag'])) {
1189: $tag = $options['tag'];
1190: } else {
1191: $tag = null;
1192: }
1193:
1194: if (is_array($path)) {
1195: $sourceTags = '';
1196: foreach ($path as &$source) {
1197: if (is_string($source)) {
1198: $source = [
1199: 'src' => $source,
1200: ];
1201: }
1202: if (!isset($source['type'])) {
1203: $ext = pathinfo($source['src'], PATHINFO_EXTENSION);
1204: $source['type'] = $this->response->getMimeType($ext);
1205: }
1206: $source['src'] = $this->Url->assetUrl($source['src'], $options);
1207: $sourceTags .= $this->formatTemplate('tagselfclosing', [
1208: 'tag' => 'source',
1209: 'attrs' => $this->templater()->formatAttributes($source)
1210: ]);
1211: }
1212: unset($source);
1213: $options['text'] = $sourceTags . $options['text'];
1214: unset($options['fullBase']);
1215: } else {
1216: if (empty($path) && !empty($options['src'])) {
1217: $path = $options['src'];
1218: }
1219: $options['src'] = $this->Url->assetUrl($path, $options);
1220: }
1221:
1222: if ($tag === null) {
1223: if (is_array($path)) {
1224: $mimeType = $path[0]['type'];
1225: } else {
1226: $mimeType = $this->response->getMimeType(pathinfo($path, PATHINFO_EXTENSION));
1227: }
1228: if (preg_match('#^video/#', $mimeType)) {
1229: $tag = 'video';
1230: } else {
1231: $tag = 'audio';
1232: }
1233: }
1234:
1235: if (isset($options['poster'])) {
1236: $options['poster'] = $this->Url->assetUrl($options['poster'], ['pathPrefix' => Configure::read('App.imageBaseUrl')] + $options);
1237: }
1238: $text = $options['text'];
1239:
1240: $options = array_diff_key($options, [
1241: 'tag' => null,
1242: 'fullBase' => null,
1243: 'pathPrefix' => null,
1244: 'text' => null
1245: ]);
1246:
1247: return $this->tag($tag, $text, $options);
1248: }
1249:
1250: /**
1251: * Build a nested list (UL/OL) out of an associative array.
1252: *
1253: * Options for $options:
1254: *
1255: * - `tag` - Type of list tag to use (ol/ul)
1256: *
1257: * Options for $itemOptions:
1258: *
1259: * - `even` - Class to use for even rows.
1260: * - `odd` - Class to use for odd rows.
1261: *
1262: * @param array $list Set of elements to list
1263: * @param array $options Options and additional HTML attributes of the list (ol/ul) tag.
1264: * @param array $itemOptions Options and additional HTML attributes of the list item (LI) tag.
1265: * @return string The nested list
1266: * @link https://book.cakephp.org/3.0/en/views/helpers/html.html#creating-nested-lists
1267: */
1268: public function nestedList(array $list, array $options = [], array $itemOptions = [])
1269: {
1270: $options += ['tag' => 'ul'];
1271: $items = $this->_nestedListItem($list, $options, $itemOptions);
1272:
1273: return $this->formatTemplate($options['tag'], [
1274: 'attrs' => $this->templater()->formatAttributes($options, ['tag']),
1275: 'content' => $items
1276: ]);
1277: }
1278:
1279: /**
1280: * Internal function to build a nested list (UL/OL) out of an associative array.
1281: *
1282: * @param array $items Set of elements to list.
1283: * @param array $options Additional HTML attributes of the list (ol/ul) tag.
1284: * @param array $itemOptions Options and additional HTML attributes of the list item (LI) tag.
1285: * @return string The nested list element
1286: * @see \Cake\View\Helper\HtmlHelper::nestedList()
1287: */
1288: protected function _nestedListItem($items, $options, $itemOptions)
1289: {
1290: $out = '';
1291:
1292: $index = 1;
1293: foreach ($items as $key => $item) {
1294: if (is_array($item)) {
1295: $item = $key . $this->nestedList($item, $options, $itemOptions);
1296: }
1297: if (isset($itemOptions['even']) && $index % 2 === 0) {
1298: $itemOptions['class'] = $itemOptions['even'];
1299: } elseif (isset($itemOptions['odd']) && $index % 2 !== 0) {
1300: $itemOptions['class'] = $itemOptions['odd'];
1301: }
1302: $out .= $this->formatTemplate('li', [
1303: 'attrs' => $this->templater()->formatAttributes($itemOptions, ['even', 'odd']),
1304: 'content' => $item
1305: ]);
1306: $index++;
1307: }
1308:
1309: return $out;
1310: }
1311:
1312: /**
1313: * Event listeners.
1314: *
1315: * @return array
1316: */
1317: public function implementedEvents()
1318: {
1319: return [];
1320: }
1321: }
1322: