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, '-'); |
|
|
|
|
92
|
|
|
$this->_View = $view; |
93
|
|
|
$this->_theme = plugin($options['theme']); |
|
|
|
|
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
|
|
|
|
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.