Issues (806)

lib/midcom/helper/head.php (1 issue)

Labels
Severity
1
<?php
2
/**
3
 * @package midcom.helper
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
use Symfony\Component\HttpKernel\KernelEvents;
10
use Symfony\Component\HttpKernel\Event\ResponseEvent;
11
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
12
13
/**
14
 * Helper functions for managing HTML head
15
 *
16
 * @package midcom.helper
17
 */
18
class midcom_helper_head implements EventSubscriberInterface
19
{
20
    /**
21
     * Array with all JavaScript declarations for the page's head.
22
     */
23
    private array $_jshead = [];
24
25
    /**
26
     * Array with all JavaScript file inclusions.
27
     */
28
    private array $_jsfiles = [];
29
30
    /**
31
     * Array with all prepend JavaScript declarations for the page's head.
32
     */
33
    private array $_prepend_jshead = [];
34
35
    /**
36
     * Boolean showing if jQuery is enabled
37
     */
38
    private bool $_jquery_enabled = false;
39
40
    /**
41
     * Array with all JQuery state scripts for the page's head.
42
     */
43
    private array $_jquery_states = [];
44
45
    /**
46
     * Array with all linked URLs for HEAD.
47
     */
48
    private array $_linkhrefs = [];
49
50
    /**
51
     * Array with all methods for the BODY's onload event.
52
     */
53
    private array $_jsonload = [];
54
55
    /**
56
     * string with all metatags to go into the page head.
57
     */
58
    private string $_meta_head = '';
59
60
    /**
61
     * String with all css styles to go into a page's head.
62
     */
63
    private string $_style_head = '';
64
65
    /**
66
     * Array with all link elements to be included in a page's head.
67
     */
68
    private array $_link_head = [];
69
70
    const HEAD_PLACEHOLDER = '<!-- MIDCOM_HEAD_ELEMENTS -->';
71
72
    private static bool $placeholder_added = false;
73
74
    private string $cachebusting = '';
75
76
    public static function getSubscribedEvents()
77
    {
78
        return [KernelEvents::RESPONSE => ['inject_head_elements']];
79
    }
80
81
    /**
82
     * Sets the page title for the current context.
83
     *
84
     * This can be retrieved by accessing the component context key
85
     * MIDCOM_CONTEXT_PAGETITLE.
86
     */
87 144
    public function set_pagetitle(string $string)
88
    {
89 144
        midcom_core_context::get()->set_key(MIDCOM_CONTEXT_PAGETITLE, $string);
90
    }
91
92
    /**
93
     * Register JavaScript File for referring in the page.
94
     *
95
     * This allows MidCOM components to register JavaScript code
96
     * during page processing. The site style code can then query this queued-up code
97
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
98
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
99
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
100
     * BODY-tag. Note, that these suggestions are not enforced, if you want a JScript
101
     * clean site, just omit the print calls and you should be fine in almost all
102
     * cases.
103
     *
104
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
105
     *
106
     * @see add_jscript()
107
     * @see add_jsonload()
108
     * @see print_head_elements()
109
     * @see print_jsonload()
110
     */
111 252
    public function add_jsfile(string $url, bool $prepend = false)
112
    {
113
        // Adds a URL for a <script type="text/javascript" src="tinymce.js"></script>
114
        // like call. $url is inserted into src. Duplicates are omitted.
115 252
        if (!in_array($url, $this->_jsfiles)) {
116 33
            $this->_jsfiles[] = $url;
117 33
            $js_call = ['url' => $url];
118 33
            if ($prepend) {
119
                // Add the javascript include to the beginning, not the end of array
120
                array_unshift($this->_jshead, $js_call);
121
            } else {
122 33
                $this->_jshead[] = $js_call;
123
            }
124
        }
125
    }
126
127
    /**
128
     * Register JavaScript Code for output directly in the page.
129
     *
130
     * This allows components to register JavaScript code
131
     * during page processing. The site style can then query this queued-up code
132
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
133
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
134
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
135
     * BODY-tag. Note, that these suggestions are not enforced
136
     *
137
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
138
     *
139
     * @see add_jsfile()
140
     * @see add_jsonload()
141
     * @see print_head_elements()
142
     * @see print_jsonload()
143
     */
144 77
    public function add_jscript(string $script, $defer = '', bool $prepend = false)
145
    {
146 77
        $js_call = ['content' => trim($script), 'defer' => $defer];
147 77
        if ($prepend) {
148
            $this->_prepend_jshead[] = $js_call;
149
        } else {
150 77
            $this->_jshead[] = $js_call;
151
        }
152
    }
153
154
    /**
155
     * Register JavaScript snippets to jQuery states.
156
     *
157
     * This allows components to register JavaScript code to the jQuery states.
158
     * Possible ready states: document.ready
159
     *
160
     * @see print_jquery_statuses()
161
     */
162 3
    public function add_jquery_state_script(string $script, string $state = 'document.ready')
163
    {
164 3
        $this->_jquery_states[$state] ??= '';
165 3
        $this->_jquery_states[$state] .= "\n" . trim($script) . "\n";
166
    }
167
168
    /**
169
     *  Register a metatag to be added to the head element.
170
     *  This allows components to register metatags to be placed in the
171
     *  head section of the page.
172
     *
173
     *  @param  array  $attributes Array of attribute => value pairs to be placed in the tag.
174
     *  @see print_head_elements()
175
     */
176
    public function add_meta_head(array $attributes)
177
    {
178
        $this->_meta_head .= '<meta' . $this->_get_attribute_string($attributes) . ' />' . "\n";
179
    }
180
181
    /**
182
     * Register a styleblock / style link  to be added to the head element.
183
     * This allows components to register extra CSS sheets they wants to include.
184
     * in the head section of the page.
185
     *
186
     * @param  string $script    The input between the <style></style> tags.
187
     * @param  array  $attributes Array of attribute=> value pairs to be placed in the tag.
188
     * @see print_head_elements()
189
     */
190
    public function add_style_head(string $script, array $attributes = [])
191
    {
192
        $this->_style_head .= '<style type="text/css"' . $this->_get_attribute_string($attributes) . '>' . $script . "</style>\n";
193
    }
194
195 8
    private function _get_attribute_string(array $attributes) : string
196
    {
197 8
        $string = '';
198 8
        foreach ($attributes as $key => $val) {
199 8
            if ($this->cachebusting && $key === 'href') {
200
                $val .= $this->cachebusting;
201
            }
202 8
            $string .= ' ' . $key . '="' . htmlspecialchars($val, ENT_COMPAT) . '"';
203
        }
204 8
        return $string;
205
    }
206
207
    /**
208
     * Register a link element to be placed in the page head.
209
     *
210
     * This allows components to register extra CSS links.
211
     * Example to use this to include a CSS link:
212
     * <code>
213
     * $attributes = array ('rel' => 'stylesheet',
214
     *                      'type' => 'text/css',
215
     *                      'href' => '/style.css'
216
     *                      );
217
     * midcom::get()->head->add_link_head($attributes);
218
     * </code>
219
     *
220
     * Each URL will only be added once. When trying to add the same URL a second time,
221
     * it will be moved to the end of the stack, so that CSS overrides behave as the developer
222
     * intended
223
     *
224
     * @param  array $attributes Array of attribute => value pairs to be placed in the tag.
225
     * @see print_head_elements()
226
     */
227 295
    public function add_link_head(array $attributes, bool $prepend = false)
228
    {
229 295
        if (!array_key_exists('href', $attributes)) {
230
            return;
231
        }
232
233
        // Register each URL only once
234 295
        if (($key = array_search($attributes['href'], $this->_linkhrefs)) !== false) {
235 287
            unset($this->_linkhrefs[$key]);
236
        }
237 295
        if ($prepend) {
238 234
            array_unshift($this->_linkhrefs, $attributes['href']);
239
        } else {
240 276
            $this->_linkhrefs[] = $attributes['href'];
241
        }
242 295
        $this->_link_head[$attributes['href']] = $attributes;
243
    }
244
245
    /**
246
     * Convenience shortcut for appending CSS files
247
     *
248
     * @param string $media The media type(s) for the stylesheet, if any
249
     */
250 269
    public function add_stylesheet(string $url, ?string $media = null)
251
    {
252 269
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media));
253
    }
254
255
    /**
256
     * Convenience shortcut for prepending CSS files
257
     *
258
     * @param string $media The media type(s) for the stylesheet, if any
259
     */
260 126
    public function prepend_stylesheet(string $url, ?string $media = null)
261
    {
262 126
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media), true);
263
    }
264
265 288
    private function prepare_stylesheet_attributes(string $url, ?string $media) : array
266
    {
267 288
        $attributes = [
268 288
            'rel'  => 'stylesheet',
269 288
            'type' => 'text/css',
270 288
            'href' => $url,
271 288
        ];
272 288
        if ($media) {
273 32
            $attributes['media'] = $media;
274
        }
275 288
        return $attributes;
276
    }
277
278
    /**
279
     * Register a JavaScript method for the body onload event
280
     *
281
     * This allows components to register JavaScript code
282
     * during page processing. The site style can then query this queued-up code
283
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
284
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
285
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
286
     * BODY-tag. Note that these suggestions are not enforced.
287
     *
288
     * @param string $method    The name of the method to be called on page startup, including parameters but excluding the ';'.
289
     * @see add_jsfile()
290
     * @see add_jscript()
291
     * @see print_head_elements()
292
     * @see print_jsonload()
293
     */
294
    public function add_jsonload(string $method)
295
    {
296
        // Adds a method name for <body onload=".."> The string must not end with a ;, it is added automagically
297
        $this->_jsonload[] = $method;
298
    }
299
300
    /**
301
     * Echo the registered javascript code.
302
     *
303
     * This allows components to register JavaScript code
304
     * during page processing. The site style code can then query this queued-up code
305
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
306
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
307
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
308
     * BODY-tag. Note, that these suggestions are not enforced
309
     *
310
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
311
     *
312
     * This is usually called during the BODY region of your style:
313
     *
314
     * <code>
315
     * <html>
316
     *     <body <?php midcom::get()->head->print_jsonload();?>>
317
     *            <!-- your actual body -->
318
     *     </body>
319
     * </html>
320
     * </code>
321
     *
322
     * @see add_jsfile()
323
     * @see add_jscript()
324
     * @see add_jsonload()
325
     * @see print_head_elements()
326
     */
327 23
    public function print_jsonload()
328
    {
329 23
        if (!empty($this->_jsonload)) {
330
            $calls = implode("; ", $this->_jsonload);
331
            echo " onload=\"$calls\" ";
332
        }
333
    }
334
335
    /**
336
     * Marks where the _head elements added should be rendered.
337
     *
338
     * Place the method within the <head> section of your page.
339
     *
340
     * This allows components to register HEAD elements
341
     * during page processing. The site style can then query this queued-up code
342
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
343
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
344
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
345
     * BODY tag. Note that these suggestions are not enforced
346
     *
347
     * @see add_link_head()
348
     * @see add_style_head()
349
     * @see add_meta_head()
350
     * @see add_jsfile()
351
     * @see add_jscript()
352
     */
353 29
    public function print_head_elements(string $cachebusting = '')
354
    {
355 29
        if ($cachebusting) {
356
            $this->cachebusting = '?cb=' . $cachebusting;
357
        }
358 29
        echo self::HEAD_PLACEHOLDER;
359 29
        self::$placeholder_added = true;
360
    }
361
362
    /**
363
     * This function renders the elements added by the various add methods
364
     * and injects them into the response
365
     */
366 352
    public function inject_head_elements(ResponseEvent $event)
367
    {
368 352
        if (!self::$placeholder_added || !$event->isMainRequest()) {
369 352
            return;
370
        }
371
        $response = $event->getResponse();
372
        $content = $response->getContent();
373
374
        $first = strpos($content, self::HEAD_PLACEHOLDER);
375
        if ($first === false) {
376
            return;
377
        }
378
379
        $head = $this->render();
380
        $new_content = substr_replace($content, $head, $first, strlen(self::HEAD_PLACEHOLDER));
381
        $response->setContent($new_content);
0 ignored issues
show
It seems like $new_content can also be of type array; however, parameter $content of Symfony\Component\HttpFo...\Response::setContent() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

381
        $response->setContent(/** @scrutinizer ignore-type */ $new_content);
Loading history...
382
        if ($length = $response->headers->get('Content-Length')) {
383
            $delta = strlen($head) - strlen(self::HEAD_PLACEHOLDER);
384
            $response->headers->set('Content-Length', $length + $delta);
385
        }
386
    }
387
388 8
    public function render() : string
389
    {
390 8
        $head = $this->_meta_head;
391 8
        foreach ($this->_linkhrefs as $url) {
392 8
            $attributes = $this->_link_head[$url];
393 8
            $is_conditional = false;
394
395 8
            if (array_key_exists('condition', $attributes)) {
396
                $head .= "<!--[if {$attributes['condition']}]>\n";
397
                $is_conditional = true;
398
                unset($attributes['condition']);
399
            }
400
401 8
            $head .= "<link" . $this->_get_attribute_string($attributes) . " />\n";
402
403 8
            if ($is_conditional) {
404
                $head .= "<![endif]-->\n";
405
            }
406
        }
407
408 8
        $head .= $this->_style_head;
409
410 8
        if (!empty($this->_prepend_jshead)) {
411 8
            $head .= array_reduce($this->_prepend_jshead, $this->render_js(...), '');
412
        }
413
414 8
        $head .= array_reduce($this->_jshead, $this->render_js(...), '');
415 8
        return $head . $this->render_jquery_statuses();
416
    }
417
418 8
    private function render_js(string $carry, array $js_call) : string
419
    {
420 8
        if (array_key_exists('url', $js_call)) {
421 8
            if ($this->cachebusting) {
422
                $js_call['url'] .= $this->cachebusting;
423
            }
424 8
            return $carry . '<script type="text/javascript" src="' . $js_call['url'] . "\"></script>\n";
425
        }
426 8
        $carry .= '<script type="text/javascript"' . ($js_call['defer'] ?? '') . ">\n";
427 8
        $carry .= $js_call['content'] . "\n";
428 8
        return $carry . "</script>\n";
429
    }
430
431 7
    public function get_jshead_elements() : array
432
    {
433 7
        return array_merge($this->_prepend_jshead, $this->_jshead);
434
    }
435
436
    public function get_link_head() : array
437
    {
438
        return $this->_link_head;
439
    }
440
441
    /**
442
     * Init jQuery
443
     *
444
     * This method adds jQuery support to the page
445
     */
446 234
    public function enable_jquery()
447
    {
448 234
        if ($this->_jquery_enabled) {
449 233
            return;
450
        }
451
452 1
        $script  = "const MIDCOM_STATIC_URL = '" . MIDCOM_STATIC_URL . "',\n";
453 1
        $script .= "      MIDCOM_PAGE_PREFIX = '" . midcom_connection::get_url('self') . "';";
454 1
        array_unshift($this->_prepend_jshead, ['content' => $script]);
455
456 1
        $version = midcom::get()->config->get('jquery_version');
457 1
        if (midcom::get()->config->get('jquery_load_from_google')) {
458
            // Use Google's hosted jQuery version
459
            array_unshift($this->_prepend_jshead, ['content' => 'google.load("jquery", "' . $version . '");']);
460
            array_unshift($this->_prepend_jshead, ['url' => 'https://www.google.com/jsapi']);
461
        } else {
462 1
            $url = MIDCOM_STATIC_URL . "/jQuery/jquery-{$version}.js";
463 1
            array_unshift($this->_prepend_jshead, ['url' => $url]);
464
        }
465
466 1
        if (!defined('MIDCOM_JQUERY_UI_URL')) {
467 1
            define('MIDCOM_JQUERY_UI_URL', MIDCOM_STATIC_URL . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version'));
468
        }
469
470 1
        $this->_jquery_enabled = true;
471
    }
472
473
    /**
474
     * Renders the scripts added by the add_jquery_state_script method.
475
     *
476
     * This method is called from print_head_elements method.
477
     *
478
     * @see add_jquery_state_script()
479
     * @see print_head_elements()
480
     */
481 8
    private function render_jquery_statuses() : string
482
    {
483 8
        if (empty($this->_jquery_states)) {
484 7
            return '';
485
        }
486
487 1
        $content = '';
488 1
        foreach ($this->_jquery_states as $status => $scripts) {
489 1
            [$target, $method] = explode('.', $status);
490 1
            $content .= "jQuery({$target}).{$method}(function() {\n";
491 1
            $content .= $scripts . "\n";
492 1
            $content .= "});\n";
493
        }
494
495 1
        return $this->render_js('', ['content' => $content]);
496
    }
497
498
    /**
499
     * Add jquery ui components
500
     *
501
     * core and widget are loaded automatically. Also loads jquery.ui theme,
502
     * either the configured theme one or a hardcoded default (base theme)
503
     */
504 225
    public function enable_jquery_ui(array $components = [])
505
    {
506 225
        $this->enable_jquery();
507 225
        $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/core.min.js');
508
509 225
        foreach ($components as $component) {
510 222
            $path = $component;
511 222
            if (str_starts_with($component, 'effect')) {
512
                if ($component !== 'effect') {
513
                    $path = 'effects/' . $component;
514
                }
515
            } else {
516 222
                $path = 'widgets/' . $component;
517
            }
518
519 222
            $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/' . $path . '.min.js');
520
521 222
            if ($component == 'datepicker') {
522
                /*
523
                 * The calendar doesn't have all lang files and some are named differently
524
                 * Since a missing lang file causes the calendar to break, let's make extra sure
525
                 * that this won't happen
526
                 */
527 36
                foreach ([midcom::get()->i18n->get_current_language(), midcom::get()->i18n->get_fallback_language()] as $lang) {
528 36
                    if (file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
529
                        $this->add_jsfile(MIDCOM_JQUERY_UI_URL . "/i18n/datepicker-{$lang}.min.js");
530
                        break;
531
                    }
532
                }
533
            }
534
        }
535
536 225
        $this->add_link_head([
537 225
            'rel'  => 'stylesheet',
538 225
            'type' => 'text/css',
539 225
            'href' => MIDCOM_STATIC_URL . '/jQuery/jquery-ui-1.12.icon-font.min.css',
540 225
        ], true);
541 225
        $this->add_link_head([
542 225
            'rel'  => 'stylesheet',
543 225
            'type' => 'text/css',
544 225
            'href' => midcom::get()->config->get('jquery_ui_theme', MIDCOM_JQUERY_UI_URL . '/themes/base/jquery-ui.min.css'),
545 225
        ], true);
546
    }
547
}
548