Completed
Push — master ( f489a4...a656f3 )
by Andreas
14:04
created

midcom_helper_head::add_meta_head()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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