Passed
Push — master ( d28158...59c306 )
by Andreas
23:44
created

midcom_helper_head::set_pagetitle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

390
        if (!/** @scrutinizer ignore-deprecated */ $event->isMasterRequest()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
391 343
            return;
392
        }
393
        $response = $event->getResponse();
394
        $content = $response->getContent();
395
396
        $first = strpos($content, self::HEAD_PLACEHOLDER);
397
        if ($first === false) {
398
            return;
399
        }
400
401
        $head = $this->render();
402
        $new_content = substr_replace($content, $head, $first, strlen(self::HEAD_PLACEHOLDER));
403
        $response->setContent($new_content);
0 ignored issues
show
Bug introduced by
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

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