Passed
Push — master ( 1ce7c8...9eeb8b )
by Andreas
22:45
created

midcom_helper__styleloader::render()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.5923

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
nc 8
nop 2
dl 0
loc 17
ccs 6
cts 9
cp 0.6667
crap 4.5923
rs 10
c 1
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
     * @throws midcom_error exception if directory does not exist.
336
     */
337 73
    public function append_styledir(string $dirname)
338
    {
339 73
        if (!file_exists($dirname)) {
340
            throw new midcom_error("Style directory $dirname does not exist!");
341
        }
342 73
        $this->_styledirs_append[midcom_core_context::get()->id][] = $dirname;
343 73
    }
344
345
    /**
346
     * Function prepend styledir
347
     */
348 81
    public function prepend_styledir(string $dirname)
349
    {
350 81
        if (!file_exists($dirname)) {
351
            throw new midcom_error("Style directory {$dirname} does not exist.");
352
        }
353 81
        $this->_styledirs_prepend[midcom_core_context::get()->id][] = $dirname;
354 81
    }
355
356
    /**
357
     * Append the styledir of a component to the queue of styledirs.
358
     */
359
    public function append_component_styledir(string $component)
360
    {
361
        $loader = midcom::get()->componentloader;
362
        $path = $loader->path_to_snippetpath($component) . "/style";
363
        $this->append_styledir($path);
364
    }
365
366
    /**
367
     * Prepend the styledir of a component
368
     */
369 81
    public function prepend_component_styledir(string $component)
370
    {
371 81
        $loader = midcom::get()->componentloader;
372 81
        $path = $loader->path_to_snippetpath($component) . "/style";
373 81
        $this->prepend_styledir($path);
374 81
    }
375
376
    /**
377
     * Appends a substyle after the currently selected component style.
378
     *
379
     * Enables a depth of more than one style during substyle selection.
380
     */
381 31
    public function append_substyle(string $newsub)
382
    {
383
        // Make sure try to use only the first argument if we get space separated list, fixes #1788
384 31
        if (strpos($newsub, ' ') !== false) {
385
            $newsub = preg_replace('/^(.+?) .+/', '$1', $newsub);
386
        }
387
388 31
        $context = midcom_core_context::get();
389 31
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
390
391 31
        if (!empty($current_style)) {
392
            $newsub = $current_style . '/' . $newsub;
393
        }
394
395 31
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
396 31
    }
397
398
    /**
399
     * Prepends a substyle before the currently selected component style.
400
     *
401
     * Enables a depth of more than one style during substyle selection.
402
     *
403
     * @param string $newsub The substyle to prepend.
404
     */
405
    function prepend_substyle($newsub)
406
    {
407
        $context = midcom_core_context::get();
408
        $current_style = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
409
410
        if (!empty($current_style)) {
411
            $newsub .= "/" . $current_style;
412
        }
413
        debug_add("Updating Component Context Substyle from $current_style to $newsub");
414
415
        $context->set_key(MIDCOM_CONTEXT_SUBSTYLE, $newsub);
416
    }
417
418
    /**
419
     * Switches the context (see dynamic load).
420
     *
421
     * Private variables are adjusted, and the prepend and append styles are merged with the componentstyle.
422
     * You cannot change the style stack after that (unless you call enter_context again of course).
423
     *
424
     * @param midcom_core_context $context The context to enter
425
     */
426 268
    public function enter_context(midcom_core_context $context)
427
    {
428
        // set new context and topic
429 268
        array_unshift($this->_context, $context); // push into context stack
430
431 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...
432
433
        // Prepare styledir stacks
434 268
        if (!isset($this->_styledirs_prepend[$context->id])) {
435 200
            $this->_styledirs_prepend[$context->id] = [];
436
        }
437 268
        if (!isset($this->_styledirs_append[$context->id])) {
438 195
            $this->_styledirs_append[$context->id] = [];
439
        }
440
441 268
        if ($this->_topic) {
442 268
            $this->initialize_from_topic($context);
443
        }
444 268
        $this->_snippetdir = $this->_get_component_snippetdir();
445
446 268
        $this->_styledirs[$context->id] = array_merge(
447 268
            $this->_styledirs_prepend[$context->id],
448 268
            [$this->_snippetdir],
449 268
            $this->_styledirs_append[$context->id]
450
        );
451 268
    }
452
453
    /**
454
     * Initializes style sources from topic
455
     */
456 268
    private function initialize_from_topic(midcom_core_context $context)
457
    {
458 268
        $_st = 0;
459
        // get user defined style for component
460
        // style inheritance
461
        // should this be cached somehow?
462 268
        if ($style = $this->_topic->style ?: $context->get_inherited_style()) {
463
            if (substr($style, 0, 6) === 'theme:') {
464
                $theme_dir = OPENPSA2_THEME_ROOT . midcom::get()->config->get('theme') . '/style';
465
                $parts = explode('/', str_replace('theme:/', '', $style));
466
467
                foreach ($parts as &$part) {
468
                    $theme_dir .= '/' . $part;
469
                    $part = $theme_dir;
470
                }
471
                foreach (array_reverse(array_filter($parts, 'is_dir')) as $dirname) {
472
                    $this->prepend_styledir($dirname);
473
                }
474
            } else {
475
                $_st = midcom_db_style::id_from_path($style);
476
            }
477
        } else {
478
            // Get style from sitewide per-component defaults.
479 268
            $styleengine_default_styles = midcom::get()->config->get('styleengine_default_styles');
480 268
            if (isset($styleengine_default_styles[$this->_topic->component])) {
481
                $_st = midcom_db_style::id_from_path($styleengine_default_styles[$this->_topic->component]);
482
            }
483
        }
484
485 268
        if ($_st) {
486
            $substyle = $context->get_key(MIDCOM_CONTEXT_SUBSTYLE);
487
488
            if (is_string($substyle)) {
489
                $chain = explode('/', $substyle);
490
                foreach ($chain as $stylename) {
491
                    if ($_subst_id = midcom_db_style::id_from_path($stylename, $_st)) {
492
                        $_st = $_subst_id;
493
                    }
494
                }
495
            }
496
        }
497 268
        $context->set_custom_key(midcom_db_style::class, $_st);
498 268
    }
499
500
    /**
501
     * Switches the context (see dynamic load). Private variables $_context, $_topic
502
     * and $_snippetdir are adjusted.
503
     *
504
     * @todo check documentation
505
     */
506 268
    public function leave_context()
507
    {
508 268
        array_shift($this->_context);
509
510 268
        $previous_context = $this->_context[0] ?? midcom_core_context::get();
511 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...
512
513 268
        $this->_snippetdir = $this->_get_component_snippetdir();
514 268
    }
515
}
516