Region::_filterBlock()   D
last analyzed

Complexity

Conditions 9
Paths 11

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 27
c 0
b 0
f 0
nc 11
nop 1
dl 0
loc 44
rs 4.909
1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace Block\View;
13
14
use Block\Model\Entity\Block;
15
use Cake\Collection\Collection;
16
use Cake\Core\Configure;
17
use Cake\I18n\I18n;
18
use Cake\ORM\TableRegistry;
19
use Cake\Utility\Inflector;
20
use CMS\Core\StaticCacheTrait;
21
use CMS\View\View;
22
23
/**
24
 * Region class.
25
 *
26
 * Represents a single region of a theme.
27
 */
28
class Region
29
{
30
31
    use StaticCacheTrait;
32
33
    /**
34
     * machine name of this region. e.g. 'left-sidebar'
35
     *
36
     * @var string
37
     */
38
    protected $_machineName = null;
39
40
    /**
41
     * Collection of blocks for this region.
42
     *
43
     * @var \Cake\Collection\Collection
44
     */
45
    protected $_blocks = null;
46
47
    /**
48
     * Maximum number of blocks this region can holds.
49
     *
50
     * @var null|int
51
     */
52
    protected $_blockLimit = null;
53
54
    /**
55
     * Information about the theme this region belongs to.
56
     *
57
     * @var \CMS\Core\Package\PluginPackage
58
     */
59
    protected $_theme;
60
61
    /**
62
     * View instance.
63
     *
64
     * @var \CMS\View\View
65
     */
66
    protected $_View = null;
67
68
    /**
69
     * Constructor.
70
     *
71
     * ### Valid options are:
72
     *
73
     * - `fixMissing`: When creating a region that is not defined by the theme, it
74
     *    will try to fix it by adding it to theme's regions if this option is set
75
     *    to TRUE. Defaults to NULL which automatically enables when `debug` is
76
     *    enabled. This option will not work when using QuickAppsCMS's core themes.
77
     *    (NOTE: This option will alter theme's `composer.json` file)
78
     *
79
     * - `theme`: Name of the theme this regions belongs to. Defaults to auto-detect.
80
     *
81
     * @param \CMS\View\View $view Instance of View class to use
82
     * @param string $name Machine name of the region. e.g.: `left-sidebar`
83
     * @param array $options Options given as an array
84
     */
85
    public function __construct(View $view, $name, array $options = [])
86
    {
87
        $options += [
88
            'fixMissing' => null,
89
            'theme' => $view->theme(),
90
        ];
91
        $this->_machineName = Inflector::slug($name, '-');
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Utility\Inflector::slug() has been deprecated with message: 3.2.7 Use Text::slug() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
92
        $this->_View = $view;
93
        $this->_theme = plugin($options['theme']);
0 ignored issues
show
Documentation Bug introduced by
It seems like plugin($options['theme']) can also be of type object<Cake\Collection\Collection>. However, the property $_theme is declared as type object<CMS\Core\Package\PluginPackage>. 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...
94
95
        if (isset($this->_theme->composer['extra']['regions'])) {
96
            $validRegions = array_keys($this->_theme->composer['extra']['regions']);
97
            $jsonPath = "{$this->_theme->path}/composer.json";
98
            $options['fixMissing'] = $options['fixMissing'] == null ? Configure::read('debug') : $options['fixMissing'];
99
            if (!in_array($this->_machineName, $validRegions) &&
100
                $options['fixMissing'] &&
101
                is_writable($jsonPath)
102
            ) {
103
                $jsonArray = json_decode(file_get_contents($jsonPath), true);
104
                if (is_array($jsonArray)) {
105
                    $humanName = Inflector::humanize(str_replace('-', '_', $this->_machineName));
106
                    $jsonArray['extra']['regions'][$this->_machineName] = $humanName;
107
                    $encode = json_encode($jsonArray, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
108
                    if ($encode) {
109
                        file_put_contents($jsonPath, $encode);
110
                    }
111
                }
112
            }
113
        }
114
    }
115
116
    /**
117
     * Returns the name of this region.
118
     *
119
     * @return string
120
     */
121
    public function name()
122
    {
123
        return $this->_machineName;
124
    }
125
126
    /**
127
     * Returns information of the theme this regions belongs to.
128
     *
129
     * ### Usage:
130
     *
131
     * ```php
132
     * $theme = $this->region('left-sidebar')->theme();
133
     * ```
134
     *
135
     * @return CMS\Core\Package\PluginPackage
136
     */
137
    public function theme()
138
    {
139
        return $this->_theme;
140
    }
141
142
    /**
143
     * Gets or sets the block collection of this region.
144
     *
145
     * When passing a collection of blocks as first argument, all blocks in the
146
     * collection will be homogenized, see homogenize() for details.
147
     *
148
     * @param \Cake\Collection\Collection $blocks Blocks collection if you want to
149
     *  overwrite current collection, leave empty to return current collection
150
     * @return \Cake\Collection\Collection
151
     * @see \Block\View\Region::homogenize()
152
     */
153
    public function blocks(Collection $blocks = null)
154
    {
155
        if ($blocks) {
156
            $this->_blocks = $blocks;
157
            $this->homogenize();
158
        } elseif ($this->_blocks === null) {
159
            $this->_prepareBlocks();
160
        }
161
162
        return $this->_blocks;
163
    }
164
165
    /**
166
     * Counts the number of blocks within this region.
167
     *
168
     * @return int
169
     */
170
    public function count()
171
    {
172
        return count($this->blocks()->toArray());
173
    }
174
175
    /**
176
     * Limits the number of blocks in this region.
177
     *
178
     * Null means unlimited number.
179
     *
180
     * @param null|int $number Defaults to null
181
     * @return \Block\View\Region
182
     */
183
    public function blockLimit($number = null)
184
    {
185
        $number = $number !== null ? intval($number) : $number;
186
        $this->_blockLimit = $number;
187
188
        return $this;
189
    }
190
191
    /**
192
     * Merge blocks from another region.
193
     *
194
     * You can not merge regions with the same machine-name, new blocks are appended
195
     * to this region.
196
     *
197
     * @param \Block\View\Region $region Region to merge with
198
     * @param bool $homogenize Set to true to make sure all blocks in the
199
     *  collection are marked as they belongs to this region
200
     * @return \Block\View\Region This region with $region's blocks appended
201
     */
202
    public function merge(Region $region, $homogenize = true)
203
    {
204
        if ($region->name() !== $this->name()) {
205
            $blocks1 = $this->blocks();
206
            $blocks2 = $region->blocks();
207
            $combined = $blocks1->append($blocks2)->toArray(false);
208
            $this->blocks(collection($combined));
209
210
            if ($homogenize) {
211
                $this->homogenize();
212
            }
213
        }
214
215
        return $this;
216
    }
217
218
    /**
219
     * Makes sure that every block in this region is actually marked as it belongs
220
     * to this region.
221
     *
222
     * Used when merging blocks from another region.
223
     *
224
     * @return \Block\View\Region This region with homogenized blocks
225
     */
226
    public function homogenize()
227
    {
228
        $this->_blocks = $this->blocks()->map(function ($block) {
229
            $block->region->set('region', $this->_machineName);
230
231
            return $block;
232
        });
233
234
        return $this;
235
    }
236
237
    /**
238
     * Render all the blocks within this region.
239
     *
240
     * @return string
241
     */
242
    public function render()
243
    {
244
        $html = '';
245
        $i = 0;
246
        foreach ($this->blocks() as $block) {
247
            if ($this->_blockLimit !== null && $i === $this->_blockLimit) {
248
                break;
249
            }
250
            $html .= $block->render($this->_View);
251
            $i++;
252
        }
253
254
        return $html;
255
    }
256
257
    /**
258
     * Fetches all block entities that could be rendered within this region.
259
     *
260
     * @return void
261
     */
262
    protected function _prepareBlocks()
263
    {
264
        $cacheKey = "{$this->_View->theme}_{$this->_machineName}";
265
        $blocks = TableRegistry::get('Block.Blocks')
266
            ->find('all')
267
            ->cache($cacheKey, 'blocks')
268
            ->contain(['Roles', 'BlockRegions'])
269
            ->matching('BlockRegions', function ($q) {
270
                return $q->where([
271
                    'BlockRegions.theme' => $this->_View->theme(),
272
                    'BlockRegions.region' => $this->_machineName,
273
                ]);
274
            })
275
            ->where(['Blocks.status' => 1])
276
            ->order(['BlockRegions.ordering' => 'ASC']);
277
278
        $blocks->sortBy(function ($block) {
279
            return $block->region->ordering;
280
        }, SORT_ASC);
281
282
        // remove blocks that cannot be rendered based on current request.
283
        $blocks = $blocks->filter(function ($block) {
284
            return $this->_filterBlock($block) && $block->renderable();
285
        });
286
287
        $this->blocks($blocks);
288
    }
289
290
    /**
291
     * Checks if the given block can be rendered.
292
     *
293
     * @param \Block\Model\Entity\Block $block Block entity
294
     * @return bool True if can be rendered
295
     */
296
    protected function _filterBlock(Block $block)
297
    {
298
        $cacheKey = "allowed_{$block->id}";
299
        $cache = static::cache($cacheKey);
300
301
        if ($cache !== null) {
302
            return $cache;
303
        }
304
305
        if (!empty($block->locale) &&
306
            !in_array(I18n::locale(), (array)$block->locale)
307
        ) {
308
            return static::cache($cacheKey, false);
309
        }
310
311
        if (!$block->isAccessibleByUser()) {
312
            return static::cache($cacheKey, false);
313
        }
314
315
        $allowed = false;
316
        switch ($block->visibility) {
317
            case 'except':
318
                // Show on all pages except listed pages
319
                $allowed = !$this->_urlMatch($block->pages);
320
                break;
321
            case 'only':
322
                // Show only on listed pages
323
                $allowed = $this->_urlMatch($block->pages);
324
                break;
325
            case 'php':
326
                // Use custom PHP code to determine visibility
327
                $allowed = php_eval($block->pages, [
328
                    'view' => &$this->_View,
329
                    'block' => &$block
330
                ]) === true;
331
                break;
332
        }
333
334
        if (!$allowed) {
335
            return static::cache($cacheKey, false);
336
        }
337
338
        return static::cache($cacheKey, true);
339
    }
340
341
    /**
342
     * Check if a current URL matches any pattern in a set of patterns.
343
     *
344
     * @param string $patterns String containing a set of patterns separated by
345
     *  \n, \r or \r\n
346
     * @return bool TRUE if the path matches a pattern, FALSE otherwise
347
     */
348
    protected function _urlMatch($patterns)
349
    {
350
        if (empty($patterns)) {
351
            return false;
352
        }
353
354
        $url = urldecode($this->_View->request->url);
355
        $path = str_starts_with($url, '/') ? str_replace_once('/', '', $url) : $url;
356
357
        if (option('url_locale_prefix')) {
358
            $patterns = explode("\n", $patterns);
359
            $locales = array_keys(quickapps('languages'));
360
            $localesPattern = '(' . implode('|', array_map('preg_quote', $locales)) . ')';
361
362
            foreach ($patterns as &$p) {
363
                if (!preg_match("/^{$localesPattern}\//", $p)) {
364
                    $p = I18n::locale() . '/' . $p;
365
                    $p = str_replace('//', '/', $p);
366
                }
367
            }
368
369
            $patterns = implode("\n", $patterns);
370
        }
371
372
        // Convert path settings to a regular expression.
373
        // Therefore replace newlines with a logical or, /* with asterisks and  "/" with the front page.
374
        $toReplace = [
375
            '/(\r\n?|\n)/', // newlines
376
            '/\\\\\*/', // asterisks
377
            '/(^|\|)\/($|\|)/' // front '/'
378
        ];
379
380
        $replacements = [
381
            '|',
382
            '.*',
383
            '\1' . preg_quote($this->_View->Url->build('/'), '/') . '\2'
384
        ];
385
386
        $patternsQuoted = preg_quote($patterns, '/');
387
        $patterns = '/^(' . preg_replace($toReplace, $replacements, $patternsQuoted) . ')$/';
388
389
        return (bool)preg_match($patterns, $path);
390
    }
391
392
    /**
393
     * Magic method for rendering this region.
394
     *
395
     * ```php
396
     * echo $this->region('left-sidebar');
397
     * ```
398
     *
399
     * @return string
400
     */
401
    public function __toString()
402
    {
403
        return $this->render();
404
    }
405
406
    /**
407
     * Returns an array that can be used to describe the internal state of
408
     * this object.
409
     *
410
     * @return array
411
     */
412
    public function __debugInfo()
413
    {
414
        return [
415
            '_machineName' => $this->_machineName,
416
            '_blocks' => $this->blocks()->toArray(),
417
            '_blockLimit' => $this->_blockLimit,
418
            '_theme' => $this->_theme,
419
            '_View' => '(object) \CMS\View\View',
420
        ];
421
    }
422
}
423