Passed
Push — master ( 9d50f2...da7c38 )
by Andreas
28:20 queued 07:36
created

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

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