Passed
Push — master ( 895265...237ce9 )
by Andreas
24:18
created

midcom_helper_head   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 595
Duplicated Lines 0 %

Test Coverage

Coverage 58.05%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 160
dl 0
loc 595
ccs 101
cts 174
cp 0.5805
rs 5.04
c 2
b 0
f 1
wmc 57

23 Methods

Rating   Name   Duplication   Size   Complexity  
A add_jsfile() 0 12 3
A add_style_head() 0 3 1
A add_object_head() 0 3 1
A set_pagetitle() 0 3 1
A add_meta_head() 0 3 1
A add_jquery_state_script() 0 8 2
A add_jscript() 0 7 2
A _get_attribute_string() 0 10 3
A add_link_head() 0 16 4
A add_stylesheet() 0 3 1
A prepare_stylesheet_attributes() 0 11 2
A print_jsonload() 0 5 2
A print_head_elements() 0 7 2
A render_js() 0 8 2
A enable_jquery() 0 29 5
A get_link_head() 0 3 1
A render() 0 33 6
A add_jsonload() 0 4 1
A inject_head_elements() 0 19 4
A prepend_stylesheet() 0 3 1
A render_jquery_statuses() 0 15 3
A get_jshead_elements() 0 3 1
B enable_jquery_ui() 0 47 8

How to fix   Complexity   

Complex Class

Complex classes like midcom_helper_head often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use midcom_helper_head, and based on these observations, apply Extract Interface, too.

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 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 246
    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 246
        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 246
    }
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 288
    public function add_link_head(array $attributes, bool $prepend = false)
276
    {
277 288
        if (!array_key_exists('href', $attributes)) {
278
            return;
279
        }
280
281
        // Register each URL only once
282 288
        if (($key = array_search($attributes['href'], $this->_linkhrefs)) !== false) {
283 281
            unset($this->_linkhrefs[$key]);
284
        }
285 288
        if ($prepend) {
286 233
            array_unshift($this->_linkhrefs, $attributes['href']);
287
        } else {
288 269
            $this->_linkhrefs[] = $attributes['href'];
289
        }
290 288
        $this->_link_head[$attributes['href']] = $attributes;
291 288
    }
292
293
    /**
294
     * Convenience shortcut for appending 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 262
    public function add_stylesheet($url, $media = false)
300
    {
301 262
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media));
302 262
    }
303
304
    /**
305
     * Convenience shortcut for prepending CSS files
306
     *
307
     * @param string $url The stylesheet URL
308
     * @param string $media The media type(s) for the stylesheet, if any
309
     */
310 115
    public function prepend_stylesheet(string $url, $media = false)
311
    {
312 115
        $this->add_link_head($this->prepare_stylesheet_attributes($url, $media), true);
313 115
    }
314
315 281
    private function prepare_stylesheet_attributes($url, $media) : array
316
    {
317
        $attributes = [
318 281
            'rel'  => 'stylesheet',
319 281
            'type' => 'text/css',
320 281
            'href' => $url,
321
        ];
322 281
        if ($media) {
323 32
            $attributes['media'] = $media;
324
        }
325 281
        return $attributes;
326
    }
327
328
    /**
329
     * Register a JavaScript method for the body onload event
330
     *
331
     * This allows components to register JavaScript code
332
     * during page processing. The site style can then query this queued-up code
333
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
334
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
335
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
336
     * BODY-tag. Note that these suggestions are not enforced.
337
     *
338
     * @param string $method    The name of the method to be called on page startup, including parameters but excluding the ';'.
339
     * @see add_jsfile()
340
     * @see add_jscript()
341
     * @see print_head_elements()
342
     * @see print_jsonload()
343
     */
344
    public function add_jsonload($method)
345
    {
346
        // Adds a method name for <body onload=".."> The string must not end with a ;, it is added automagically
347
        $this->_jsonload[] = $method;
348
    }
349
350
    /**
351
     * Echo the registered javascript code.
352
     *
353
     * This allows components to register JavaScript code
354
     * during page processing. The site style code can then query this queued-up code
355
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
356
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
357
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
358
     * BODY-tag. Note, that these suggestions are not enforced
359
     *
360
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
361
     *
362
     * This is usually called during the BODY region of your style:
363
     *
364
     * <code>
365
     * <html>
366
     *     <body <?php midcom::get()->head->print_jsonload();?>>
367
     *            <!-- your actual body -->
368
     *     </body>
369
     * </html>
370
     * </code>
371
     *
372
     * @see add_jsfile()
373
     * @see add_jscript()
374
     * @see add_jsonload()
375
     * @see print_head_elements()
376
     */
377 25
    public function print_jsonload()
378
    {
379 25
        if (!empty($this->_jsonload)) {
380
            $calls = implode("; ", $this->_jsonload);
381
            echo " onload=\"$calls\" ";
382
        }
383 25
    }
384
385
    /**
386
     * Marks where the _head elements added should be rendered.
387
     *
388
     * Place the method within the <head> section of your page.
389
     *
390
     * This allows components to register HEAD elements
391
     * during page processing. The site style can then query this queued-up code
392
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
393
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
394
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
395
     * BODY tag. Note that these suggestions are not enforced
396
     *
397
     * @see add_link_head()
398
     * @see add_object_head()
399
     * @see add_style_head()
400
     * @see add_meta_head()
401
     * @see add_jsfile()
402
     * @see add_jscript()
403
     */
404 28
    public function print_head_elements()
405
    {
406 28
        if (!self::$listener_added) {
407 1
            midcom::get()->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'inject_head_elements']);
408 1
            self::$listener_added = true;
409
        }
410 28
        echo self::HEAD_PLACEHOLDER;
411 28
    }
412
413
    /**
414
     * This function renders the elements added by the various add methods
415
     * and injects them into the response
416
     *
417
     * @param ResponseEvent $event
418
     */
419 335
    public function inject_head_elements(ResponseEvent $event)
420
    {
421 335
        if (!$event->isMasterRequest()) {
422 335
            return;
423
        }
424
        $response = $event->getResponse();
425
        $content = $response->getContent();
426
427
        $first = strpos($content, self::HEAD_PLACEHOLDER);
428
        if ($first === false) {
429
            return;
430
        }
431
432
        $head = $this->render();
433
        $new_content = substr_replace($content, $head, $first, strlen(self::HEAD_PLACEHOLDER));
434
        $response->setContent($new_content);
435
        if ($length = $response->headers->get('Content-Length')) {
436
            $delta = strlen($head) - strlen(self::HEAD_PLACEHOLDER);
437
            $response->headers->set('Content-Length', $length + $delta);
438
        }
439
    }
440
441
    public function render() : string
442
    {
443
        $head = $this->_meta_head;
444
        foreach ($this->_linkhrefs as $url) {
445
            $attributes = $this->_link_head[$url];
446
            $is_conditional = false;
447
448
            if (array_key_exists('condition', $attributes)) {
449
                $head .= "<!--[if {$attributes['condition']}]>\n";
450
                $is_conditional = true;
451
                unset($attributes['condition']);
452
            }
453
454
            $head .= "<link" . $this->_get_attribute_string($attributes) . " />\n";
455
456
            if ($is_conditional) {
457
                $head .= "<![endif]-->\n";
458
            }
459
        }
460
461
        $head .= $this->_object_head;
462
        $head .= $this->_style_head;
463
464
        if ($this->_jquery_enabled) {
465
            $head .= $this->_jquery_init_scripts;
466
        }
467
468
        if (!empty($this->_prepend_jshead)) {
469
            $head .= array_reduce($this->_prepend_jshead, [$this, 'render_js'], '');
470
        }
471
472
        $head .= array_reduce($this->_jshead, [$this, 'render_js'], '');
473
        return $head . $this->render_jquery_statuses();
474
    }
475
476 1
    private function render_js(string $carry, array $js_call) : string
477
    {
478 1
        if (array_key_exists('url', $js_call)) {
479 1
            return $carry . '<script type="text/javascript" src="' . $js_call['url'] . "\"></script>\n";
480
        }
481 1
        $carry .= '<script type="text/javascript"' . ($js_call['defer'] ?? '') . ">\n";
482 1
        $carry .= $js_call['content'] . "\n";
483 1
        return $carry . "</script>\n";
484
    }
485
486 6
    public function get_jshead_elements() : array
487
    {
488 6
        return $this->_prepend_jshead + $this->_jshead;
489
    }
490
491
    public function get_link_head() : array
492
    {
493
        return $this->_link_head;
494
    }
495
496
    /**
497
     * Init jQuery
498
     *
499
     * This method adds jQuery support to the page
500
     */
501 246
    public function enable_jquery($version = null)
502
    {
503 246
        if ($this->_jquery_enabled) {
504 245
            return;
505
        }
506
507 1
        if (!$version) {
508 1
            $version = midcom::get()->config->get('jquery_version');
509
        }
510
511 1
        if (midcom::get()->config->get('jquery_load_from_google')) {
512
            // Use Google's hosted jQuery version
513
            $this->_jquery_init_scripts .= $this->render_js("\n", ['url' => 'https://www.google.com/jsapi']);
514
            $this->_jquery_init_scripts .= $this->render_js('', ['content' => 'google.load("jquery", "' . $version . '");']);
515
        } else {
516 1
            $url = MIDCOM_STATIC_URL . "/jQuery/jquery-{$version}.js";
517 1
            $this->_jquery_init_scripts .= $this->render_js("\n", ['url' => $url]);
518
        }
519
520 1
        if (!defined('MIDCOM_JQUERY_UI_URL')) {
521 1
            define('MIDCOM_JQUERY_UI_URL', MIDCOM_STATIC_URL . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version'));
522
        }
523
524 1
        $script  = "const MIDCOM_STATIC_URL = '" . MIDCOM_STATIC_URL . "',\n";
525 1
        $script .= "      MIDCOM_PAGE_PREFIX = '" . midcom_connection::get_url('self') . "';\n";
526
527 1
        $this->_jquery_init_scripts .= $this->render_js('', ['content' => trim($script)]);
528
529 1
        $this->_jquery_enabled = true;
530 1
    }
531
532
    /**
533
     * Renders the scripts added by the add_jquery_state_script method.
534
     *
535
     * This method is called from print_head_elements method.
536
     *
537
     * @see add_jquery_state_script()
538
     * @see print_head_elements()
539
     */
540
    private function render_jquery_statuses() : string
541
    {
542
        if (empty($this->_jquery_states)) {
543
            return '';
544
        }
545
546
        $content = '';
547
        foreach ($this->_jquery_states as $status => $scripts) {
548
            list($status_target, $status_method) = explode('.', $status);
549
            $content .= "jQuery({$status_target}).{$status_method}(function() {\n";
550
            $content .= $scripts . "\n";
551
            $content .= "});\n";
552
        }
553
554
        return $this->render_js('', ['content' => $content]);
555
    }
556
557
    /**
558
     * Add jquery ui components
559
     *
560
     * core and widget are loaded automatically. Also loads jquery.ui theme,
561
     * either the configured theme one or a hardcoded default (base theme)
562
     *
563
     * @param array $components The components that should be loaded
564
     */
565 223
    public function enable_jquery_ui(array $components = [])
566
    {
567 223
        $this->enable_jquery();
568 223
        $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/core.min.js');
569
570 223
        foreach ($components as $component) {
571 221
            $path = $component;
572 221
            if (strpos($component, 'effect') === 0) {
573
                if ($component !== 'effect') {
574
                    $path = 'effects/' . $component;
575
                }
576
            } else {
577 221
                $path = 'widgets/' . $component;
578
            }
579
580 221
            $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/' . $path . '.min.js');
581
582 221
            if ($component == 'datepicker') {
583 36
                $lang = midcom::get()->i18n->get_current_language();
584
                /*
585
                 * The calendar doesn't have all lang files and some are named differently
586
                 * Since a missing lang file causes the calendar to break, let's make extra sure
587
                 * that this won't happen
588
                 */
589 36
                if (!file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
590 36
                    $lang = midcom::get()->i18n->get_fallback_language();
591 36
                    if (!file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
592 36
                        $lang = null;
593
                    }
594
                }
595
596 36
                if ($lang) {
597
                    $this->add_jsfile(MIDCOM_JQUERY_UI_URL . "/i18n/datepicker-{$lang}.min.js");
598
                }
599
            }
600
        }
601
602 223
        $this->add_link_head([
603 223
            'rel'  => 'stylesheet',
604
            'type' => 'text/css',
605
            'href' => MIDCOM_STATIC_URL . '/jQuery/jquery-ui-1.12.icon-font.min.css',
606 223
        ], true);
607 223
        $this->add_link_head([
608 223
            'rel'  => 'stylesheet',
609 223
            'type' => 'text/css',
610 223
            'href' => midcom::get()->config->get('jquery_ui_theme', MIDCOM_JQUERY_UI_URL . '/themes/base/jquery-ui.min.css'),
611 223
        ], true);
612 223
    }
613
}
614