Passed
Push — master ( 1c78a6...1ce7c8 )
by Andreas
16:39
created

midcom_helper__styleloader::enter_context()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 13
nc 8
nop 1
dl 0
loc 24
ccs 14
cts 14
cp 1
crap 4
rs 9.8333
c 2
b 0
f 0
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 topic
60
     *
61
     * @var midcom_db_topic
62
     */
63
    private $_topic;
64
65
    /**
66
     * Default style path
67
     *
68
     * @var string
69
     */
70
    private $_snippetdir;
71
72
    /**
73
     * Context stack
74
     *
75
     * @var midcom_core_context[]
76
     */
77
    private $_context = [];
78
79
    /**
80
     * Default style element cache
81
     *
82
     * @var array
83
     */
84
    private $_snippets = [];
85
86
    /**
87
     * List of styledirs to handle after componentstyle
88
     *
89
     * @var array
90
     */
91
    private $_styledirs_append = [];
92
93
    /**
94
     * List of styledirs to handle before componentstyle
95
     *
96
     * @var array
97
     */
98
    private $_styledirs_prepend = [];
99
100
    /**
101
     * The stack of directories to check for styles.
102
     */
103
    private $_styledirs = [];
104
105
    /**
106
     * Data to pass to the style
107
     *
108
     * @var array
109
     */
110
    public $data;
111
112
    /**
113
     * Returns a style element that matches $name and is in style $id.
114
     * It also returns an element if it is not in the given style,
115
     * but in one of its parent styles.
116
     *
117
     * @param int $id        The style id to search in.
118
     * @param string $name    The element to locate.
119
     * @return string    Value of the found element, or false on failure.
120
     */
121
    private function _get_element_in_styletree($id, string $name) : ?string
122
    {
123
        static $cached = [];
124
        $cache_key = $id . '::' . $name;
125
126
        if (array_key_exists($cache_key, $cached)) {
127
            return $cached[$cache_key];
128
        }
129
130
        $element_mc = midgard_element::new_collector('style', $id);
131
        $element_mc->set_key_property('guid');
132
        $element_mc->add_value_property('value');
133
        $element_mc->add_constraint('name', '=', $name);
134
        $element_mc->execute();
135
136
        if ($keys = $element_mc->list_keys()) {
137
            $element_guid = key($keys);
138
            $cached[$cache_key] = $element_mc->get_subkey($element_guid, 'value');
139
            midcom::get()->cache->content->register($element_guid);
140
            return $cached[$cache_key];
141
        }
142
143
        // No such element on this level, check parents
144
        $style_mc = midgard_style::new_collector('id', $id);
145
        $style_mc->set_key_property('guid');
146
        $style_mc->add_value_property('up');
147
        $style_mc->add_constraint('up', '>', 0);
148
        $style_mc->execute();
149
150
        if ($keys = $style_mc->list_keys()) {
151
            $style_guid = key($keys);
152
            midcom::get()->cache->content->register($style_guid);
153
            $up = $style_mc->get_subkey($style_guid, 'up');
154
            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

154
            return $this->_get_element_in_styletree(/** @scrutinizer ignore-type */ $up, $name);
Loading history...
155
        }
156
157
        $cached[$cache_key] = null;
158
        return $cached[$cache_key];
159
    }
160
161
    /**
162
     * Looks for a style element matching $path (either in a user defined style
163
     * or the default style snippetdir) and displays/evaluates it.
164
     *
165
     * @param string $path    The style element to show.
166
     * @return boolean            True on success, false otherwise.
167
     */
168 202
    public function show($path) : bool
169
    {
170 202
        if ($this->_context === []) {
171
            debug_add("Trying to show '{$path}' but there is no context set", MIDCOM_LOG_INFO);
172
            return false;
173
        }
174
175 202
        $style = $this->load($path);
176
177 202
        if ($style === null) {
178 24
            if ($path == 'ROOT') {
179
                // Go to fallback ROOT instead of displaying a blank page
180
                return $this->show_midcom($path);
181
            }
182
183 24
            debug_add("The element '{$path}' could not be found.", MIDCOM_LOG_INFO);
184 24
            return false;
185
        }
186 201
        $this->render($style, $path);
187
188 201
        return true;
189
    }
190
191
    /**
192
     * Load style element content
193
     *
194
     * @param string $path The element name
195
     */
196 202
    public function load($path) : ?string
197
    {
198 202
        $element = $path;
199
        // we have full qualified path to element
200 202
        if (preg_match("|(.*)/(.*)|", $path, $matches)) {
201
            $styleid = midcom_db_style::id_from_path($matches[1]);
202
            $element = $matches[2];
203
        }
204
205 202
        if ($styleid = $styleid ?? $this->_context[0]->get_custom_key(midcom_db_style::class)) {
206
            $style = $this->_get_element_in_styletree($styleid, $element);
207
        }
208
209 202
        if (empty($style)) {
210 202
            $style = $this->_get_element_from_snippet($element);
211
        }
212 202
        return $style;
213
    }
214
215
    /**
216
     * Renders the style element with current request data
217
     *
218
     * @param string $style The style element content
219
     * @param string $path the element's name
220
     * @throws midcom_error
221
     */
222 201
    private function render(string $style, string $path)
223
    {
224 201
        if (midcom::get()->config->get('wrap_style_show_with_name')) {
225
            $style = "\n<!-- Start of style '{$path}' -->\n" . $style;
226
            $style .= "\n<!-- End of style '{$path}' -->\n";
227
        }
228
229
        // This is a bit of a hack to allow &(); tags
230 201
        $preparsed = midcom_helper_misc::preparse($style);
231 201
        if (midcom_core_context::get()->has_custom_key('request_data')) {
232 201
            $data =& midcom_core_context::get()->get_custom_key('request_data');
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
233
        }
234
235 201
        if (eval('?>' . $preparsed) === false) {
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
236
            // Note that src detection will be semi-reliable, as it depends on all errors being
237
            // found before caching kicks in.
238
            throw new midcom_error("Failed to parse style element '{$path}', see above for PHP errors.");
239
        }
240 201
    }
241
242
    /**
243
     * Looks for a midcom core style element matching $path and displays/evaluates it.
244
     * This offers a bit reduced functionality and will only look in the DB root style,
245
     * the theme directory and midcom's style directory, because it has to work even when
246
     * midcom is not yet fully initialized
247
     *
248
     * @param string $path    The style element to show.
249
     * @return boolean            True on success, false otherwise.
250
     */
251 1
    public function show_midcom($path) : bool
252
    {
253 1
        $_element = $path;
254 1
        $_style = null;
255
256 1
        $context = midcom_core_context::get();
257
258
        try {
259 1
            $root_topic = $context->get_key(MIDCOM_CONTEXT_ROOTTOPIC);
260 1
            if (   $root_topic->style
261 1
                && $db_style = midcom_db_style::id_from_path($root_topic->style)) {
262 1
                $_style = $this->_get_element_in_styletree($db_style, $_element);
263
            }
264
        } catch (midcom_error_forbidden $e) {
265
            $e->log();
266
        }
267
268 1
        if ($_style === null) {
269 1
            if (isset($this->_styledirs[$context->id])) {
270 1
                $styledirs_backup = $this->_styledirs;
271
            }
272 1
            $this->_snippetdir = MIDCOM_ROOT . '/midcom/style';
273 1
            $this->_styledirs[$context->id][0] = $this->_snippetdir;
274
275 1
            $_style = $this->_get_element_from_snippet($_element);
276
277 1
            if (isset($styledirs_backup)) {
278 1
                $this->_styledirs = $styledirs_backup;
279
            }
280
        }
281
282 1
        if ($_style !== null) {
283 1
            $this->render($_style, $path);
284 1
            return true;
285
        }
286
        debug_add("The element '{$path}' could not be found.", MIDCOM_LOG_INFO);
287
        return false;
288
    }
289
290
    /**
291
     * Try to get element from default style snippet
292
     */
293 202
    private function _get_element_from_snippet(string $_element) : ?string
294
    {
295 202
        $src = "{$this->_snippetdir}/{$_element}";
296 202
        if (array_key_exists($src, $this->_snippets)) {
297 14
            return $this->_snippets[$src];
298
        }
299 202
        if (   midcom::get()->config->get('theme')
300 202
            && $content = midcom_helper_misc::get_element_content($_element)) {
301 6
            $this->_snippets[$src] = $content;
302 6
            return $content;
303
        }
304
305 202
        $current_context = midcom_core_context::get()->id;
306 202
        foreach ($this->_styledirs[$current_context] as $path) {
307 202
            $filename = $path . "/{$_element}.php";
308 202
            if (file_exists($filename)) {
309 192
                if (!array_key_exists($filename, $this->_snippets)) {
310 147
                    $this->_snippets[$filename] = file_get_contents($filename);
311
                }
312 192
                return $this->_snippets[$filename];
313
            }
314
        }
315 24
        return null;
316
    }
317
318
    /**
319
     * Gets the component styledir associated with the topic's component.
320
     *
321
     * @return mixed the path to the component's style directory.
322
     */
323 268
    private function _get_component_snippetdir() : ?string
324
    {
325 268
        if (empty($this->_topic->component)) {
326 1
            return null;
327
        }
328 268
        return midcom::get()->componentloader->path_to_snippetpath($this->_topic->component) . "/style";
329
    }
330
331
    /**
332
     * Adds an extra style directory to check for style elements at
333
     * the end of the styledir queue.
334
     *
335
     * @param string $dirname path of style directory within midcom.
336
     * @throws midcom_error exception if directory does not exist.
337
     */
338 73
    function append_styledir($dirname)
339
    {
340 73
        if (!file_exists($dirname)) {
341
            throw new midcom_error("Style directory $dirname does not exist!");
342
        }
343 73
        $this->_styledirs_append[midcom_core_context::get()->id][] = $dirname;
344 73
    }
345
346
    /**
347
     * Function prepend styledir
348
     *
349
     * @param string $dirname path of styledirectory within midcom.
350
     * @return boolean true if directory appended
351
     * @throws midcom_error if directory does not exist.
352
     */
353 81
    function prepend_styledir($dirname)
354
    {
355 81
        if (!file_exists($dirname)) {
356
            throw new midcom_error("Style directory {$dirname} does not exist.");
357
        }
358 81
        $this->_styledirs_prepend[midcom_core_context::get()->id][] = $dirname;
359 81
        return true;
360
    }
361
362
    /**
363
     * Append the styledir of a component to the queue of styledirs.
364
     *
365
     * @param string $component Component name
366
     * @throws midcom_error exception if directory does not exist.
367
     */
368
    function append_component_styledir($component)
369
    {
370
        $loader = midcom::get()->componentloader;
371
        $path = $loader->path_to_snippetpath($component) . "/style";
372
        $this->append_styledir($path);
373
    }
374
375
    /**
376
     * Prepend the styledir of a component
377
     *
378
     * @param string $component component name
379
     */
380 81
    public function prepend_component_styledir($component)
381
    {
382 81
        $loader = midcom::get()->componentloader;
383 81
        $path = $loader->path_to_snippetpath($component) . "/style";
384 81
        $this->prepend_styledir($path);
385 81
    }
386
387
    /**
388
     * Appends a substyle after the currently selected component style.
389
     *
390
     * Enables a depth of more than one style during substyle selection.
391
     *
392
     * @param string $newsub The substyle to append.
393
     */
394 31
    public function append_substyle($newsub)
395
    {
396
        // Make sure try to use only the first argument if we get space separated list, fixes #1788
397 31
        if (strpos($newsub, ' ') !== false) {
398
            $newsub = preg_replace('/^(.+?) .+/', '$1', $newsub);
399
        }
400
401 31
        $context = midcom_core_context::get();
402 31
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
403
404 31
        if (!empty($current_style)) {
405
            $newsub = $current_style . '/' . $newsub;
406
        }
407
408 31
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
409 31
    }
410
411
    /**
412
     * Prepends a substyle before the currently selected component style.
413
     *
414
     * Enables a depth of more than one style during substyle selection.
415
     *
416
     * @param string $newsub The substyle to prepend.
417
     */
418
    function prepend_substyle($newsub)
419
    {
420
        $context = midcom_core_context::get();
421
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
422
423
        if (!empty($current_style)) {
424
            $newsub .= "/" . $current_style;
425
        }
426
        debug_add("Updating Component Context Substyle from $current_style to $newsub");
427
428
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
429
    }
430
431
    /**
432
     * Switches the context (see dynamic load).
433
     *
434
     * Private variables are adjusted, and the prepend and append styles are merged with the componentstyle.
435
     * You cannot change the style stack after that (unless you call enter_context again of course).
436
     *
437
     * @param midcom_core_context $context The context to enter
438
     */
439 268
    public function enter_context(midcom_core_context $context)
440
    {
441
        // set new context and topic
442 268
        array_unshift($this->_context, $context); // push into context stack
443
444 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...
445
446
        // Prepare styledir stacks
447 268
        if (!isset($this->_styledirs_prepend[$context->id])) {
448 200
            $this->_styledirs_prepend[$context->id] = [];
449
        }
450 268
        if (!isset($this->_styledirs_append[$context->id])) {
451 195
            $this->_styledirs_append[$context->id] = [];
452
        }
453
454 268
        if ($this->_topic) {
455 268
            $this->initialize_from_topic($context);
456
        }
457 268
        $this->_snippetdir = $this->_get_component_snippetdir();
458
459 268
        $this->_styledirs[$context->id] = array_merge(
460 268
            $this->_styledirs_prepend[$context->id],
461 268
            [$this->_snippetdir],
462 268
            $this->_styledirs_append[$context->id]
463
        );
464 268
    }
465
466
    /**
467
     * Initializes style sources from topic
468
     */
469 268
    private function initialize_from_topic(midcom_core_context $context)
470
    {
471 268
        $_st = 0;
472
        // get user defined style for component
473
        // style inheritance
474
        // should this be cached somehow?
475 268
        if ($style = $this->_topic->style ?: $context->get_inherited_style()) {
476
            if (substr($style, 0, 6) === 'theme:') {
477
                $theme_dir = OPENPSA2_THEME_ROOT . midcom::get()->config->get('theme') . '/style';
478
                $parts = explode('/', str_replace('theme:/', '', $style));
479
480
                foreach ($parts as &$part) {
481
                    $theme_dir .= '/' . $part;
482
                    $part = $theme_dir;
483
                }
484
                foreach (array_reverse(array_filter($parts, 'is_dir')) as $dirname) {
485
                    $this->prepend_styledir($dirname);
486
                }
487
            } else {
488
                $_st = midcom_db_style::id_from_path($style);
489
            }
490
        } else {
491
            // Get style from sitewide per-component defaults.
492 268
            $styleengine_default_styles = midcom::get()->config->get('styleengine_default_styles');
493 268
            if (isset($styleengine_default_styles[$this->_topic->component])) {
494
                $_st = midcom_db_style::id_from_path($styleengine_default_styles[$this->_topic->component]);
495
            }
496
        }
497
498 268
        if ($_st) {
499
            $substyle = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
500
501
            if (is_string($substyle)) {
502
                $chain = explode('/', $substyle);
503
                foreach ($chain as $stylename) {
504
                    if ($_subst_id = midcom_db_style::id_from_path($stylename, $_st)) {
505
                        $_st = $_subst_id;
506
                    }
507
                }
508
            }
509
        }
510 268
        $context->set_custom_key(midcom_db_style::class, $_st);
511 268
    }
512
513
    /**
514
     * Switches the context (see dynamic load). Private variables $_context, $_topic
515
     * and $_snippetdir are adjusted.
516
     *
517
     * @todo check documentation
518
     */
519 268
    public function leave_context()
520
    {
521 268
        array_shift($this->_context);
522
523 268
        $previous_context = $this->_context[0] ?? midcom_core_context::get();
524 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...
525
526 268
        $this->_snippetdir = $this->_get_component_snippetdir();
527 268
    }
528
}
529