Completed
Push — master ( d4240c...624b69 )
by Andreas
26:55
created

midcom_helper_head::add_link_head()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 16
ccs 8
cts 9
cp 0.8889
crap 4.0218
rs 9.9666
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
    /**
94
     * Sets the page title for the current context.
95
     *
96
     * This can be retrieved by accessing the component context key
97
     * MIDCOM_CONTEXT_PAGETITLE.
98
     *
99
     * @param string $string    The title to set.
100
     */
101 135
    public function set_pagetitle($string)
102
    {
103 135
        midcom_core_context::get()->set_key(MIDCOM_CONTEXT_PAGETITLE, $string);
104 135
    }
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
     * @param string $url    The URL to the file to-be referenced.
121
     * @param boolean $prepend Whether to add the JS include to beginning of includes
122
     * @see add_jscript()
123
     * @see add_jsonload()
124
     * @see print_head_elements()
125
     * @see print_jsonload()
126
     */
127 246
    public function add_jsfile(string $url, bool $prepend = false)
128
    {
129
        // Adds a URL for a <script type="text/javascript" src="tinymce.js"></script>
130
        // like call. $url is inserted into src. Duplicates are omitted.
131 246
        if (!in_array($url, $this->_jsfiles)) {
132 33
            $this->_jsfiles[] = $url;
133 33
            $js_call = ['url' => $url];
134 33
            if ($prepend) {
135
                // Add the javascript include to the beginning, not the end of array
136
                array_unshift($this->_jshead, $js_call);
137
            } else {
138 33
                $this->_jshead[] = $js_call;
139
            }
140
        }
141 246
    }
142
143
    /**
144
     * Register JavaScript Code for output directly in the page.
145
     *
146
     * This allows components to register JavaScript code
147
     * during page processing. The site style can then query this queued-up code
148
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
149
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
150
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
151
     * BODY-tag. Note, that these suggestions are not enforced
152
     *
153
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
154
     *
155
     * @param string $script    The code to be included directly in the page.
156
     * @see add_jsfile()
157
     * @see add_jsonload()
158
     * @see print_head_elements()
159
     * @see print_jsonload()
160
     */
161 29
    public function add_jscript(string $script, $defer = '', bool $prepend = false)
162
    {
163 29
        $js_call = ['content' => trim($script), 'defer' => $defer];
164 29
        if ($prepend) {
165
            $this->_prepend_jshead[] = $js_call;
166
        } else {
167 29
            $this->_jshead[] = $js_call;
168
        }
169 29
    }
170
171
    /**
172
     * Register JavaScript snippets to jQuery states.
173
     *
174
     * This allows components to register JavaScript code to the jQuery states.
175
     * Possible ready states: document.ready
176
     *
177
     * @param string $script    The code to be included in the state.
178
     * @param string $state    The state where to include the code to. Defaults to document.ready
179
     * @see print_jquery_statuses()
180
     */
181 3
    public function add_jquery_state_script(string $script, string $state = 'document.ready')
182
    {
183 3
        $js_call = "\n" . trim($script) . "\n";
184
185 3
        if (!isset($this->_jquery_states[$state])) {
186 1
            $this->_jquery_states[$state] = $js_call;
187
        } else {
188 2
            $this->_jquery_states[$state] .= $js_call;
189
        }
190 3
    }
191
192
    /**
193
     *  Register a metatag to be added to the head element.
194
     *  This allows components to register metatags to be placed in the
195
     *  head section of the page.
196
     *
197
     *  @param  array  $attributes Array of attribute => value pairs to be placed in the tag.
198
     *  @see print_head_elements()
199
     */
200
    public function add_meta_head(array $attributes)
201
    {
202
        $this->_meta_head .= '<meta' . $this->_get_attribute_string($attributes) . ' />' . "\n";
203
    }
204
205
    /**
206
     * Register a styleblock / style link  to be added to the head element.
207
     * This allows components to register extra CSS sheets they wants to include.
208
     * in the head section of the page.
209
     *
210
     * @param  string $script    The input between the <style></style> tags.
211
     * @param  array  $attributes Array of attribute=> value pairs to be placed in the tag.
212
     * @see print_head_elements()
213
     */
214
    public function add_style_head(string $script, array $attributes = [])
215
    {
216
        $this->_style_head .= '<style type="text/css"' . $this->_get_attribute_string($attributes) . '>' . $script . "</style>\n";
217
    }
218
219
    private function _get_attribute_string(array $attributes) : string
220
    {
221
        $string = '';
222
        foreach ($attributes as $key => $val) {
223
            $string .= ' ' . $key . '="' . htmlspecialchars($val, ENT_COMPAT) . '"';
224
        }
225
        return $string;
226
    }
227
228
    /**
229
     * Register a link element to be placed in the page head.
230
     *
231
     * This allows components to register extra CSS links.
232
     * Example to use this to include a CSS link:
233
     * <code>
234
     * $attributes = array ('rel' => 'stylesheet',
235
     *                      'type' => 'text/css',
236
     *                      'href' => '/style.css'
237
     *                      );
238
     * midcom::get()->head->add_link_head($attributes);
239
     * </code>
240
     *
241
     * Each URL will only be added once. When trying to add the same URL a second time,
242
     * it will be moved to the end of the stack, so that CSS overrides behave as the developer
243
     * intended
244
     *
245
     * @param  array $attributes Array of attribute => value pairs to be placed in the tag.
246
     * @see print_head_elements()
247
     */
248 288
    public function add_link_head(array $attributes, bool $prepend = false)
249
    {
250 288
        if (!array_key_exists('href', $attributes)) {
251
            return;
252
        }
253
254
        // Register each URL only once
255 288
        if (($key = array_search($attributes['href'], $this->_linkhrefs)) !== false) {
256 281
            unset($this->_linkhrefs[$key]);
257
        }
258 288
        if ($prepend) {
259 233
            array_unshift($this->_linkhrefs, $attributes['href']);
260
        } else {
261 269
            $this->_linkhrefs[] = $attributes['href'];
262
        }
263 288
        $this->_link_head[$attributes['href']] = $attributes;
264 288
    }
265
266
    /**
267
     * Convenience shortcut for appending CSS files
268
     *
269
     * @param string $url The stylesheet URL
270
     * @param string $media The media type(s) for the stylesheet, if any
271
     */
272 262
    public function add_stylesheet(string $url, string $media = null)
273
    {
274 262
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media));
275 262
    }
276
277
    /**
278
     * Convenience shortcut for prepending CSS files
279
     *
280
     * @param string $url The stylesheet URL
281
     * @param string $media The media type(s) for the stylesheet, if any
282
     */
283 115
    public function prepend_stylesheet(string $url, string $media = null)
284
    {
285 115
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media), true);
286 115
    }
287
288 281
    private function prepare_stylesheet_attributes(string $url, ?string $media) : array
289
    {
290
        $attributes = [
291 281
            'rel'  => 'stylesheet',
292 281
            'type' => 'text/css',
293 281
            'href' => $url,
294
        ];
295 281
        if ($media) {
296 32
            $attributes['media'] = $media;
297
        }
298 281
        return $attributes;
299
    }
300
301
    /**
302
     * Register a JavaScript method for the body onload event
303
     *
304
     * This allows components to register JavaScript code
305
     * during page processing. The site style can then query this queued-up code
306
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
307
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
308
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
309
     * BODY-tag. Note that these suggestions are not enforced.
310
     *
311
     * @param string $method    The name of the method to be called on page startup, including parameters but excluding the ';'.
312
     * @see add_jsfile()
313
     * @see add_jscript()
314
     * @see print_head_elements()
315
     * @see print_jsonload()
316
     */
317
    public function add_jsonload(string $method)
318
    {
319
        // Adds a method name for <body onload=".."> The string must not end with a ;, it is added automagically
320
        $this->_jsonload[] = $method;
321
    }
322
323
    /**
324
     * Echo the registered javascript code.
325
     *
326
     * This allows components to register JavaScript code
327
     * during page processing. The site style code can then query this queued-up code
328
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
329
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
330
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
331
     * BODY-tag. Note, that these suggestions are not enforced
332
     *
333
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
334
     *
335
     * This is usually called during the BODY region of your style:
336
     *
337
     * <code>
338
     * <html>
339
     *     <body <?php midcom::get()->head->print_jsonload();?>>
340
     *            <!-- your actual body -->
341
     *     </body>
342
     * </html>
343
     * </code>
344
     *
345
     * @see add_jsfile()
346
     * @see add_jscript()
347
     * @see add_jsonload()
348
     * @see print_head_elements()
349
     */
350 25
    public function print_jsonload()
351
    {
352 25
        if (!empty($this->_jsonload)) {
353
            $calls = implode("; ", $this->_jsonload);
354
            echo " onload=\"$calls\" ";
355
        }
356 25
    }
357
358
    /**
359
     * Marks where the _head elements added should be rendered.
360
     *
361
     * Place the method within the <head> section of your page.
362
     *
363
     * This allows components to register HEAD elements
364
     * during page processing. The site style can then query this queued-up code
365
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
366
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
367
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
368
     * BODY tag. Note that these suggestions are not enforced
369
     *
370
     * @see add_link_head()
371
     * @see add_style_head()
372
     * @see add_meta_head()
373
     * @see add_jsfile()
374
     * @see add_jscript()
375
     */
376 28
    public function print_head_elements()
377
    {
378 28
        if (!self::$listener_added) {
379 1
            midcom::get()->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'inject_head_elements']);
380 1
            self::$listener_added = true;
381
        }
382 28
        echo self::HEAD_PLACEHOLDER;
383 28
    }
384
385
    /**
386
     * This function renders the elements added by the various add methods
387
     * and injects them into the response
388
     *
389
     * @param ResponseEvent $event
390
     */
391 335
    public function inject_head_elements(ResponseEvent $event)
392
    {
393 335
        if (!$event->isMasterRequest()) {
394 335
            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);
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
    public function render() : string
414
    {
415
        $head = $this->_meta_head;
416
        foreach ($this->_linkhrefs as $url) {
417
            $attributes = $this->_link_head[$url];
418
            $is_conditional = false;
419
420
            if (array_key_exists('condition', $attributes)) {
421
                $head .= "<!--[if {$attributes['condition']}]>\n";
422
                $is_conditional = true;
423
                unset($attributes['condition']);
424
            }
425
426
            $head .= "<link" . $this->_get_attribute_string($attributes) . " />\n";
427
428
            if ($is_conditional) {
429
                $head .= "<![endif]-->\n";
430
            }
431
        }
432
433
        $head .= $this->_style_head;
434
435
        if (!empty($this->_prepend_jshead)) {
436
            $head .= array_reduce($this->_prepend_jshead, [$this, 'render_js'], '');
437
        }
438
439
        $head .= array_reduce($this->_jshead, [$this, 'render_js'], '');
440
        return $head . $this->render_jquery_statuses();
441
    }
442
443
    private function render_js(string $carry, array $js_call) : string
444
    {
445
        if (array_key_exists('url', $js_call)) {
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 $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') . "';\n";
476 1
        array_unshift($this->_prepend_jshead, ['content' => trim($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
     * @param array $components The components that should be loaded
527
     */
528 223
    public function enable_jquery_ui(array $components = [])
529
    {
530 223
        $this->enable_jquery();
531 223
        $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/core.min.js');
532
533 223
        foreach ($components as $component) {
534 221
            $path = $component;
535 221
            if (str_starts_with($component, 'effect')) {
536
                if ($component !== 'effect') {
537
                    $path = 'effects/' . $component;
538
                }
539
            } else {
540 221
                $path = 'widgets/' . $component;
541
            }
542
543 221
            $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/' . $path . '.min.js');
544
545 221
            if ($component == 'datepicker') {
546 36
                $lang = midcom::get()->i18n->get_current_language();
547
                /*
548
                 * The calendar doesn't have all lang files and some are named differently
549
                 * Since a missing lang file causes the calendar to break, let's make extra sure
550
                 * that this won't happen
551
                 */
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 = midcom::get()->i18n->get_fallback_language();
554 36
                    if (!file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
555 36
                        $lang = null;
556
                    }
557
                }
558
559 36
                if ($lang) {
560
                    $this->add_jsfile(MIDCOM_JQUERY_UI_URL . "/i18n/datepicker-{$lang}.min.js");
561
                }
562
            }
563
        }
564
565 223
        $this->add_link_head([
566 223
            'rel'  => 'stylesheet',
567
            'type' => 'text/css',
568
            'href' => MIDCOM_STATIC_URL . '/jQuery/jquery-ui-1.12.icon-font.min.css',
569 223
        ], true);
570 223
        $this->add_link_head([
571 223
            'rel'  => 'stylesheet',
572 223
            'type' => 'text/css',
573 223
            'href' => midcom::get()->config->get('jquery_ui_theme', MIDCOM_JQUERY_UI_URL . '/themes/base/jquery-ui.min.css'),
574 223
        ], true);
575 223
    }
576
}
577