Passed
Push — master ( 30cd5d...5bfcaa )
by Andreas
23:09
created

midcom_helper__styleloader::show()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.25

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 21
ccs 9
cts 12
cp 0.75
crap 4.25
rs 9.9
1
<?php
2
/**
3
 * @package midcom.helper
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * This class is responsible for all style management. It is instantiated by the MidCOM framework
11
 * and accessible through the midcom::get()->style object.
12
 *
13
 * The method <code>show($style)</code> returns the style element $style for the current
14
 * component:
15
 *
16
 * It checks whether a style path is defined for the current component.
17
 *
18
 * - If there is a user defined style path, the element named $style in
19
 *   this path is returned,
20
 * - otherwise the element "$style" is taken from the default style of the
21
 *   current component (/path/to/component/style/$path).
22
 *
23
 * (The default fallback is always the default style, e.g. if $style
24
 * is not in the user defined style path)
25
 *
26
 * To enable cross-style referencing and provide the opportunity to access
27
 * any style element, "show" can be called with a full qualified style
28
 * path (like "/mystyle/element1", while the current page's style may be set
29
 * to "/yourstyle").
30
 *
31
 * Note: To make sure sub-styles and elements included in styles are handled
32
 * correctly, use:
33
 *
34
 * <code>
35
 * <?php midcom_show_style ("elementname"); ?>
36
 * </code>
37
 *
38
 * Style Inheritance
39
 *
40
 * The basic path the styleloader follows to find a style element is:
41
 * 1. Topic style -> if the current topic has a style set
42
 * 2. Inherited topic style -> if the topic inherits a style from another topic.
43
 * 3. Site-wide per-component default style -> if defined in MidCOM configuration key styleengine_default_styles
44
 * 4. Theme style -> the style of the MidCOM component.
45
 * 5. The file style. This is usually the elements found in the component's style directory.
46
 *
47
 * Regarding nr. 4:
48
 * It is possible to add extra file styles if so is needed for example by a portal component.
49
 * This is done either using the append/prepend component_style functions or by setting it
50
 * to another directory by calling (append|prepend)_styledir directly.
51
 *
52
 * NB: You cannot change this in another style element or in a _show() function in a component.
53
 *
54
 * @package midcom.helper
55
 */
56
class midcom_helper__styleloader
57
{
58
    /**
59
     * Current style scope
60
     *
61
     * @var array
62
     */
63
    private $_scope = [];
64
65
    /**
66
     * Current topic
67
     *
68
     * @var midcom_db_topic
69
     */
70
    private $_topic;
71
72
    /**
73
     * Default style path
74
     *
75
     * @var string
76
     */
77
    private $_snippetdir;
78
79
    /**
80
     * Context stack
81
     *
82
     * @var midcom_core_context[]
83
     */
84
    private $_context = [];
85
86
    /**
87
     * Default style element cache
88
     *
89
     * @var array
90
     */
91
    private $_snippets = [];
92
93
    /**
94
     * List of styledirs to handle after componentstyle
95
     *
96
     * @var array
97
     */
98
    private $_styledirs_append = [];
99
100
    /**
101
     * List of styledirs to handle before componentstyle
102
     *
103
     * @var array
104
     */
105
    private $_styledirs_prepend = [];
106
107
    /**
108
     * The stack of directories to check for styles.
109
     */
110
    private $_styledirs = [];
111
112
    /**
113
     * Data to pass to the style
114
     *
115
     * @var array
116
     */
117
    public $data;
118
119
    /**
120
     * Returns a style element that matches $name and is in style $id.
121
     * It also returns an element if it is not in the given style,
122
     * but in one of its parent styles.
123
     *
124
     * @param int $id        The style id to search in.
125
     * @param string $name    The element to locate.
126
     * @return string    Value of the found element, or false on failure.
127
     */
128
    private function _get_element_in_styletree($id, string $name) : ?string
129
    {
130
        static $cached = [];
131
        $cache_key = $id . '::' . $name;
132
133
        if (array_key_exists($cache_key, $cached)) {
134
            return $cached[$cache_key];
135
        }
136
137
        $element_mc = midgard_element::new_collector('style', $id);
138
        $element_mc->set_key_property('guid');
139
        $element_mc->add_value_property('value');
140
        $element_mc->add_constraint('name', '=', $name);
141
        $element_mc->execute();
142
143
        if ($keys = $element_mc->list_keys()) {
144
            $element_guid = key($keys);
145
            $cached[$cache_key] = $element_mc->get_subkey($element_guid, 'value');
146
            midcom::get()->cache->content->register($element_guid);
147
            return $cached[$cache_key];
148
        }
149
150
        // No such element on this level, check parents
151
        $style_mc = midgard_style::new_collector('id', $id);
152
        $style_mc->set_key_property('guid');
153
        $style_mc->add_value_property('up');
154
        $style_mc->add_constraint('up', '>', 0);
155
        $style_mc->execute();
156
157
        if ($keys = $style_mc->list_keys()) {
158
            $style_guid = key($keys);
159
            midcom::get()->cache->content->register($style_guid);
160
            $up = $style_mc->get_subkey($style_guid, 'up');
161
            return $this->_get_element_in_styletree($up, $name);
0 ignored issues
show
Bug introduced by
It seems like $up can also be of type false; however, parameter $id of midcom_helper__styleload..._element_in_styletree() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

161
            return $this->_get_element_in_styletree(/** @scrutinizer ignore-type */ $up, $name);
Loading history...
162
        }
163
164
        $cached[$cache_key] = null;
165
        return $cached[$cache_key];
166
    }
167
168
    /**
169
     * Looks for a style element matching $path (either in a user defined style
170
     * or the default style snippetdir) and displays/evaluates it.
171
     *
172
     * @param string $path    The style element to show.
173
     * @return boolean            True on success, false otherwise.
174
     */
175 202
    public function show($path) : bool
176
    {
177 202
        if ($this->_context === []) {
178
            debug_add("Trying to show '{$path}' but there is no context set", MIDCOM_LOG_INFO);
179
            return false;
180
        }
181
182 202
        $style = $this->load($path);
183
184 202
        if ($style === false) {
185 24
            if ($path == 'ROOT') {
186
                // Go to fallback ROOT instead of displaying a blank page
187
                return $this->show_midcom($path);
188
            }
189
190 24
            debug_add("The element '{$path}' could not be found.", MIDCOM_LOG_INFO);
191 24
            return false;
192
        }
193 201
        $this->render($style, $path);
194
195 201
        return true;
196
    }
197
198
    /**
199
     * Load style element content
200
     *
201
     * @param string $path The element name
202
     * @return false|string
203
     */
204 202
    public function load($path)
205
    {
206 202
        $element = $path;
207
        // we have full qualified path to element
208 202
        if (preg_match("|(.*)/(.*)|", $path, $matches)) {
209
            $stylepath = $matches[1];
210
            $element = $matches[2];
211
        }
212
213 202
        if (   isset($stylepath)
214 202
            && $styleid = midcom_db_style::id_from_path($stylepath)) {
215
            array_unshift($this->_scope, $styleid);
216
        }
217
218 202
        if (!empty($this->_scope[0])) {
219
            $style = $this->_get_element_in_styletree($this->_scope[0], $element);
220
        }
221
222 202
        if (!empty($styleid)) {
223
            array_shift($this->_scope);
224
        }
225
226 202
        if (empty($style)) {
227 202
            $style = $this->_get_element_from_snippet($element);
228
        }
229 202
        return $style;
230
    }
231
232
    /**
233
     * Renders the style element with current request data
234
     *
235
     * @param string $style The style element content
236
     * @param string $path the element's name
237
     * @throws midcom_error
238
     */
239 201
    private function render(string $style, string $path)
240
    {
241 201
        if (midcom::get()->config->get('wrap_style_show_with_name')) {
242
            $style = "\n<!-- Start of style '{$path}' -->\n" . $style;
243
            $style .= "\n<!-- End of style '{$path}' -->\n";
244
        }
245
246
        // This is a bit of a hack to allow &(); tags
247 201
        $preparsed = midcom_helper_misc::preparse($style);
248 201
        if (midcom_core_context::get()->has_custom_key('request_data')) {
249 201
            $data =& midcom_core_context::get()->get_custom_key('request_data');
0 ignored issues
show
Bug introduced by
'request_data' of type string is incompatible with the type integer expected by parameter $key of midcom_core_context::get_custom_key(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

249
            $data =& midcom_core_context::get()->get_custom_key(/** @scrutinizer ignore-type */ 'request_data');
Loading history...
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
250
        }
251
252 201
        if (eval('?>' . $preparsed) === false) {
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
253
            // Note that src detection will be semi-reliable, as it depends on all errors being
254
            // found before caching kicks in.
255
            throw new midcom_error("Failed to parse style element '{$path}', see above for PHP errors.");
256
        }
257 201
    }
258
259
    /**
260
     * Looks for a midcom core style element matching $path and displays/evaluates it.
261
     * This offers a bit reduced functionality and will only look in the DB root style,
262
     * the theme directory and midcom's style directory, because it has to work even when
263
     * midcom is not yet fully initialized
264
     *
265
     * @param string $path    The style element to show.
266
     * @return boolean            True on success, false otherwise.
267
     */
268 1
    public function show_midcom($path) : bool
269
    {
270 1
        $_element = $path;
271 1
        $_style = false;
272
273 1
        $context = midcom_core_context::get();
274
275
        try {
276 1
            $root_topic = $context->get_key(MIDCOM_CONTEXT_ROOTTOPIC);
277 1
            if (   $root_topic->style
278 1
                && $db_style = midcom_db_style::id_from_path($root_topic->style)) {
279 1
                $_style = $this->_get_element_in_styletree($db_style, $_element);
280
            }
281
        } catch (midcom_error_forbidden $e) {
282
            $e->log();
283
        }
284
285 1
        if ($_style === false) {
286 1
            if (isset($this->_styledirs[$context->id])) {
287 1
                $styledirs_backup = $this->_styledirs;
288
            }
289 1
            $this->_snippetdir = MIDCOM_ROOT . '/midcom/style';
290 1
            $this->_styledirs[$context->id][0] = $this->_snippetdir;
291
292 1
            $_style = $this->_get_element_from_snippet($_element);
293
294 1
            if (isset($styledirs_backup)) {
295 1
                $this->_styledirs = $styledirs_backup;
296
            }
297
        }
298
299 1
        if ($_style !== false) {
300 1
            $this->render($_style, $path);
301 1
            return true;
302
        }
303
        debug_add("The element '{$path}' could not be found.", MIDCOM_LOG_INFO);
304
        return false;
305
    }
306
307
    /**
308
     * Try to get element from default style snippet
309
     */
310 202
    private function _get_element_from_snippet(string $_element)
311
    {
312 202
        $src = "{$this->_snippetdir}/{$_element}";
313 202
        if (array_key_exists($src, $this->_snippets)) {
314 14
            return $this->_snippets[$src];
315
        }
316 202
        if (   midcom::get()->config->get('theme')
317 202
            && $content = midcom_helper_misc::get_element_content($_element)) {
318 6
            $this->_snippets[$src] = $content;
319 6
            return $content;
320
        }
321
322 202
        $current_context = midcom_core_context::get()->id;
323 202
        foreach ($this->_styledirs[$current_context] as $path) {
324 202
            $filename = $path . "/{$_element}.php";
325 202
            if (file_exists($filename)) {
326 192
                if (!array_key_exists($filename, $this->_snippets)) {
327 147
                    $this->_snippets[$filename] = file_get_contents($filename);
328
                }
329 192
                return $this->_snippets[$filename];
330
            }
331
        }
332 24
        return false;
333
    }
334
335
    /**
336
     * Gets the component style.
337
     *
338
     * @todo Document
339
     *
340
     * @return int Database ID if the style to use in current view or false
341
     */
342 268
    private function _get_component_style()
343
    {
344 268
        $_st = false;
345 268
        if (!$this->_topic) {
346 1
            return $_st;
347
        }
348
        // get user defined style for component
349
        // style inheritance
350
        // should this be cached somehow?
351 268
        if ($style = $this->_topic->style ?: midcom_core_context::get()->get_inherited_style()) {
352
            if (substr($style, 0, 6) === 'theme:') {
353
                $theme_dir = OPENPSA2_THEME_ROOT . midcom::get()->config->get('theme') . '/style';
354
                $parts = explode('/', str_replace('theme:/', '', $style));
355
356
                foreach ($parts as &$part) {
357
                    $theme_dir .= '/' . $part;
358
                    $part = $theme_dir;
359
                }
360
                foreach (array_reverse(array_filter($parts, 'is_dir')) as $dirname) {
361
                    $this->prepend_styledir($dirname);
362
                }
363
            } else {
364
                $_st = midcom_db_style::id_from_path($style);
365
            }
366
        } else {
367
            // Get style from sitewide per-component defaults.
368 268
            $styleengine_default_styles = midcom::get()->config->get('styleengine_default_styles');
369 268
            if (isset($styleengine_default_styles[$this->_topic->component])) {
370
                $_st = midcom_db_style::id_from_path($styleengine_default_styles[$this->_topic->component]);
371
            }
372
        }
373
374 268
        if ($_st) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $_st of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
375
            $substyle = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_SUBSTYLE);
376
377
            if (is_string($substyle)) {
378
                $chain = explode('/', $substyle);
379
                foreach ($chain as $stylename) {
380
                    if ($_subst_id = midcom_db_style::id_from_path($stylename, $_st)) {
381
                        $_st = $_subst_id;
382
                    }
383
                }
384
            }
385
        }
386 268
        return $_st;
387
    }
388
389
    /**
390
     * Gets the component styledir associated with the topic's component.
391
     *
392
     * @return mixed the path to the component's style directory.
393
     */
394 268
    private function _get_component_snippetdir()
395
    {
396
        // get component's snippetdir (for default styles)
397 268
        $loader = midcom::get()->componentloader;
398 268
        if (empty($this->_topic->component)) {
399 1
            return null;
400
        }
401 268
        return $loader->path_to_snippetpath($this->_topic->component) . "/style";
402
    }
403
404
    /**
405
     * Adds an extra style directory to check for style elements at
406
     * the end of the styledir queue.
407
     *
408
     * @param string $dirname path of style directory within midcom.
409
     * @throws midcom_error exception if directory does not exist.
410
     */
411 73
    function append_styledir($dirname)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
412
    {
413 73
        if (!file_exists($dirname)) {
414
            throw new midcom_error("Style directory $dirname does not exist!");
415
        }
416 73
        $this->_styledirs_append[midcom_core_context::get()->id][] = $dirname;
417 73
    }
418
419
    /**
420
     * Function prepend styledir
421
     *
422
     * @param string $dirname path of styledirectory within midcom.
423
     * @return boolean true if directory appended
424
     * @throws midcom_error if directory does not exist.
425
     */
426 81
    function prepend_styledir($dirname)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
427
    {
428 81
        if (!file_exists($dirname)) {
429
            throw new midcom_error("Style directory {$dirname} does not exist.");
430
        }
431 81
        $this->_styledirs_prepend[midcom_core_context::get()->id][] = $dirname;
432 81
        return true;
433
    }
434
435
    /**
436
     * Append the styledir of a component to the queue of styledirs.
437
     *
438
     * @param string $component Component name
439
     * @throws midcom_error exception if directory does not exist.
440
     */
441
    function append_component_styledir($component)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
442
    {
443
        $loader = midcom::get()->componentloader;
444
        $path = $loader->path_to_snippetpath($component) . "/style";
445
        $this->append_styledir($path);
446
    }
447
448
    /**
449
     * Prepend the styledir of a component
450
     *
451
     * @param string $component component name
452
     */
453 81
    public function prepend_component_styledir($component)
454
    {
455 81
        $loader = midcom::get()->componentloader;
456 81
        $path = $loader->path_to_snippetpath($component) . "/style";
457 81
        $this->prepend_styledir($path);
458 81
    }
459
460
    /**
461
     * Appends a substyle after the currently selected component style.
462
     *
463
     * Enables a depth of more than one style during substyle selection.
464
     *
465
     * @param string $newsub The substyle to append.
466
     */
467 31
    public function append_substyle($newsub)
468
    {
469
        // Make sure try to use only the first argument if we get space separated list, fixes #1788
470 31
        if (strpos($newsub, ' ') !== false) {
471
            $newsub = preg_replace('/^(.+?) .+/', '$1', $newsub);
472
        }
473
474 31
        $context = midcom_core_context::get();
475 31
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
476
477 31
        if (!empty($current_style)) {
478
            $newsub = $current_style . '/' . $newsub;
479
        }
480
481 31
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
482 31
    }
483
484
    /**
485
     * Prepends a substyle before the currently selected component style.
486
     *
487
     * Enables a depth of more than one style during substyle selection.
488
     *
489
     * @param string $newsub The substyle to prepend.
490
     */
491
    function prepend_substyle($newsub)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
492
    {
493
        $context = midcom_core_context::get();
494
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
495
496
        if (!empty($current_style)) {
497
            $newsub .= "/" . $current_style;
498
        }
499
        debug_add("Updating Component Context Substyle from $current_style to $newsub");
500
501
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
502
    }
503
504
    /**
505
     * Switches the context (see dynamic load).
506
     *
507
     * Private variables are adjusted, and the prepend and append styles are merged with the componentstyle.
508
     * You cannot change the style stack after that (unless you call enter_context again of course).
509
     *
510
     * @param midcom_core_context $context The context to enter
511
     */
512 268
    public function enter_context(midcom_core_context $context)
513
    {
514
        // set new context and topic
515 268
        array_unshift($this->_context, $context); // push into context stack
516
517 268
        $this->_topic = $context->get_key(MIDCOM_CONTEXT_CONTENTTOPIC);
0 ignored issues
show
Documentation Bug introduced by
It seems like $context->get_key(MIDCOM_CONTEXT_CONTENTTOPIC) can also be of type false. However, the property $_topic is declared as type midcom_db_topic. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
518
519
        // Prepare styledir stacks
520 268
        if (!isset($this->_styledirs_prepend[$context->id])) {
521 200
            $this->_styledirs_prepend[$context->id] = [];
522
        }
523 268
        if (!isset($this->_styledirs_append[$context->id])) {
524 195
            $this->_styledirs_append[$context->id] = [];
525
        }
526
527 268
        if ($_st = $this->_get_component_style()) {
528
            array_unshift($this->_scope, $_st);
529
        }
530
531 268
        $this->_snippetdir = $this->_get_component_snippetdir();
532
533 268
        $this->_styledirs[$context->id] = array_merge(
534 268
            $this->_styledirs_prepend[$context->id],
535 268
            [$this->_snippetdir],
536 268
            $this->_styledirs_append[$context->id]
537
        );
538 268
    }
539
540
    /**
541
     * Switches the context (see dynamic load). Private variables $_context, $_topic
542
     * and $_snippetdir are adjusted.
543
     *
544
     * @todo check documentation
545
     */
546 268
    public function leave_context()
547
    {
548 268
        if ($this->_get_component_style()) {
549
            array_shift($this->_scope);
550
        }
551 268
        array_shift($this->_context);
552
553 268
        $previous_context = $this->_context[0] ?? midcom_core_context::get();
554 268
        $this->_topic = $previous_context->get_key(MIDCOM_CONTEXT_CONTENTTOPIC);
0 ignored issues
show
Documentation Bug introduced by
It seems like $previous_context->get_k...M_CONTEXT_CONTENTTOPIC) can also be of type false. However, the property $_topic is declared as type midcom_db_topic. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
555
556 268
        $this->_snippetdir = $this->_get_component_snippetdir();
557 268
    }
558
}
559