Completed
Push — master ( c3a706...f2f724 )
by Andreas
18:35
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
    private $_jquery_init_scripts = '';
48
49
    /**
50
     * Array with all JQuery state scripts for the page's head.
51
     *
52
     * @var array
53
     */
54
    private $_jquery_states = [];
55
56
    /**
57
     * Array with all linked URLs for HEAD.
58
     *
59
     * @var Array
60
     */
61
    private $_linkhrefs = [];
62
63
    /**
64
     * Array with all methods for the BODY's onload event.
65
     *
66
     * @var Array
67
     */
68
    private $_jsonload = [];
69
70
    /**
71
     * string with all metatags to go into the page head.
72
     *
73
     * @var string
74
     */
75
    private $_meta_head = '';
76
77
    /**
78
     * String with all css styles to go into a page's head.
79
     *
80
     * @var string
81
     */
82
    private $_style_head = '';
83
84
    /**
85
     * Array with all link elements to be included in a page's head.
86
     *
87
     * @var array
88
     */
89
    private $_link_head = [];
90
91
    const HEAD_PLACEHOLDER = '<!-- MIDCOM_HEAD_ELEMENTS -->';
92
93
    private static $listener_added = false;
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
     * @param string $string    The title to set.
102
     */
103 135
    public function set_pagetitle($string)
104
    {
105 135
        midcom_core_context::get()->set_key(MIDCOM_CONTEXT_PAGETITLE, $string);
106 135
    }
107
108
    /**
109
     * Register JavaScript File for referring in the page.
110
     *
111
     * This allows MidCOM components to register JavaScript code
112
     * during page processing. The site style code can then query this queued-up code
113
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
114
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
115
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
116
     * BODY-tag. Note, that these suggestions are not enforced, if you want a JScript
117
     * clean site, just omit the print calls and you should be fine in almost all
118
     * cases.
119
     *
120
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
121
     *
122
     * @param string $url    The URL to the file to-be referenced.
123
     * @param boolean $prepend Whether to add the JS include to beginning of includes
124
     * @see add_jscript()
125
     * @see add_jsonload()
126
     * @see print_head_elements()
127
     * @see print_jsonload()
128
     */
129 246
    public function add_jsfile(string $url, bool $prepend = false)
130
    {
131
        // Adds a URL for a <script type="text/javascript" src="tinymce.js"></script>
132
        // like call. $url is inserted into src. Duplicates are omitted.
133 246
        if (!in_array($url, $this->_jsfiles)) {
134 33
            $this->_jsfiles[] = $url;
135 33
            $js_call = ['url' => $url];
136 33
            if ($prepend) {
137
                // Add the javascript include to the beginning, not the end of array
138
                array_unshift($this->_jshead, $js_call);
139
            } else {
140 33
                $this->_jshead[] = $js_call;
141
            }
142
        }
143 246
    }
144
145
    /**
146
     * Register JavaScript Code for output directly in the page.
147
     *
148
     * This allows components to register JavaScript code
149
     * during page processing. The site style can then query this queued-up code
150
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
151
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
152
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
153
     * BODY-tag. Note, that these suggestions are not enforced
154
     *
155
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
156
     *
157
     * @param string $script    The code to be included directly in the page.
158
     * @see add_jsfile()
159
     * @see add_jsonload()
160
     * @see print_head_elements()
161
     * @see print_jsonload()
162
     */
163 29
    public function add_jscript(string $script, $defer = '', bool $prepend = false)
164
    {
165 29
        $js_call = ['content' => trim($script), 'defer' => $defer];
166 29
        if ($prepend) {
167
            $this->_prepend_jshead[] = $js_call;
168
        } else {
169 29
            $this->_jshead[] = $js_call;
170
        }
171 29
    }
172
173
    /**
174
     * Register JavaScript snippets to jQuery states.
175
     *
176
     * This allows components to register JavaScript code to the jQuery states.
177
     * Possible ready states: document.ready
178
     *
179
     * @param string $script    The code to be included in the state.
180
     * @param string $state    The state where to include the code to. Defaults to document.ready
181
     * @see print_jquery_statuses()
182
     */
183 3
    public function add_jquery_state_script(string $script, string $state = 'document.ready')
184
    {
185 3
        $js_call = "\n" . trim($script) . "\n";
186
187 3
        if (!isset($this->_jquery_states[$state])) {
188 1
            $this->_jquery_states[$state] = $js_call;
189
        } else {
190 2
            $this->_jquery_states[$state] .= $js_call;
191
        }
192 3
    }
193
194
    /**
195
     *  Register a metatag to be added to the head element.
196
     *  This allows components to register metatags to be placed in the
197
     *  head section of the page.
198
     *
199
     *  @param  array  $attributes Array of attribute => value pairs to be placed in the tag.
200
     *  @see print_head_elements()
201
     */
202
    public function add_meta_head(array $attributes)
203
    {
204
        $this->_meta_head .= '<meta' . $this->_get_attribute_string($attributes) . ' />' . "\n";
205
    }
206
207
    /**
208
     * Register a styleblock / style link  to be added to the head element.
209
     * This allows components to register extra CSS sheets they wants to include.
210
     * in the head section of the page.
211
     *
212
     * @param  string $script    The input between the <style></style> tags.
213
     * @param  array  $attributes Array of attribute=> value pairs to be placed in the tag.
214
     * @see print_head_elements()
215
     */
216
    public function add_style_head(string $script, array $attributes = [])
217
    {
218
        $this->_style_head .= '<style type="text/css"' . $this->_get_attribute_string($attributes) . '>' . $script . "</style>\n";
219
    }
220
221
    private function _get_attribute_string(array $attributes) : string
222
    {
223
        $string = '';
224
        foreach ($attributes as $key => $val) {
225
            $string .= ' ' . $key . '="' . htmlspecialchars($val, ENT_COMPAT) . '"';
226
        }
227
        return $string;
228
    }
229
230
    /**
231
     * Register a link element to be placed in the page head.
232
     *
233
     * This allows components to register extra CSS links.
234
     * Example to use this to include a CSS link:
235
     * <code>
236
     * $attributes = array ('rel' => 'stylesheet',
237
     *                      'type' => 'text/css',
238
     *                      'href' => '/style.css'
239
     *                      );
240
     * midcom::get()->head->add_link_head($attributes);
241
     * </code>
242
     *
243
     * Each URL will only be added once. When trying to add the same URL a second time,
244
     * it will be moved to the end of the stack, so that CSS overrides behave as the developer
245
     * intended
246
     *
247
     * @param  array $attributes Array of attribute => value pairs to be placed in the tag.
248
     * @see print_head_elements()
249
     */
250 288
    public function add_link_head(array $attributes, bool $prepend = false)
251
    {
252 288
        if (!array_key_exists('href', $attributes)) {
253
            return;
254
        }
255
256
        // Register each URL only once
257 288
        if (($key = array_search($attributes['href'], $this->_linkhrefs)) !== false) {
258 281
            unset($this->_linkhrefs[$key]);
259
        }
260 288
        if ($prepend) {
261 233
            array_unshift($this->_linkhrefs, $attributes['href']);
262
        } else {
263 269
            $this->_linkhrefs[] = $attributes['href'];
264
        }
265 288
        $this->_link_head[$attributes['href']] = $attributes;
266 288
    }
267
268
    /**
269
     * Convenience shortcut for appending CSS files
270
     *
271
     * @param string $url The stylesheet URL
272
     * @param string $media The media type(s) for the stylesheet, if any
273
     */
274 262
    public function add_stylesheet(string $url, string $media = null)
275
    {
276 262
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media));
277 262
    }
278
279
    /**
280
     * Convenience shortcut for prepending CSS files
281
     *
282
     * @param string $url The stylesheet URL
283
     * @param string $media The media type(s) for the stylesheet, if any
284
     */
285 115
    public function prepend_stylesheet(string $url, string $media = null)
286
    {
287 115
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media), true);
288 115
    }
289
290 281
    private function prepare_stylesheet_attributes(string $url, ?string $media) : array
291
    {
292
        $attributes = [
293 281
            'rel'  => 'stylesheet',
294 281
            'type' => 'text/css',
295 281
            'href' => $url,
296
        ];
297 281
        if ($media) {
298 32
            $attributes['media'] = $media;
299
        }
300 281
        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 25
    public function print_jsonload()
353
    {
354 25
        if (!empty($this->_jsonload)) {
355
            $calls = implode("; ", $this->_jsonload);
356
            echo " onload=\"$calls\" ";
357
        }
358 25
    }
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 28
    public function print_head_elements()
379
    {
380 28
        if (!self::$listener_added) {
381 1
            midcom::get()->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'inject_head_elements']);
382 1
            self::$listener_added = true;
383
        }
384 28
        echo self::HEAD_PLACEHOLDER;
385 28
    }
386
387
    /**
388
     * This function renders the elements added by the various add methods
389
     * and injects them into the response
390
     *
391
     * @param ResponseEvent $event
392
     */
393 335
    public function inject_head_elements(ResponseEvent $event)
394
    {
395 335
        if (!$event->isMasterRequest()) {
396 335
            return;
397
        }
398
        $response = $event->getResponse();
399
        $content = $response->getContent();
400
401
        $first = strpos($content, self::HEAD_PLACEHOLDER);
402
        if ($first === false) {
403
            return;
404
        }
405
406
        $head = $this->render();
407
        $new_content = substr_replace($content, $head, $first, strlen(self::HEAD_PLACEHOLDER));
408
        $response->setContent($new_content);
409
        if ($length = $response->headers->get('Content-Length')) {
410
            $delta = strlen($head) - strlen(self::HEAD_PLACEHOLDER);
411
            $response->headers->set('Content-Length', $length + $delta);
412
        }
413
    }
414
415
    public function render() : string
416
    {
417
        $head = $this->_meta_head;
418
        foreach ($this->_linkhrefs as $url) {
419
            $attributes = $this->_link_head[$url];
420
            $is_conditional = false;
421
422
            if (array_key_exists('condition', $attributes)) {
423
                $head .= "<!--[if {$attributes['condition']}]>\n";
424
                $is_conditional = true;
425
                unset($attributes['condition']);
426
            }
427
428
            $head .= "<link" . $this->_get_attribute_string($attributes) . " />\n";
429
430
            if ($is_conditional) {
431
                $head .= "<![endif]-->\n";
432
            }
433
        }
434
435
        $head .= $this->_style_head;
436
437
        if ($this->_jquery_enabled) {
438
            $head .= $this->_jquery_init_scripts;
439
        }
440
441
        if (!empty($this->_prepend_jshead)) {
442
            $head .= array_reduce($this->_prepend_jshead, [$this, 'render_js'], '');
443
        }
444
445
        $head .= array_reduce($this->_jshead, [$this, 'render_js'], '');
446
        return $head . $this->render_jquery_statuses();
447
    }
448
449 1
    private function render_js(string $carry, array $js_call) : string
450
    {
451 1
        if (array_key_exists('url', $js_call)) {
452 1
            return $carry . '<script type="text/javascript" src="' . $js_call['url'] . "\"></script>\n";
453
        }
454 1
        $carry .= '<script type="text/javascript"' . ($js_call['defer'] ?? '') . ">\n";
455 1
        $carry .= $js_call['content'] . "\n";
456 1
        return $carry . "</script>\n";
457
    }
458
459 6
    public function get_jshead_elements() : array
460
    {
461 6
        return $this->_prepend_jshead + $this->_jshead;
462
    }
463
464
    public function get_link_head() : array
465
    {
466
        return $this->_link_head;
467
    }
468
469
    /**
470
     * Init jQuery
471
     *
472
     * This method adds jQuery support to the page
473
     */
474 246
    public function enable_jquery()
475
    {
476 246
        if ($this->_jquery_enabled) {
477 245
            return;
478
        }
479
480 1
        $version = midcom::get()->config->get('jquery_version');
481 1
        if (midcom::get()->config->get('jquery_load_from_google')) {
482
            // Use Google's hosted jQuery version
483
            $this->_jquery_init_scripts .= $this->render_js("\n", ['url' => 'https://www.google.com/jsapi']);
484
            $this->_jquery_init_scripts .= $this->render_js('', ['content' => 'google.load("jquery", "' . $version . '");']);
485
        } else {
486 1
            $url = MIDCOM_STATIC_URL . "/jQuery/jquery-{$version}.js";
487 1
            $this->_jquery_init_scripts .= $this->render_js("\n", ['url' => $url]);
488
        }
489
490 1
        if (!defined('MIDCOM_JQUERY_UI_URL')) {
491 1
            define('MIDCOM_JQUERY_UI_URL', MIDCOM_STATIC_URL . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version'));
492
        }
493
494 1
        $script  = "const MIDCOM_STATIC_URL = '" . MIDCOM_STATIC_URL . "',\n";
495 1
        $script .= "      MIDCOM_PAGE_PREFIX = '" . midcom_connection::get_url('self') . "';\n";
496
497 1
        $this->_jquery_init_scripts .= $this->render_js('', ['content' => trim($script)]);
498
499 1
        $this->_jquery_enabled = true;
500 1
    }
501
502
    /**
503
     * Renders the scripts added by the add_jquery_state_script method.
504
     *
505
     * This method is called from print_head_elements method.
506
     *
507
     * @see add_jquery_state_script()
508
     * @see print_head_elements()
509
     */
510
    private function render_jquery_statuses() : string
511
    {
512
        if (empty($this->_jquery_states)) {
513
            return '';
514
        }
515
516
        $content = '';
517
        foreach ($this->_jquery_states as $status => $scripts) {
518
            [$target, $method] = explode('.', $status);
519
            $content .= "jQuery({$target}).{$method}(function() {\n";
520
            $content .= $scripts . "\n";
521
            $content .= "});\n";
522
        }
523
524
        return $this->render_js('', ['content' => $content]);
525
    }
526
527
    /**
528
     * Add jquery ui components
529
     *
530
     * core and widget are loaded automatically. Also loads jquery.ui theme,
531
     * either the configured theme one or a hardcoded default (base theme)
532
     *
533
     * @param array $components The components that should be loaded
534
     */
535 223
    public function enable_jquery_ui(array $components = [])
536
    {
537 223
        $this->enable_jquery();
538 223
        $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/core.min.js');
539
540 223
        foreach ($components as $component) {
541 221
            $path = $component;
542 221
            if (str_starts_with($component, 'effect')) {
543
                if ($component !== 'effect') {
544
                    $path = 'effects/' . $component;
545
                }
546
            } else {
547 221
                $path = 'widgets/' . $component;
548
            }
549
550 221
            $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/' . $path . '.min.js');
551
552 221
            if ($component == 'datepicker') {
553 36
                $lang = midcom::get()->i18n->get_current_language();
554
                /*
555
                 * The calendar doesn't have all lang files and some are named differently
556
                 * Since a missing lang file causes the calendar to break, let's make extra sure
557
                 * that this won't happen
558
                 */
559 36
                if (!file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
560 36
                    $lang = midcom::get()->i18n->get_fallback_language();
561 36
                    if (!file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
562 36
                        $lang = null;
563
                    }
564
                }
565
566 36
                if ($lang) {
567
                    $this->add_jsfile(MIDCOM_JQUERY_UI_URL . "/i18n/datepicker-{$lang}.min.js");
568
                }
569
            }
570
        }
571
572 223
        $this->add_link_head([
573 223
            'rel'  => 'stylesheet',
574
            'type' => 'text/css',
575
            'href' => MIDCOM_STATIC_URL . '/jQuery/jquery-ui-1.12.icon-font.min.css',
576 223
        ], true);
577 223
        $this->add_link_head([
578 223
            'rel'  => 'stylesheet',
579 223
            'type' => 'text/css',
580 223
            'href' => midcom::get()->config->get('jquery_ui_theme', MIDCOM_JQUERY_UI_URL . '/themes/base/jquery-ui.min.css'),
581 223
        ], true);
582 223
    }
583
}
584