Passed
Push — master ( 5bfcaa...1c78a6 )
by Andreas
16:41
created

midcom_helper__styleloader::load()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.432

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 2
b 0
f 0
nc 8
nop 1
dl 0
loc 17
ccs 7
cts 10
cp 0.7
crap 4.432
rs 9.9666
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 === null) {
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
     */
203 202
    public function load($path) : ?string
204
    {
205 202
        $element = $path;
206
        // we have full qualified path to element
207 202
        if (preg_match("|(.*)/(.*)|", $path, $matches)) {
208
            $styleid = midcom_db_style::id_from_path($matches[1]);
209
            $element = $matches[2];
210
        }
211
212 202
        if ($styleid = $styleid ?? $this->_scope[0] ?? null) {
213
            $style = $this->_get_element_in_styletree($styleid, $element);
214
        }
215
216 202
        if (empty($style)) {
217 202
            $style = $this->_get_element_from_snippet($element);
218
        }
219 202
        return $style;
220
    }
221
222
    /**
223
     * Renders the style element with current request data
224
     *
225
     * @param string $style The style element content
226
     * @param string $path the element's name
227
     * @throws midcom_error
228
     */
229 201
    private function render(string $style, string $path)
230
    {
231 201
        if (midcom::get()->config->get('wrap_style_show_with_name')) {
232
            $style = "\n<!-- Start of style '{$path}' -->\n" . $style;
233
            $style .= "\n<!-- End of style '{$path}' -->\n";
234
        }
235
236
        // This is a bit of a hack to allow &(); tags
237 201
        $preparsed = midcom_helper_misc::preparse($style);
238 201
        if (midcom_core_context::get()->has_custom_key('request_data')) {
239 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

239
            $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...
240
        }
241
242 201
        if (eval('?>' . $preparsed) === false) {
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
243
            // Note that src detection will be semi-reliable, as it depends on all errors being
244
            // found before caching kicks in.
245
            throw new midcom_error("Failed to parse style element '{$path}', see above for PHP errors.");
246
        }
247 201
    }
248
249
    /**
250
     * Looks for a midcom core style element matching $path and displays/evaluates it.
251
     * This offers a bit reduced functionality and will only look in the DB root style,
252
     * the theme directory and midcom's style directory, because it has to work even when
253
     * midcom is not yet fully initialized
254
     *
255
     * @param string $path    The style element to show.
256
     * @return boolean            True on success, false otherwise.
257
     */
258 1
    public function show_midcom($path) : bool
259
    {
260 1
        $_element = $path;
261 1
        $_style = null;
262
263 1
        $context = midcom_core_context::get();
264
265
        try {
266 1
            $root_topic = $context->get_key(MIDCOM_CONTEXT_ROOTTOPIC);
267 1
            if (   $root_topic->style
268 1
                && $db_style = midcom_db_style::id_from_path($root_topic->style)) {
269 1
                $_style = $this->_get_element_in_styletree($db_style, $_element);
270
            }
271
        } catch (midcom_error_forbidden $e) {
272
            $e->log();
273
        }
274
275 1
        if ($_style === null) {
276 1
            if (isset($this->_styledirs[$context->id])) {
277 1
                $styledirs_backup = $this->_styledirs;
278
            }
279 1
            $this->_snippetdir = MIDCOM_ROOT . '/midcom/style';
280 1
            $this->_styledirs[$context->id][0] = $this->_snippetdir;
281
282 1
            $_style = $this->_get_element_from_snippet($_element);
283
284 1
            if (isset($styledirs_backup)) {
285 1
                $this->_styledirs = $styledirs_backup;
286
            }
287
        }
288
289 1
        if ($_style !== null) {
290 1
            $this->render($_style, $path);
291 1
            return true;
292
        }
293
        debug_add("The element '{$path}' could not be found.", MIDCOM_LOG_INFO);
294
        return false;
295
    }
296
297
    /**
298
     * Try to get element from default style snippet
299
     */
300 202
    private function _get_element_from_snippet(string $_element) : ?string
301
    {
302 202
        $src = "{$this->_snippetdir}/{$_element}";
303 202
        if (array_key_exists($src, $this->_snippets)) {
304 14
            return $this->_snippets[$src];
305
        }
306 202
        if (   midcom::get()->config->get('theme')
307 202
            && $content = midcom_helper_misc::get_element_content($_element)) {
308 6
            $this->_snippets[$src] = $content;
309 6
            return $content;
310
        }
311
312 202
        $current_context = midcom_core_context::get()->id;
313 202
        foreach ($this->_styledirs[$current_context] as $path) {
314 202
            $filename = $path . "/{$_element}.php";
315 202
            if (file_exists($filename)) {
316 192
                if (!array_key_exists($filename, $this->_snippets)) {
317 147
                    $this->_snippets[$filename] = file_get_contents($filename);
318
                }
319 192
                return $this->_snippets[$filename];
320
            }
321
        }
322 24
        return null;
323
    }
324
325
    /**
326
     * Gets the component style.
327
     *
328
     * @todo Document
329
     *
330
     * @return int Database ID if the style to use in current view or false
331
     */
332 268
    private function _get_component_style()
333
    {
334 268
        $_st = false;
335 268
        if (!$this->_topic) {
336 1
            return $_st;
337
        }
338
        // get user defined style for component
339
        // style inheritance
340
        // should this be cached somehow?
341 268
        if ($style = $this->_topic->style ?: midcom_core_context::get()->get_inherited_style()) {
342
            if (substr($style, 0, 6) === 'theme:') {
343
                $theme_dir = OPENPSA2_THEME_ROOT . midcom::get()->config->get('theme') . '/style';
344
                $parts = explode('/', str_replace('theme:/', '', $style));
345
346
                foreach ($parts as &$part) {
347
                    $theme_dir .= '/' . $part;
348
                    $part = $theme_dir;
349
                }
350
                foreach (array_reverse(array_filter($parts, 'is_dir')) as $dirname) {
351
                    $this->prepend_styledir($dirname);
352
                }
353
            } else {
354
                $_st = midcom_db_style::id_from_path($style);
355
            }
356
        } else {
357
            // Get style from sitewide per-component defaults.
358 268
            $styleengine_default_styles = midcom::get()->config->get('styleengine_default_styles');
359 268
            if (isset($styleengine_default_styles[$this->_topic->component])) {
360
                $_st = midcom_db_style::id_from_path($styleengine_default_styles[$this->_topic->component]);
361
            }
362
        }
363
364 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...
365
            $substyle = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_SUBSTYLE);
366
367
            if (is_string($substyle)) {
368
                $chain = explode('/', $substyle);
369
                foreach ($chain as $stylename) {
370
                    if ($_subst_id = midcom_db_style::id_from_path($stylename, $_st)) {
371
                        $_st = $_subst_id;
372
                    }
373
                }
374
            }
375
        }
376 268
        return $_st;
377
    }
378
379
    /**
380
     * Gets the component styledir associated with the topic's component.
381
     *
382
     * @return mixed the path to the component's style directory.
383
     */
384 268
    private function _get_component_snippetdir()
385
    {
386
        // get component's snippetdir (for default styles)
387 268
        $loader = midcom::get()->componentloader;
388 268
        if (empty($this->_topic->component)) {
389 1
            return null;
390
        }
391 268
        return $loader->path_to_snippetpath($this->_topic->component) . "/style";
392
    }
393
394
    /**
395
     * Adds an extra style directory to check for style elements at
396
     * the end of the styledir queue.
397
     *
398
     * @param string $dirname path of style directory within midcom.
399
     * @throws midcom_error exception if directory does not exist.
400
     */
401 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...
402
    {
403 73
        if (!file_exists($dirname)) {
404
            throw new midcom_error("Style directory $dirname does not exist!");
405
        }
406 73
        $this->_styledirs_append[midcom_core_context::get()->id][] = $dirname;
407 73
    }
408
409
    /**
410
     * Function prepend styledir
411
     *
412
     * @param string $dirname path of styledirectory within midcom.
413
     * @return boolean true if directory appended
414
     * @throws midcom_error if directory does not exist.
415
     */
416 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...
417
    {
418 81
        if (!file_exists($dirname)) {
419
            throw new midcom_error("Style directory {$dirname} does not exist.");
420
        }
421 81
        $this->_styledirs_prepend[midcom_core_context::get()->id][] = $dirname;
422 81
        return true;
423
    }
424
425
    /**
426
     * Append the styledir of a component to the queue of styledirs.
427
     *
428
     * @param string $component Component name
429
     * @throws midcom_error exception if directory does not exist.
430
     */
431
    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...
432
    {
433
        $loader = midcom::get()->componentloader;
434
        $path = $loader->path_to_snippetpath($component) . "/style";
435
        $this->append_styledir($path);
436
    }
437
438
    /**
439
     * Prepend the styledir of a component
440
     *
441
     * @param string $component component name
442
     */
443 81
    public function prepend_component_styledir($component)
444
    {
445 81
        $loader = midcom::get()->componentloader;
446 81
        $path = $loader->path_to_snippetpath($component) . "/style";
447 81
        $this->prepend_styledir($path);
448 81
    }
449
450
    /**
451
     * Appends a substyle after the currently selected component style.
452
     *
453
     * Enables a depth of more than one style during substyle selection.
454
     *
455
     * @param string $newsub The substyle to append.
456
     */
457 31
    public function append_substyle($newsub)
458
    {
459
        // Make sure try to use only the first argument if we get space separated list, fixes #1788
460 31
        if (strpos($newsub, ' ') !== false) {
461
            $newsub = preg_replace('/^(.+?) .+/', '$1', $newsub);
462
        }
463
464 31
        $context = midcom_core_context::get();
465 31
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
466
467 31
        if (!empty($current_style)) {
468
            $newsub = $current_style . '/' . $newsub;
469
        }
470
471 31
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
472 31
    }
473
474
    /**
475
     * Prepends a substyle before the currently selected component style.
476
     *
477
     * Enables a depth of more than one style during substyle selection.
478
     *
479
     * @param string $newsub The substyle to prepend.
480
     */
481
    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...
482
    {
483
        $context = midcom_core_context::get();
484
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
485
486
        if (!empty($current_style)) {
487
            $newsub .= "/" . $current_style;
488
        }
489
        debug_add("Updating Component Context Substyle from $current_style to $newsub");
490
491
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
492
    }
493
494
    /**
495
     * Switches the context (see dynamic load).
496
     *
497
     * Private variables are adjusted, and the prepend and append styles are merged with the componentstyle.
498
     * You cannot change the style stack after that (unless you call enter_context again of course).
499
     *
500
     * @param midcom_core_context $context The context to enter
501
     */
502 268
    public function enter_context(midcom_core_context $context)
503
    {
504
        // set new context and topic
505 268
        array_unshift($this->_context, $context); // push into context stack
506
507 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...
508
509
        // Prepare styledir stacks
510 268
        if (!isset($this->_styledirs_prepend[$context->id])) {
511 200
            $this->_styledirs_prepend[$context->id] = [];
512
        }
513 268
        if (!isset($this->_styledirs_append[$context->id])) {
514 195
            $this->_styledirs_append[$context->id] = [];
515
        }
516
517 268
        if ($_st = $this->_get_component_style()) {
518
            array_unshift($this->_scope, $_st);
519
        }
520
521 268
        $this->_snippetdir = $this->_get_component_snippetdir();
522
523 268
        $this->_styledirs[$context->id] = array_merge(
524 268
            $this->_styledirs_prepend[$context->id],
525 268
            [$this->_snippetdir],
526 268
            $this->_styledirs_append[$context->id]
527
        );
528 268
    }
529
530
    /**
531
     * Switches the context (see dynamic load). Private variables $_context, $_topic
532
     * and $_snippetdir are adjusted.
533
     *
534
     * @todo check documentation
535
     */
536 268
    public function leave_context()
537
    {
538 268
        if ($this->_get_component_style()) {
539
            array_shift($this->_scope);
540
        }
541 268
        array_shift($this->_context);
542
543 268
        $previous_context = $this->_context[0] ?? midcom_core_context::get();
544 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...
545
546 268
        $this->_snippetdir = $this->_get_component_snippetdir();
547 268
    }
548
}
549