Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
MakeTag
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
3 / 3
11
100.00% covered (success)
100.00%
1 / 1
 __invoke
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 verifyTagName
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 compileTag
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * Make tag view helper
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2020.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  View_Helpers
25 * @author   Chris Hallberg <crhallberg@gmail.com>
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org/wiki/development Wiki
29 */
30
31namespace VuFind\View\Helper\Root;
32
33use function in_array;
34use function is_array;
35
36/**
37 * Make tag view helper
38 *
39 * @category VuFind
40 * @package  View_Helpers
41 * @author   Chris Hallberg <crhallberg@gmail.com>
42 * @author   Demian Katz <demian.katz@villanova.edu>
43 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
44 * @link     https://vufind.org/wiki/development Wiki
45 */
46class MakeTag extends \Laminas\View\Helper\AbstractHelper
47{
48    /**
49     * List of all valid body tags
50     *
51     * Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
52     * Last checked: September 27, 2022
53     *
54     * @var string[]
55     */
56    protected $validBodyTags = [
57        'a',
58        'abbr',
59        'acronym',
60        'address',
61        'applet',
62        'area',
63        'article',
64        'aside',
65        'audio',
66        'b',
67        'base',
68        'bdi',
69        'bdo',
70        'bgsound',
71        'big',
72        'blink',
73        'blockquote',
74        'body',
75        'br',
76        'button',
77        'canvas',
78        'caption',
79        'center',
80        'cite',
81        'code',
82        'col',
83        'colgroup',
84        'content',
85        'data',
86        'datalist',
87        'dd',
88        'del',
89        'details',
90        'dfn',
91        'dialog',
92        'dir',
93        'div',
94        'dl',
95        'dt',
96        'em',
97        'embed',
98        'fieldset',
99        'figcaption',
100        'figure',
101        'font',
102        'footer',
103        'form',
104        'frame',
105        'frameset',
106        'h1',
107        'h2',
108        'h3',
109        'h4',
110        'h5',
111        'h6',
112        'head',
113        'header',
114        'hgroup',
115        'hr',
116        'html',
117        'i',
118        'iframe',
119        'image',
120        'img',
121        'input',
122        'ins',
123        'kbd',
124        'keygen',
125        'label',
126        'legend',
127        'li',
128        'link',
129        'main',
130        'map',
131        'mark',
132        'marquee',
133        'math',
134        'menu',
135        'menuitem',
136        'meta',
137        'meter',
138        'nav',
139        'nobr',
140        'noembed',
141        'noframes',
142        'noscript',
143        'object',
144        'ol',
145        'optgroup',
146        'option',
147        'output',
148        'p',
149        'param',
150        'picture',
151        'plaintext',
152        'portal',
153        'pre',
154        'progress',
155        'q',
156        'rb',
157        'rp',
158        'rt',
159        'rtc',
160        'ruby',
161        's',
162        'samp',
163        'script',
164        'section',
165        'select',
166        'shadow',
167        'slot',
168        'small',
169        'source',
170        'spacer',
171        'span',
172        'strike',
173        'strong',
174        'style',
175        'sub',
176        'summary',
177        'sup',
178        'svg',
179        'table',
180        'tbody',
181        'td',
182        'template',
183        'textarea',
184        'tfoot',
185        'th',
186        'thead',
187        'time',
188        'title',
189        'tr',
190        'track',
191        'tt',
192        'u',
193        'ul',
194        'var',
195        'video',
196        'wbr',
197        'xmp',
198    ];
199
200    /**
201     * List of all void tags (tags that access no innerHTML)
202     *
203     * Source: https://html.spec.whatwg.org/multipage/syntax.html#void-elements
204     * Last checked: September 27, 2022
205     *
206     * @var string[]
207     */
208    protected $voidElements = [
209        'area',
210        'base',
211        'br',
212        'col',
213        'embed',
214        'hr',
215        'img',
216        'input',
217        'link',
218        'meta',
219        'param', // deprecated, but included for back-compatibility
220        'source',
221        'track',
222        'wbr',
223    ];
224
225    /**
226     * List of deprecated elements that should be replaced.
227     *
228     * Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
229     * Last checked: September 27, 2022
230     *
231     * @var string[]
232     */
233    protected $deprecatedElements = [
234        'acronym',
235        'applet',
236        'bgsound',
237        'big',
238        'blink',
239        'center',
240        'content',
241        'dir',
242        'font',
243        'frame',
244        'frameset',
245        'image',
246        'keygen',
247        'marquee',
248        'menuitem',
249        'nobr',
250        'noembed',
251        'noframes',
252        'param',
253        'plaintext',
254        'rb',
255        'rtc',
256        'shadow',
257        'spacer',
258        'strike',
259        'tt',
260        'xmp',
261    ];
262
263    /**
264     * Render an HTML tag
265     *
266     * A string passed into $attrs will be treated like a class.
267     * These two are equivalent:
268     * > MakeTag('div', 'Success!', 'alert alert-success')
269     * > MakeTag('div', 'Success!', ['class => 'alert alert-success'])
270     *
271     * Additional options
272     * - escapeContent: Default true, set to false to skip escaping (like for HTML).
273     *
274     * @param string       $tagName  Element tag name
275     * @param string       $contents Element contents (must be properly-formed HTML)
276     * @param string|array $attrs    Tag attributes (associative array or class name)
277     * @param array        $options  Additional options
278     *
279     * @return string HTML for an anchor tag
280     */
281    public function __invoke(
282        string $tagName,
283        string $contents,
284        $attrs = [],
285        $options = []
286    ) {
287        // $attrs not an object, interpret as class name
288        if (!is_array($attrs)) {
289            $attrs = !empty($attrs) ? ['class' => $attrs] : [];
290        }
291
292        // Compile attributes
293        return $this->compileTag($tagName, $contents, $attrs, $options);
294    }
295
296    /**
297     * Verify HTML tag matches HTML spec
298     *
299     * @param string $tagName Element tag name
300     *
301     * @return void
302     */
303    protected function verifyTagName(string $tagName)
304    {
305        // Simplify check by making tag lowercase
306        $lowerTagName = mb_strtolower($tagName, 'UTF-8');
307
308        // Existing tag?
309        if (in_array($lowerTagName, $this->validBodyTags)) {
310            // Deprecated tag? Throw warning.
311            if (in_array($lowerTagName, $this->deprecatedElements)) {
312                trigger_error(
313                    "'<$lowerTagName>' is deprecated and should be replaced.",
314                    E_USER_WARNING
315                );
316            }
317
318            return;
319        }
320
321        // Check if it's a valid custom element
322        // Spec: https://html.spec.whatwg.org/#autonomous-custom-element
323
324        // All valid characters for a Potential Custom Element Name
325        // Concated for clarity (space not a valid character)
326        $PCENChar = '[' .
327            '\-\.0-9_a-z' .
328            '\x{B7}' .
329            '\x{C0}-\x{D6}' .
330            '\x{D8}-\x{F6}' .
331            '\x{F8}-\x{37D}' .
332            '\x{37F}-\x{1FFF}' .
333            '\x{200C}-\x{200D}' .
334            '\x{203F}-\x{2040}' .
335            '\x{2070}-\x{218F}' .
336            '\x{2C00}-\x{2FEF}' .
337            '\x{3001}-\x{D7FF}' .
338            '\x{F900}-\x{FDCF}' .
339            '\x{FDF0}-\x{FFFD}' .
340            '\x{10000}-\x{EFFFF}' .
341            ']*';
342
343        // First character must be a letter (uppercase or lowercase)
344        // Needs one hyphen to designate custom element, more groups valid
345        $validCustomTagPattern = '/^[a-z]' . $PCENChar . '(\-' . $PCENChar . ')+$/u';
346
347        // Is valid custom tag?
348        if (!preg_match($validCustomTagPattern, $lowerTagName)) {
349            throw new \InvalidArgumentException('Invalid tag name: ' . $tagName);
350        }
351    }
352
353    /**
354     * Turn associative array into a string of attributes in an anchor
355     *
356     * Additional options
357     * - escapeContent: Default true, set to false to skip escaping (like for HTML).
358     *
359     * @param string $tagName  HTML tag name
360     * @param string $contents InnerHTML
361     * @param array  $attrs    Tag attributes (associative array)
362     * @param array  $options  Additional options
363     *
364     * @return string
365     */
366    protected function compileTag(
367        string $tagName,
368        string $contents,
369        $attrs = [],
370        $options = []
371    ) {
372        $this->verifyTagName($tagName);
373
374        $htmlAttrs = $this->getView()->plugin('htmlAttributes')($attrs);
375
376        if (empty($contents) && in_array($tagName, $this->voidElements)) {
377            return '<' . $tagName . $htmlAttrs . '>';
378        }
379
380        // Special option: escape content
381        if ($options['escapeContent'] ?? true) {
382            $contents = $this->getView()->plugin('escapeHtml')($contents);
383        }
384
385        return '<' . $tagName . $htmlAttrs . '>' . $contents . '</' . $tagName . '>';
386    }
387}