1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * @package toolkit |
||||
5 | */ |
||||
6 | /** |
||||
7 | * The AdministrationPage class represents a Symphony backend page. |
||||
8 | * It extends the HTMLPage class and unlike the Frontend, is generated |
||||
9 | * using a number XMLElement objects. Instances of this class override |
||||
10 | * the view, switchboard and action functions to construct the page. These |
||||
11 | * functions act as pseudo MVC, with the switchboard being controller, |
||||
12 | * and the view/action being the view. |
||||
13 | */ |
||||
14 | |||||
15 | class AdministrationPage extends HTMLPage |
||||
16 | { |
||||
17 | /** |
||||
18 | * An array of `Alert` objects used to display page level |
||||
19 | * messages to Symphony backend users one by one. Prior to Symphony 2.3 |
||||
20 | * this variable only held a single `Alert` object. |
||||
21 | * @var array |
||||
22 | */ |
||||
23 | public $Alert = array(); |
||||
24 | |||||
25 | /** |
||||
26 | * As the name suggests, a `<div>` that holds the following `$Header`, |
||||
27 | * `$Contents` and `$Footer`. |
||||
28 | * @var XMLElement |
||||
29 | */ |
||||
30 | public $Wrapper = null; |
||||
31 | |||||
32 | /** |
||||
33 | * A `<div>` that contains the header of a Symphony backend page, which |
||||
34 | * typically contains the Site title and the navigation. |
||||
35 | * @var XMLElement |
||||
36 | */ |
||||
37 | public $Header = null; |
||||
38 | |||||
39 | /** |
||||
40 | * A `<div>` that contains the breadcrumbs, the page title and some contextual |
||||
41 | * actions (e.g. "Create new"). |
||||
42 | * @since Symphony 2.3 |
||||
43 | * @var XMLElement |
||||
44 | */ |
||||
45 | public $Context = null; |
||||
46 | |||||
47 | /** |
||||
48 | * An object that stores the markup for the breadcrumbs and is only used |
||||
49 | * internally. |
||||
50 | * @since Symphony 2.3 |
||||
51 | * @var XMLElement |
||||
52 | */ |
||||
53 | public $Breadcrumbs = null; |
||||
54 | |||||
55 | /** |
||||
56 | * An array of Drawer widgets for the current page |
||||
57 | * @since Symphony 2.3 |
||||
58 | * @var array |
||||
59 | */ |
||||
60 | public $Drawer = array(); |
||||
61 | |||||
62 | /** |
||||
63 | * A `<div>` that contains the content of a Symphony backend page. |
||||
64 | * @var XMLElement |
||||
65 | */ |
||||
66 | public $Contents = null; |
||||
67 | |||||
68 | /** |
||||
69 | * An associative array of the navigation where the key is the group |
||||
70 | * index, and the value is an associative array of 'name', 'index' and |
||||
71 | * 'children'. Name is the name of the this group, index is the same as |
||||
72 | * the key and children is an associative array of navigation items containing |
||||
73 | * the keys 'link', 'name' and 'visible'. In Symphony, all navigation items |
||||
74 | * are contained within a group, and the group has no 'default' link, therefore |
||||
75 | * it is up to the children to provide the link to pages. This link should be |
||||
76 | * relative to the Symphony path, although it is possible to provide an |
||||
77 | * absolute link by providing a key, 'relative' with the value false. |
||||
78 | * @var array |
||||
79 | */ |
||||
80 | public $_navigation = array(); |
||||
81 | |||||
82 | /** |
||||
83 | * An associative array describing this pages context. This |
||||
84 | * can include the section handle, the current entry_id, the page |
||||
85 | * name and any flags such as 'saved' or 'created'. This variable |
||||
86 | * often provided in delegates so extensions can manipulate based |
||||
87 | * off the current context or add new keys. |
||||
88 | * @var array |
||||
89 | */ |
||||
90 | public $_context = null; |
||||
91 | |||||
92 | /** |
||||
93 | * The class attribute of the `<body>` element for this page. Defaults |
||||
94 | * to an empty string |
||||
95 | * @var string |
||||
96 | */ |
||||
97 | private $_body_class = ''; |
||||
98 | |||||
99 | /** |
||||
100 | * Constructor calls the parent constructor to set up |
||||
101 | * the basic HTML, Head and Body `XMLElement`'s. This function |
||||
102 | * also sets the `XMLElement` element style to be HTML, instead of XML |
||||
103 | */ |
||||
104 | public function __construct() |
||||
105 | { |
||||
106 | parent::__construct(); |
||||
107 | |||||
108 | $this->Html->setElementStyle('html'); |
||||
109 | } |
||||
110 | |||||
111 | /** |
||||
112 | * Specifies the type of page that being created. This is used to |
||||
113 | * trigger various styling hooks. If your page is mainly a form, |
||||
114 | * pass 'form' as the parameter, if it's displaying a single entry, |
||||
115 | * pass 'single'. If any other parameter is passed, the 'index' |
||||
116 | * styling will be applied. |
||||
117 | * |
||||
118 | * @param string $type |
||||
119 | * Accepts 'form' or 'single', any other `$type` will trigger 'index' |
||||
120 | * styling. |
||||
121 | */ |
||||
122 | public function setPageType($type = 'form') |
||||
0 ignored issues
–
show
Coding Style
introduced
by
![]() |
|||||
123 | { |
||||
124 | $this->setBodyClass($type == 'form' || $type == 'single' ? 'single' : 'index'); |
||||
0 ignored issues
–
show
|
|||||
125 | } |
||||
126 | |||||
127 | /** |
||||
128 | * Setter function to set the class attribute on the `<body>` element. |
||||
129 | * This function will respect any previous classes that have been added |
||||
130 | * to this `<body>` |
||||
131 | * |
||||
132 | * @param string $class |
||||
133 | * The string of the classname, multiple classes can be specified by |
||||
134 | * uses a space separator |
||||
135 | */ |
||||
136 | public function setBodyClass($class) |
||||
137 | { |
||||
138 | // Prevents duplicate "index" classes |
||||
139 | if (!isset($this->_context['page']) || $this->_context['page'] !== 'index' || $class !== 'index') { |
||||
140 | $this->_body_class .= $class; |
||||
141 | } |
||||
142 | } |
||||
143 | |||||
144 | /** |
||||
145 | * Accessor for `$this->_context` which includes contextual information |
||||
146 | * about the current page such as the class, file location or page root. |
||||
147 | * This information varies depending on if the page is provided by an |
||||
148 | * extension, is for the publish area, is the login page or any other page |
||||
149 | * |
||||
150 | * @since Symphony 2.3 |
||||
151 | * @return array |
||||
152 | */ |
||||
153 | public function getContext() |
||||
154 | { |
||||
155 | return $this->_context; |
||||
156 | } |
||||
157 | |||||
158 | /** |
||||
159 | * Given a `$message` and an optional `$type`, this function will |
||||
160 | * add an Alert instance into this page's `$this->Alert` property. |
||||
161 | * Since Symphony 2.3, there may be more than one `Alert` per page. |
||||
162 | * Unless the Alert is an Error, it is required the `$message` be |
||||
163 | * passed to this function. |
||||
164 | * |
||||
165 | * @param string $message |
||||
166 | * The message to display to users |
||||
167 | * @param string $type |
||||
168 | * An Alert constant, being `Alert::NOTICE`, `Alert::ERROR` or |
||||
169 | * `Alert::SUCCESS`. The differing types will show the error |
||||
170 | * in a different style in the backend. If omitted, this defaults |
||||
171 | * to `Alert::NOTICE`. |
||||
172 | * @throws Exception |
||||
173 | */ |
||||
174 | public function pageAlert($message = null, $type = Alert::NOTICE) |
||||
0 ignored issues
–
show
|
|||||
175 | { |
||||
176 | if (is_null($message) && $type == Alert::ERROR) { |
||||
177 | $message = __('There was a problem rendering this page. Please check the activity log for more details.'); |
||||
178 | } else { |
||||
179 | $message = __($message); |
||||
180 | } |
||||
181 | |||||
182 | if (strlen(trim($message)) == 0) { |
||||
183 | throw new Exception(__('A message must be supplied unless the alert is of type Alert::ERROR')); |
||||
184 | } |
||||
185 | |||||
186 | $this->Alert[] = new Alert($message, $type); |
||||
187 | } |
||||
188 | |||||
189 | /** |
||||
190 | * Appends the heading of this Symphony page to the Context element. |
||||
191 | * Action buttons can be provided (e.g. "Create new") as second parameter. |
||||
192 | * |
||||
193 | * @since Symphony 2.3 |
||||
194 | * @param string $value |
||||
195 | * The heading text |
||||
196 | * @param array|XMLElement|string $actions |
||||
197 | * Some contextual actions to append to the heading, they can be provided as |
||||
198 | * an array of XMLElements or strings. Traditionally Symphony uses this to append |
||||
199 | * a "Create new" link to the Context div. |
||||
200 | */ |
||||
201 | public function appendSubheading($value, $actions = null) |
||||
0 ignored issues
–
show
|
|||||
202 | { |
||||
203 | if (!is_array($actions) && $actions) { // Backward compatibility |
||||
204 | $actions = array($actions); |
||||
205 | } |
||||
206 | |||||
207 | if (!empty($actions)) { |
||||
208 | foreach ($actions as $a) { |
||||
209 | $this->insertAction($a); |
||||
210 | } |
||||
211 | } |
||||
212 | |||||
213 | $this->Breadcrumbs->appendChild(new XMLElement('h2', $value, array('role' => 'heading', 'id' => 'symphony-subheading'))); |
||||
214 | } |
||||
215 | |||||
216 | /** |
||||
217 | * This function allows a user to insert an Action button to the page. |
||||
218 | * It accepts an `XMLElement` (which should be of the `Anchor` type), |
||||
219 | * an optional parameter `$prepend`, which when `true` will add this |
||||
220 | * action before any existing actions. |
||||
221 | * |
||||
222 | * @since Symphony 2.3 |
||||
223 | * @see core.Widget#Anchor |
||||
224 | * @param XMLElement $action |
||||
225 | * An Anchor element to add to the top of the page. |
||||
226 | * @param boolean $append |
||||
227 | * If true, this will add the `$action` after existing actions, otherwise |
||||
228 | * it will be added before existing actions. By default this is `true`, |
||||
229 | * which will add the `$action` after current actions. |
||||
230 | */ |
||||
231 | public function insertAction(XMLElement $action, $append = true) |
||||
0 ignored issues
–
show
|
|||||
232 | { |
||||
233 | $actions = $this->Context->getChildrenByName('ul'); |
||||
234 | |||||
235 | // Actions haven't be added yet, create the element |
||||
236 | if (empty($actions)) { |
||||
237 | $ul = new XMLElement('ul', null, array('class' => 'actions')); |
||||
238 | $this->Context->appendChild($ul); |
||||
239 | } else { |
||||
240 | $ul = current($actions); |
||||
241 | $this->Context->replaceChildAt(1, $ul); |
||||
242 | } |
||||
243 | |||||
244 | $li = new XMLElement('li', $action); |
||||
245 | |||||
246 | if ($append) { |
||||
247 | $ul->prependChild($li); |
||||
248 | } else { |
||||
249 | $ul->appendChild($li); |
||||
250 | } |
||||
251 | } |
||||
252 | |||||
253 | /** |
||||
254 | * Allows developers to specify a list of nav items that build the |
||||
255 | * path to the current page or, in jargon, "breadcrumbs". |
||||
256 | * |
||||
257 | * @since Symphony 2.3 |
||||
258 | * @param array $values |
||||
259 | * An array of `XMLElement`'s or strings that compose the path. If breadcrumbs |
||||
260 | * already exist, any new item will be appended to the rightmost part of the |
||||
261 | * path. |
||||
262 | */ |
||||
263 | public function insertBreadcrumbs(array $values) |
||||
264 | { |
||||
265 | if (empty($values)) { |
||||
266 | return; |
||||
267 | } |
||||
268 | |||||
269 | if ($this->Breadcrumbs instanceof XMLElement && count($this->Breadcrumbs->getChildrenByName('nav')) === 1) { |
||||
270 | $nav = $this->Breadcrumbs->getChildrenByName('nav'); |
||||
271 | $nav = $nav[0]; |
||||
272 | |||||
273 | $p = $nav->getChild(0); |
||||
274 | } else { |
||||
275 | $p = new XMLElement('p'); |
||||
276 | $nav = new XMLElement('nav'); |
||||
277 | $nav->appendChild($p); |
||||
278 | |||||
279 | $this->Breadcrumbs->prependChild($nav); |
||||
280 | } |
||||
281 | |||||
282 | foreach ($values as $v) { |
||||
283 | $p->appendChild($v); |
||||
284 | $p->appendChild(new XMLElement('span', '›', array('class' => 'sep'))); |
||||
285 | } |
||||
286 | } |
||||
287 | |||||
288 | /** |
||||
289 | * Allows a Drawer element to added to the backend page in one of three |
||||
290 | * positions, `horizontal`, `vertical-left` or `vertical-right`. The button |
||||
291 | * to trigger the visibility of the drawer will be added after existing |
||||
292 | * actions by default. |
||||
293 | * |
||||
294 | * @since Symphony 2.3 |
||||
295 | * @see core.Widget#Drawer |
||||
296 | * @param XMLElement $drawer |
||||
297 | * An XMLElement representing the drawer, use `Widget::Drawer` to construct |
||||
298 | * @param string $position |
||||
299 | * Where `$position` can be `horizontal`, `vertical-left` or |
||||
300 | * `vertical-right`. Defaults to `horizontal`. |
||||
301 | * @param string $button |
||||
302 | * If not passed, a button to open/close the drawer will not be added |
||||
303 | * to the interface. Accepts 'prepend' or 'append' values, which will |
||||
304 | * add the button before or after existing buttons. Defaults to `prepend`. |
||||
305 | * If any other value is passed, no button will be added. |
||||
306 | * @throws InvalidArgumentException |
||||
307 | */ |
||||
308 | public function insertDrawer(XMLElement $drawer, $position = 'horizontal', $button = 'append') |
||||
0 ignored issues
–
show
|
|||||
309 | { |
||||
310 | $drawer->addClass($position); |
||||
311 | $drawer->setAttribute('data-position', $position); |
||||
312 | $drawer->setAttribute('role', 'complementary'); |
||||
313 | $this->Drawer[$position][] = $drawer; |
||||
314 | |||||
315 | if (in_array($button, array('prepend', 'append'))) { |
||||
316 | $this->insertAction( |
||||
317 | Widget::Anchor( |
||||
318 | $drawer->getAttribute('data-label'), |
||||
319 | '#' . $drawer->getAttribute('id'), |
||||
320 | null, |
||||
321 | 'button drawer ' . $position |
||||
322 | ), |
||||
323 | ($button === 'append' ? true : false) |
||||
0 ignored issues
–
show
|
|||||
324 | ); |
||||
325 | } |
||||
326 | } |
||||
327 | |||||
328 | /** |
||||
329 | * This function initialises a lot of the basic elements that make up a Symphony |
||||
330 | * backend page such as the default stylesheets and scripts, the navigation and |
||||
331 | * the footer. Any alerts are also appended by this function. `view()` is called to |
||||
332 | * build the actual content of the page. The `InitialiseAdminPageHead` delegate |
||||
333 | * allows extensions to add elements to the `<head>`. The `CanAccessPage` delegate |
||||
334 | * allows extensions to restrict access to pages. |
||||
335 | * |
||||
336 | * @see view() |
||||
337 | * @uses InitialiseAdminPageHead |
||||
338 | * @uses CanAccessPage |
||||
339 | * @param array $context |
||||
340 | * An associative array describing this pages context. This |
||||
341 | * can include the section handle, the current entry_id, the page |
||||
342 | * name and any flags such as 'saved' or 'created'. This list is not exhaustive |
||||
343 | * and extensions can add their own keys to the array. |
||||
344 | * @throws InvalidArgumentException |
||||
345 | * @throws SymphonyErrorPage |
||||
346 | */ |
||||
347 | public function build(array $context = array()) |
||||
0 ignored issues
–
show
|
|||||
348 | { |
||||
349 | $this->_context = $context; |
||||
350 | |||||
351 | if (!$this->canAccessPage()) { |
||||
352 | Administration::instance()->throwCustomError( |
||||
353 | __('You are not authorised to access this page.'), |
||||
354 | __('Access Denied'), |
||||
355 | Page::HTTP_STATUS_UNAUTHORIZED |
||||
356 | ); |
||||
357 | } |
||||
358 | |||||
359 | $this->Html->setDTD('<!DOCTYPE html>'); |
||||
360 | $this->Html->setAttribute('lang', Lang::get()); |
||||
361 | $this->addElementToHead(new XMLElement('meta', null, array('charset' => 'UTF-8')), 0); |
||||
362 | $this->addElementToHead(new XMLElement('meta', null, array('http-equiv' => 'X-UA-Compatible', 'content' => 'IE=edge,chrome=1')), 1); |
||||
363 | $this->addElementToHead(new XMLElement('meta', null, array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1')), 2); |
||||
364 | |||||
365 | // Add styles |
||||
366 | $this->addStylesheetToHead(ASSETS_URL . '/css/symphony.min.css', 'screen', 2, false); |
||||
0 ignored issues
–
show
|
|||||
367 | |||||
368 | // Calculate timezone offset from UTC |
||||
369 | $timezone = new DateTimeZone(DateTimeObj::getSetting('timezone')); |
||||
0 ignored issues
–
show
It seems like
DateTimeObj::getSetting('timezone') can also be of type array ; however, parameter $timezone of DateTimeZone::__construct() does only seem to accept string , 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
![]() |
|||||
370 | $datetime = new DateTime('now', $timezone); |
||||
371 | $timezoneOffset = intval($timezone->getOffset($datetime)) / 60; |
||||
372 | |||||
373 | // Add scripts |
||||
374 | $environment = array( |
||||
375 | |||||
376 | 'root' => URL, |
||||
0 ignored issues
–
show
|
|||||
377 | 'symphony' => SYMPHONY_URL, |
||||
0 ignored issues
–
show
|
|||||
378 | 'path' => '/' . Symphony::Configuration()->get('admin-path', 'symphony'), |
||||
379 | 'route' => getCurrentPage(), |
||||
380 | 'version' => Symphony::Configuration()->get('version', 'symphony'), |
||||
381 | 'lang' => Lang::get(), |
||||
382 | 'user' => array( |
||||
383 | |||||
384 | 'fullname' => Symphony::Author()->getFullName(), |
||||
385 | 'name' => Symphony::Author()->get('first_name'), |
||||
386 | 'type' => Symphony::Author()->get('user_type'), |
||||
387 | 'id' => Symphony::Author()->get('id') |
||||
388 | ), |
||||
389 | 'datetime' => array( |
||||
390 | |||||
391 | 'formats' => DateTimeObj::getDateFormatMappings(), |
||||
392 | 'timezone-offset' => $timezoneOffset |
||||
393 | ), |
||||
394 | 'env' => array_merge( |
||||
395 | |||||
396 | array('page-namespace' => Symphony::getPageNamespace()), |
||||
397 | $this->_context |
||||
398 | ) |
||||
399 | ); |
||||
400 | |||||
401 | $envJsonOptions = 0; |
||||
402 | // Those are PHP 5.4+ |
||||
403 | if (defined('JSON_UNESCAPED_SLASHES') && defined('JSON_UNESCAPED_UNICODE')) { |
||||
404 | $envJsonOptions = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; |
||||
405 | } |
||||
406 | |||||
407 | $this->addElementToHead( |
||||
408 | new XMLElement('script', json_encode($environment, $envJsonOptions), array( |
||||
409 | 'type' => 'application/json', |
||||
410 | 'id' => 'environment' |
||||
411 | )), |
||||
412 | 4 |
||||
413 | ); |
||||
414 | |||||
415 | $this->addScriptToHead(ASSETS_URL . '/js/symphony.min.js', 6, false); |
||||
416 | |||||
417 | // Initialise page containers |
||||
418 | $this->Wrapper = new XMLElement('div', null, array('id' => 'wrapper')); |
||||
419 | $this->Header = new XMLElement('header', null, array('id' => 'header')); |
||||
420 | $this->Context = new XMLElement('div', null, array('id' => 'context')); |
||||
421 | $this->Breadcrumbs = new XMLElement('div', null, array('id' => 'breadcrumbs')); |
||||
422 | $this->Contents = new XMLElement('div', null, array('id' => 'contents', 'role' => 'main')); |
||||
423 | $this->Form = Widget::Form(Administration::instance()->getCurrentPageURL(), 'post', null, null, array('role' => 'form')); |
||||
424 | |||||
425 | /** |
||||
426 | * Allows developers to insert items into the page HEAD. Use |
||||
427 | * `Administration::instance()->Page` for access to the page object. |
||||
428 | * |
||||
429 | * @since In Symphony 2.3.2 this delegate was renamed from |
||||
430 | * `InitaliseAdminPageHead` to the correct spelling of |
||||
431 | * `InitialiseAdminPageHead`. The old delegate is supported |
||||
432 | * until Symphony 3.0 |
||||
433 | * |
||||
434 | * @delegate InitialiseAdminPageHead |
||||
435 | * @param string $context |
||||
436 | * '/backend/' |
||||
437 | */ |
||||
438 | Symphony::ExtensionManager()->notifyMembers('InitialiseAdminPageHead', '/backend/'); |
||||
439 | Symphony::ExtensionManager()->notifyMembers('InitaliseAdminPageHead', '/backend/'); |
||||
440 | |||||
441 | $this->addHeaderToPage('Content-Type', 'text/html; charset=UTF-8'); |
||||
442 | $this->addHeaderToPage('Cache-Control', 'no-cache, must-revalidate, max-age=0'); |
||||
443 | $this->addHeaderToPage('Expires', 'Mon, 12 Dec 1982 06:14:00 GMT'); |
||||
444 | $this->addHeaderToPage('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT'); |
||||
445 | $this->addHeaderToPage('Pragma', 'no-cache'); |
||||
446 | |||||
447 | // If not set by another extension, lock down the backend |
||||
448 | if (!array_key_exists('x-frame-options', $this->headers())) { |
||||
449 | $this->addHeaderToPage('X-Frame-Options', 'SAMEORIGIN'); |
||||
450 | } |
||||
451 | |||||
452 | if (!array_key_exists('x-content-type-options', $this->headers())) { |
||||
453 | $this->addHeaderToPage('X-Content-Type-Options', 'nosniff'); |
||||
454 | } |
||||
455 | |||||
456 | if (!array_key_exists('x-xss-protection', $this->headers())) { |
||||
457 | $this->addHeaderToPage('X-XSS-Protection', '1; mode=block'); |
||||
458 | } |
||||
459 | |||||
460 | if (!array_key_exists('referrer-policy', $this->headers())) { |
||||
461 | $this->addHeaderToPage('Referrer-Policy', 'same-origin'); |
||||
462 | } |
||||
463 | |||||
464 | if (isset($_REQUEST['action'])) { |
||||
465 | $this->action(); |
||||
466 | Symphony::Profiler()->sample('Page action run', PROFILE_LAP); |
||||
0 ignored issues
–
show
|
|||||
467 | } |
||||
468 | |||||
469 | $h1 = new XMLElement('h1'); |
||||
470 | $h1->appendChild(Widget::Anchor(Symphony::Configuration()->get('sitename', 'general'), rtrim(URL, '/') . '/')); |
||||
0 ignored issues
–
show
It seems like
Symphony::Configuration(...('sitename', 'general') can also be of type array ; however, parameter $value of Widget::Anchor() does only seem to accept string , 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
![]() |
|||||
471 | $this->Header->appendChild($h1); |
||||
472 | |||||
473 | $this->appendUserLinks(); |
||||
474 | $this->appendNavigation(); |
||||
475 | |||||
476 | // Add Breadcrumbs |
||||
477 | $this->Context->prependChild($this->Breadcrumbs); |
||||
478 | $this->Contents->appendChild($this->Form); |
||||
479 | |||||
480 | // Validate date time config |
||||
481 | $dateFormat = defined('__SYM_DATE_FORMAT__') ? __SYM_DATE_FORMAT__ : null; |
||||
0 ignored issues
–
show
|
|||||
482 | if (empty($dateFormat)) { |
||||
483 | $this->pageAlert( |
||||
484 | __('Your <code>%s</code> file does not define a date format', array(basename(CONFIG))), |
||||
0 ignored issues
–
show
|
|||||
485 | Alert::NOTICE |
||||
486 | ); |
||||
487 | } |
||||
0 ignored issues
–
show
|
|||||
488 | $timeFormat = defined('__SYM_TIME_FORMAT__') ? __SYM_TIME_FORMAT__ : null; |
||||
0 ignored issues
–
show
|
|||||
489 | if (empty($timeFormat)) { |
||||
490 | $this->pageAlert( |
||||
491 | __('Your <code>%s</code> file does not define a time format.', array(basename(CONFIG))), |
||||
492 | Alert::NOTICE |
||||
493 | ); |
||||
494 | } |
||||
495 | |||||
496 | $this->view(); |
||||
497 | |||||
498 | $this->appendAlert(); |
||||
499 | |||||
500 | Symphony::Profiler()->sample('Page content created', PROFILE_LAP); |
||||
501 | } |
||||
502 | |||||
503 | /** |
||||
504 | * Checks the current Symphony Author can access the current page. |
||||
505 | * This check uses the `ASSETS . /xml/navigation.xml` file to determine |
||||
506 | * if the current page (or the current page namespace) can be viewed |
||||
507 | * by the currently logged in Author. |
||||
508 | * |
||||
509 | * @since Symphony 2.7.0 |
||||
510 | * It fires a delegate, CanAccessPage, to allow extensions to restrict access |
||||
511 | * to the current page |
||||
512 | * |
||||
513 | * @uses CanAccessPage |
||||
514 | * |
||||
515 | * @link http://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml |
||||
516 | * @return boolean |
||||
517 | * true if the Author can access the current page, false otherwise |
||||
518 | */ |
||||
519 | public function canAccessPage() |
||||
520 | { |
||||
521 | $nav = $this->getNavigationArray(); |
||||
522 | $page = '/' . trim(getCurrentPage(), '/') . '/'; |
||||
523 | |||||
524 | $page_limit = 'author'; |
||||
525 | |||||
526 | foreach ($nav as $item) { |
||||
527 | if ( |
||||
0 ignored issues
–
show
|
|||||
528 | // If page directly matches one of the children |
||||
529 | General::in_array_multi($page, $item['children']) |
||||
530 | // If the page namespace matches one of the children (this will usually drop query |
||||
531 | // string parameters such as /edit/1/) |
||||
532 | || General::in_array_multi(Symphony::getPageNamespace() . '/', $item['children']) |
||||
533 | ) { |
||||
534 | if (is_array($item['children'])) { |
||||
535 | foreach ($item['children'] as $c) { |
||||
536 | if ($c['link'] === $page && isset($c['limit'])) { |
||||
537 | $page_limit = $c['limit']; |
||||
538 | // TODO: break out of the loop here in Symphony 3.0.0 |
||||
539 | } |
||||
540 | } |
||||
541 | } |
||||
542 | |||||
543 | if (isset($item['limit']) && $page_limit !== 'primary') { |
||||
544 | if ($page_limit === 'author' && $item['limit'] === 'developer') { |
||||
545 | $page_limit = 'developer'; |
||||
546 | } |
||||
547 | } |
||||
548 | } elseif (isset($item['link']) && $page === $item['link'] && isset($item['limit'])) { |
||||
549 | $page_limit = $item['limit']; |
||||
550 | } |
||||
551 | } |
||||
552 | |||||
553 | $hasAccess = $this->doesAuthorHaveAccess($page_limit); |
||||
554 | |||||
555 | if ($hasAccess) { |
||||
556 | $page_context = $this->getContext(); |
||||
557 | $section_handle = !isset($page_context['section_handle']) ? null : $page_context['section_handle']; |
||||
558 | /** |
||||
559 | * Immediately after the core access rules allowed access to this page |
||||
560 | * (i.e. not called if the core rules denied it). |
||||
561 | * Extension developers must only further restrict access to it. |
||||
562 | * Extension developers must also take care of checking the current value |
||||
563 | * of the allowed parameter in order to prevent conflicts with other extensions. |
||||
564 | * `$context['allowed'] = $context['allowed'] && customLogic();` |
||||
565 | * |
||||
566 | * @delegate CanAccessPage |
||||
567 | * @since Symphony 2.7.0 |
||||
568 | * @see doesAuthorHaveAccess() |
||||
569 | * @param string $context |
||||
570 | * '/backend/' |
||||
571 | * @param bool $allowed |
||||
572 | * A flag to further restrict access to the page, passed by reference |
||||
573 | * @param string $page_limit |
||||
574 | * The computed page limit for the current page |
||||
575 | * @param string $page_url |
||||
576 | * The computed page url for the current page |
||||
577 | * @param int $section.id |
||||
578 | * The id of the section for this url |
||||
579 | * @param string $section.handle |
||||
580 | * The handle of the section for this url |
||||
581 | */ |
||||
582 | Symphony::ExtensionManager()->notifyMembers('CanAccessPage', '/backend/', array( |
||||
583 | 'allowed' => &$hasAccess, |
||||
584 | 'page_limit' => $page_limit, |
||||
585 | 'page_url' => $page, |
||||
586 | 'section' => array( |
||||
587 | 'id' => !$section_handle ? 0 : SectionManager::fetchIDFromHandle($section_handle), |
||||
0 ignored issues
–
show
|
|||||
588 | 'handle' => $section_handle |
||||
589 | ), |
||||
590 | )); |
||||
591 | } |
||||
592 | |||||
593 | return $hasAccess; |
||||
594 | } |
||||
595 | |||||
596 | /** |
||||
597 | * Given the limit of the current navigation item or page, this function |
||||
598 | * returns if the current Author can access that item or not. |
||||
599 | * |
||||
600 | * @since Symphony 2.5.1 |
||||
601 | * @param string $item_limit |
||||
602 | * @return boolean |
||||
603 | */ |
||||
604 | public function doesAuthorHaveAccess($item_limit = null) |
||||
0 ignored issues
–
show
|
|||||
605 | { |
||||
606 | $can_access = false; |
||||
607 | |||||
608 | if (!isset($item_limit) || $item_limit === 'author') { |
||||
609 | $can_access = true; |
||||
610 | } elseif ($item_limit === 'developer' && Symphony::Author()->isDeveloper()) { |
||||
611 | $can_access = true; |
||||
612 | } elseif ($item_limit === 'manager' && (Symphony::Author()->isManager() || Symphony::Author()->isDeveloper())) { |
||||
613 | $can_access = true; |
||||
614 | } elseif ($item_limit === 'primary' && Symphony::Author()->isPrimaryAccount()) { |
||||
615 | $can_access = true; |
||||
616 | } |
||||
617 | |||||
618 | return $can_access; |
||||
619 | } |
||||
620 | |||||
621 | /** |
||||
622 | * Appends the `$this->Header`, `$this->Context` and `$this->Contents` |
||||
623 | * to `$this->Wrapper` before adding the ID and class attributes for |
||||
624 | * the `<body>` element. This function will also place any Drawer elements |
||||
625 | * in their relevant positions in the page. After this has completed the |
||||
626 | * parent `generate()` is called which will convert the `XMLElement`'s |
||||
627 | * into strings ready for output. |
||||
628 | * |
||||
629 | * @see core.HTMLPage#generate() |
||||
630 | * @param null $page |
||||
0 ignored issues
–
show
|
|||||
631 | * @return string |
||||
632 | */ |
||||
633 | public function generate($page = null) |
||||
0 ignored issues
–
show
|
|||||
634 | { |
||||
635 | $this->Wrapper->appendChild($this->Header); |
||||
636 | |||||
637 | // Add horizontal drawers (inside #context) |
||||
638 | if (isset($this->Drawer['horizontal'])) { |
||||
639 | $this->Context->appendChildArray($this->Drawer['horizontal']); |
||||
640 | } |
||||
641 | |||||
642 | $this->Wrapper->appendChild($this->Context); |
||||
643 | |||||
644 | // Add vertical-left drawers (between #context and #contents) |
||||
645 | if (isset($this->Drawer['vertical-left'])) { |
||||
646 | $this->Contents->appendChildArray($this->Drawer['vertical-left']); |
||||
647 | } |
||||
648 | |||||
649 | // Add vertical-right drawers (after #contents) |
||||
650 | if (isset($this->Drawer['vertical-right'])) { |
||||
651 | $this->Contents->appendChildArray($this->Drawer['vertical-right']); |
||||
652 | } |
||||
653 | |||||
654 | $this->Wrapper->appendChild($this->Contents); |
||||
655 | |||||
656 | $this->Body->appendChild($this->Wrapper); |
||||
657 | |||||
658 | $this->__appendBodyId(); |
||||
659 | $this->__appendBodyClass($this->_context); |
||||
660 | |||||
661 | /** |
||||
662 | * This is just prior to the page headers being rendered, and is suitable for changing them |
||||
663 | * @delegate PreRenderHeaders |
||||
664 | * @since Symphony 2.7.0 |
||||
665 | * @param string $context |
||||
666 | * '/backend/' |
||||
667 | */ |
||||
668 | Symphony::ExtensionManager()->notifyMembers('PreRenderHeaders', '/backend/'); |
||||
669 | |||||
670 | return parent::generate($page); |
||||
671 | } |
||||
672 | |||||
673 | /** |
||||
674 | * Uses this pages PHP classname as the `<body>` ID attribute. |
||||
675 | * This function removes 'content' from the start of the classname |
||||
676 | * and converts all uppercase letters to lowercase and prefixes them |
||||
677 | * with a hyphen. |
||||
678 | */ |
||||
679 | private function __appendBodyId() |
||||
680 | { |
||||
681 | // trim "content" from beginning of class name |
||||
682 | $body_id = preg_replace("/^content/", '', get_class($this)); |
||||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
The string literal
/^content/ does not require double quotes, as per coding-style, please use single quotes.
PHP provides two ways to mark string literals. Either with single quotes String literals in single quotes on the other hand are evaluated very literally and the only two
characters that needs escaping in the literal are the single quote itself ( Double quoted string literals may contain other variables or more complex escape sequences. <?php
$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";
print $doubleQuoted;
will print an indented: If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear. For more information on PHP string literals and available escape sequences see the PHP core documentation. ![]() |
|||||
683 | |||||
684 | // lowercase any uppercase letters and prefix with a hyphen |
||||
685 | $body_id = trim( |
||||
686 | preg_replace_callback( |
||||
687 | "/([A-Z])/", |
||||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
The string literal
/([A-Z])/ does not require double quotes, as per coding-style, please use single quotes.
PHP provides two ways to mark string literals. Either with single quotes String literals in single quotes on the other hand are evaluated very literally and the only two
characters that needs escaping in the literal are the single quote itself ( Double quoted string literals may contain other variables or more complex escape sequences. <?php
$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";
print $doubleQuoted;
will print an indented: If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear. For more information on PHP string literals and available escape sequences see the PHP core documentation. ![]() |
|||||
688 | function($id) { |
||||
0 ignored issues
–
show
|
|||||
689 | return "-" . strtolower($id[0]); |
||||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
The string literal
- does not require double quotes, as per coding-style, please use single quotes.
PHP provides two ways to mark string literals. Either with single quotes String literals in single quotes on the other hand are evaluated very literally and the only two
characters that needs escaping in the literal are the single quote itself ( Double quoted string literals may contain other variables or more complex escape sequences. <?php
$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";
print $doubleQuoted;
will print an indented: If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear. For more information on PHP string literals and available escape sequences see the PHP core documentation. ![]() |
|||||
690 | }, |
||||
691 | $body_id |
||||
692 | ), |
||||
693 | '-' |
||||
694 | ); |
||||
695 | |||||
696 | if (!empty($body_id)) { |
||||
697 | $this->Body->setAttribute('id', trim($body_id)); |
||||
698 | } |
||||
699 | } |
||||
700 | |||||
701 | /** |
||||
702 | * Given the context of the current page, which is an associative |
||||
703 | * array, this function will append the values to the page's body as |
||||
704 | * classes. If an context value is numeric it will be prepended by 'id-', |
||||
705 | * otherwise all classes will be prefixed by the context key. |
||||
706 | * |
||||
707 | * @param array $context |
||||
708 | */ |
||||
709 | private function __appendBodyClass(array $context = array()) |
||||
0 ignored issues
–
show
|
|||||
710 | { |
||||
711 | $body_class = ''; |
||||
712 | |||||
713 | foreach ($context as $key => $value) { |
||||
714 | if (is_numeric($value)) { |
||||
715 | $value = 'id-' . $value; |
||||
716 | |||||
717 | // Add prefixes to all context values by making the |
||||
718 | // class be {key}-{value}. #1397 ^BA |
||||
719 | } elseif (!is_numeric($key) && isset($value)) { |
||||
720 | // Skip arrays |
||||
721 | if (is_array($value)) { |
||||
722 | $value = null; |
||||
723 | } else { |
||||
724 | $value = str_replace('_', '-', $key) . '-'. $value; |
||||
725 | } |
||||
726 | } |
||||
727 | |||||
728 | if ($value !== null) { |
||||
729 | $body_class .= trim($value) . ' '; |
||||
730 | } |
||||
731 | } |
||||
732 | |||||
733 | $classes = array_merge(explode(' ', trim($body_class)), explode(' ', trim($this->_body_class))); |
||||
734 | $body_class = trim(implode(' ', $classes)); |
||||
735 | |||||
736 | if (!empty($body_class)) { |
||||
737 | $this->Body->setAttribute('class', $body_class); |
||||
738 | } |
||||
739 | } |
||||
740 | |||||
741 | /** |
||||
742 | * Called to build the content for the page. This function immediately calls |
||||
743 | * `__switchboard()` which acts a bit of a controller to show content based on |
||||
744 | * off a type, such as 'view' or 'action'. `AdministrationPages` can override this |
||||
745 | * function to just display content if they do not need the switchboard functionality |
||||
746 | * |
||||
747 | * @see __switchboard() |
||||
748 | */ |
||||
749 | public function view() |
||||
750 | { |
||||
751 | $this->__switchboard(); |
||||
752 | } |
||||
753 | |||||
754 | /** |
||||
755 | * This function is called when `$_REQUEST` contains a key of 'action'. |
||||
756 | * Any logic that needs to occur immediately for the action to complete |
||||
757 | * should be contained within this function. By default this calls the |
||||
758 | * `__switchboard` with the type set to 'action'. |
||||
759 | * |
||||
760 | * @see __switchboard() |
||||
761 | */ |
||||
762 | public function action() |
||||
763 | { |
||||
764 | $this->__switchboard('action'); |
||||
765 | } |
||||
766 | |||||
767 | /** |
||||
768 | * The `__switchboard` function acts as a controller to display content |
||||
769 | * based off the $type. By default, the `$type` is 'view' but it can be set |
||||
770 | * also set to 'action'. The `$type` is prepended by __ and the context is |
||||
771 | * append to the $type to create the name of the function that will provide |
||||
772 | * that logic. For example, if the $type was action and the context of the |
||||
773 | * current page was new, the resulting function to be called would be named |
||||
774 | * `__actionNew()`. If an action function is not provided by the Page, this function |
||||
775 | * returns nothing, however if a view function is not provided, a 404 page |
||||
776 | * will be returned. |
||||
777 | * |
||||
778 | * @param string $type |
||||
779 | * Either 'view' or 'action', by default this will be 'view' |
||||
780 | * @throws SymphonyErrorPage |
||||
781 | */ |
||||
782 | public function __switchboard($type = 'view') |
||||
0 ignored issues
–
show
|
|||||
783 | { |
||||
784 | if (!isset($this->_context[0]) || trim($this->_context[0]) === '') { |
||||
785 | $context = 'index'; |
||||
786 | } else { |
||||
787 | $context = $this->_context[0]; |
||||
788 | } |
||||
789 | |||||
790 | $function = ($type == 'action' ? '__action' : '__view') . ucfirst($context); |
||||
0 ignored issues
–
show
|
|||||
791 | |||||
792 | if (!method_exists($this, $function)) { |
||||
793 | // If there is no action function, just return without doing anything |
||||
794 | if ($type == 'action') { |
||||
795 | return; |
||||
796 | } |
||||
797 | |||||
798 | Administration::instance()->errorPageNotFound(); |
||||
799 | } |
||||
800 | |||||
801 | $this->$function(null); |
||||
802 | } |
||||
803 | |||||
804 | /** |
||||
805 | * If `$this->Alert` is set, it will be added to this page. The |
||||
806 | * `AppendPageAlert` delegate is fired to allow extensions to provide their |
||||
807 | * their own Alert messages for this page. Since Symphony 2.3, there may be |
||||
808 | * more than one `Alert` per page. Alerts are displayed in the order of |
||||
809 | * severity, with Errors first, then Success alerts followed by Notices. |
||||
810 | * |
||||
811 | * @uses AppendPageAlert |
||||
812 | */ |
||||
813 | public function appendAlert() |
||||
814 | { |
||||
815 | /** |
||||
816 | * Allows for appending of alerts. Administration::instance()->Page->Alert is way to tell what |
||||
817 | * is currently in the system |
||||
818 | * |
||||
819 | * @delegate AppendPageAlert |
||||
820 | * @param string $context |
||||
821 | * '/backend/' |
||||
822 | */ |
||||
823 | Symphony::ExtensionManager()->notifyMembers('AppendPageAlert', '/backend/'); |
||||
824 | |||||
0 ignored issues
–
show
|
|||||
825 | |||||
826 | if (!is_array($this->Alert) || empty($this->Alert)) { |
||||
0 ignored issues
–
show
|
|||||
827 | return; |
||||
828 | } |
||||
829 | |||||
830 | usort($this->Alert, array($this, 'sortAlerts')); |
||||
831 | |||||
832 | // Using prependChild ruins our order (it's backwards, but with most |
||||
833 | // recent notices coming after oldest notices), so reversing the array |
||||
834 | // fixes this. We need to prepend so that without Javascript the notices |
||||
835 | // are at the top of the markup. See #1312 |
||||
836 | $this->Alert = array_reverse($this->Alert); |
||||
837 | |||||
838 | foreach ($this->Alert as $alert) { |
||||
839 | $this->Header->prependChild($alert->asXML()); |
||||
840 | } |
||||
841 | } |
||||
842 | |||||
843 | // Errors first, success next, then notices. |
||||
844 | public function sortAlerts($a, $b) |
||||
845 | { |
||||
846 | if ($a->{'type'} === $b->{'type'}) { |
||||
847 | return 0; |
||||
848 | } |
||||
849 | |||||
850 | if ( |
||||
0 ignored issues
–
show
|
|||||
851 | ($a->{'type'} === Alert::ERROR && $a->{'type'} !== $b->{'type'}) |
||||
852 | || ($a->{'type'} === Alert::SUCCESS && $b->{'type'} === Alert::NOTICE) |
||||
853 | ) { |
||||
854 | return -1; |
||||
855 | } |
||||
856 | |||||
857 | return 1; |
||||
858 | } |
||||
859 | |||||
860 | /** |
||||
861 | * This function will append the Navigation to the AdministrationPage. |
||||
862 | * It fires a delegate, NavigationPreRender, to allow extensions to manipulate |
||||
863 | * the navigation. Extensions should not use this to add their own navigation, |
||||
864 | * they should provide the navigation through their fetchNavigation function. |
||||
865 | * Note with the Section navigation groups, if there is only one section in a group |
||||
866 | * and that section is set to visible, the group will not appear in the navigation. |
||||
867 | * |
||||
868 | * @uses NavigationPreRender |
||||
869 | * @see getNavigationArray() |
||||
870 | * @see toolkit.Extension#fetchNavigation() |
||||
871 | */ |
||||
872 | public function appendNavigation() |
||||
873 | { |
||||
874 | $nav = $this->getNavigationArray(); |
||||
875 | |||||
876 | /** |
||||
877 | * Immediately before displaying the admin navigation. Provided with the |
||||
878 | * navigation array. Manipulating it will alter the navigation for all pages. |
||||
879 | * |
||||
880 | * @delegate NavigationPreRender |
||||
881 | * @param string $context |
||||
882 | * '/backend/' |
||||
883 | * @param array $nav |
||||
884 | * An associative array of the current navigation, passed by reference |
||||
885 | */ |
||||
886 | Symphony::ExtensionManager()->notifyMembers('NavigationPreRender', '/backend/', array( |
||||
887 | 'navigation' => &$nav, |
||||
888 | )); |
||||
889 | |||||
890 | $navElement = new XMLElement('nav', null, array('id' => 'nav', 'role' => 'navigation')); |
||||
891 | $contentNav = new XMLElement('ul', null, array('class' => 'content', 'role' => 'menubar')); |
||||
892 | $structureNav = new XMLElement('ul', null, array('class' => 'structure', 'role' => 'menubar')); |
||||
893 | |||||
894 | foreach ($nav as $n) { |
||||
895 | if (isset($n['visible']) && $n['visible'] === 'no') { |
||||
896 | continue; |
||||
897 | } |
||||
898 | |||||
899 | $item_limit = isset($n['limit']) ? $n['limit'] : null; |
||||
900 | |||||
901 | if ($this->doesAuthorHaveAccess($item_limit)) { |
||||
902 | $xGroup = new XMLElement('li', General::sanitize($n['name']), array('role' => 'presentation')); |
||||
903 | |||||
904 | if (isset($n['class']) && trim($n['name']) !== '') { |
||||
905 | $xGroup->setAttribute('class', $n['class']); |
||||
906 | } |
||||
907 | |||||
908 | $hasChildren = false; |
||||
909 | $xChildren = new XMLElement('ul', null, array('role' => 'menu')); |
||||
910 | |||||
911 | if (is_array($n['children']) && !empty($n['children'])) { |
||||
912 | foreach ($n['children'] as $c) { |
||||
913 | // adapt for Yes and yes |
||||
914 | if (strtolower($c['visible']) !== 'yes') { |
||||
915 | continue; |
||||
916 | } |
||||
917 | |||||
918 | $child_item_limit = isset($c['limit']) ? $c['limit'] : null; |
||||
919 | |||||
920 | if ($this->doesAuthorHaveAccess($child_item_limit)) { |
||||
921 | $xChild = new XMLElement('li'); |
||||
922 | $xChild->setAttribute('role', 'menuitem'); |
||||
923 | $linkChild = Widget::Anchor(General::sanitize($c['name']), SYMPHONY_URL . $c['link']); |
||||
0 ignored issues
–
show
|
|||||
924 | if (isset($c['target'])) { |
||||
925 | $linkChild->setAttribute('target', $c['target']); |
||||
926 | } |
||||
0 ignored issues
–
show
|
|||||
927 | $xChild->appendChild($linkChild); |
||||
928 | $xChildren->appendChild($xChild); |
||||
929 | $hasChildren = true; |
||||
930 | } |
||||
931 | } |
||||
932 | |||||
933 | if ($hasChildren) { |
||||
934 | $xGroup->setAttribute('aria-haspopup', 'true'); |
||||
935 | $xGroup->appendChild($xChildren); |
||||
936 | |||||
937 | if ($n['type'] === 'content') { |
||||
938 | $contentNav->appendChild($xGroup); |
||||
939 | } elseif ($n['type'] === 'structure') { |
||||
940 | $structureNav->prependChild($xGroup); |
||||
941 | } |
||||
942 | } |
||||
943 | } |
||||
944 | } |
||||
945 | } |
||||
946 | |||||
947 | $navElement->appendChild($contentNav); |
||||
948 | $navElement->appendChild($structureNav); |
||||
949 | $this->Header->appendChild($navElement); |
||||
950 | Symphony::Profiler()->sample('Navigation Built', PROFILE_LAP); |
||||
0 ignored issues
–
show
|
|||||
951 | } |
||||
952 | |||||
953 | /** |
||||
954 | * Returns the `$_navigation` variable of this Page. If it is empty, |
||||
955 | * it will be built by `__buildNavigation` |
||||
956 | * |
||||
957 | * When it calls `__buildNavigation`, it fires a delegate, NavigationPostBuild, |
||||
958 | * to allow extensions to manipulate the navigation. |
||||
959 | * |
||||
960 | * @uses NavigationPostBuild |
||||
961 | * @see __buildNavigation() |
||||
962 | * @return array |
||||
963 | */ |
||||
964 | public function getNavigationArray() |
||||
965 | { |
||||
966 | if (empty($this->_navigation)) { |
||||
967 | $this->__buildNavigation(); |
||||
968 | } |
||||
969 | |||||
970 | return $this->_navigation; |
||||
971 | } |
||||
972 | |||||
973 | /** |
||||
974 | * This method fills the `$nav` array with value |
||||
975 | * from the `ASSETS/xml/navigation.xml` file |
||||
976 | * |
||||
977 | * @link http://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml |
||||
978 | * |
||||
979 | * @since Symphony 2.3.2 |
||||
980 | * |
||||
981 | * @param array $nav |
||||
982 | * The navigation array that will receive nav nodes |
||||
983 | */ |
||||
984 | private function buildXmlNavigation(&$nav) |
||||
985 | { |
||||
986 | $xml = simplexml_load_file(ASSETS . '/xml/navigation.xml'); |
||||
0 ignored issues
–
show
|
|||||
987 | |||||
988 | // Loop over the default Symphony navigation file, converting |
||||
989 | // it into an associative array representation |
||||
990 | foreach ($xml->xpath('/navigation/group') as $n) { |
||||
991 | $index = (string)$n->attributes()->index; |
||||
992 | $children = $n->xpath('children/item'); |
||||
993 | $content = $n->attributes(); |
||||
994 | |||||
995 | // If the index is already set, increment the index and check again. |
||||
996 | // Rinse and repeat until the index is not set. |
||||
997 | if (isset($nav[$index])) { |
||||
998 | do { |
||||
999 | $index++; |
||||
1000 | } while (isset($nav[$index])); |
||||
1001 | } |
||||
1002 | |||||
1003 | $nav[$index] = array( |
||||
1004 | 'name' => __(strval($content->name)), |
||||
1005 | 'type' => 'structure', |
||||
1006 | 'index' => $index, |
||||
1007 | 'children' => array() |
||||
1008 | ); |
||||
1009 | |||||
1010 | if (strlen(trim((string)$content->limit)) > 0) { |
||||
1011 | $nav[$index]['limit'] = (string)$content->limit; |
||||
1012 | } |
||||
1013 | |||||
1014 | if (count($children) > 0) { |
||||
1015 | foreach ($children as $child) { |
||||
1016 | $item = array( |
||||
1017 | 'link' => (string)$child->attributes()->link, |
||||
1018 | 'name' => __(strval($child->attributes()->name)), |
||||
1019 | 'visible' => ((string)$child->attributes()->visible == 'no' ? 'no' : 'yes'), |
||||
0 ignored issues
–
show
|
|||||
1020 | ); |
||||
1021 | |||||
1022 | $limit = (string)$child->attributes()->limit; |
||||
1023 | |||||
1024 | if (strlen(trim($limit)) > 0) { |
||||
1025 | $item['limit'] = $limit; |
||||
1026 | } |
||||
1027 | |||||
1028 | $nav[$index]['children'][] = $item; |
||||
1029 | } |
||||
1030 | } |
||||
1031 | } |
||||
1032 | } |
||||
1033 | |||||
1034 | /** |
||||
1035 | * This method fills the `$nav` array with value |
||||
1036 | * from each Section |
||||
1037 | * |
||||
1038 | * @since Symphony 2.3.2 |
||||
1039 | * |
||||
1040 | * @param array $nav |
||||
1041 | * The navigation array that will receive nav nodes |
||||
1042 | */ |
||||
1043 | private function buildSectionNavigation(&$nav) |
||||
1044 | { |
||||
1045 | // Build the section navigation, grouped by their navigation groups |
||||
1046 | $sections = SectionManager::fetch(null, 'asc', 'sortorder'); |
||||
1047 | |||||
1048 | if (is_array($sections) && !empty($sections)) { |
||||
1049 | foreach ($sections as $s) { |
||||
1050 | $group_index = self::__navigationFindGroupIndex($nav, $s->get('navigation_group')); |
||||
1051 | |||||
1052 | if ($group_index === false) { |
||||
1053 | $group_index = General::array_find_available_index($nav, 0); |
||||
1054 | |||||
1055 | $nav[$group_index] = array( |
||||
1056 | 'name' => $s->get('navigation_group'), |
||||
1057 | 'type' => 'content', |
||||
1058 | 'index' => $group_index, |
||||
1059 | 'children' => array() |
||||
1060 | ); |
||||
1061 | } |
||||
1062 | |||||
1063 | $hasAccess = true; |
||||
1064 | $url = '/publish/' . $s->get('handle') . '/'; |
||||
1065 | /** |
||||
1066 | * Immediately after the core access rules allowed access to this page |
||||
1067 | * (i.e. not called if the core rules denied it). |
||||
1068 | * Extension developers must only further restrict access to it. |
||||
1069 | * Extension developers must also take care of checking the current value |
||||
1070 | * of the allowed parameter in order to prevent conflicts with other extensions. |
||||
1071 | * `$context['allowed'] = $context['allowed'] && customLogic();` |
||||
1072 | * |
||||
1073 | * @delegate CanAccessPage |
||||
1074 | * @since Symphony 2.7.0 |
||||
1075 | * @see doesAuthorHaveAccess() |
||||
1076 | * @param string $context |
||||
1077 | * '/backend/' |
||||
1078 | * @param bool $allowed |
||||
1079 | * A flag to further restrict access to the page, passed by reference |
||||
1080 | * @param string $page_limit |
||||
1081 | * The computed page limit for the current page |
||||
1082 | * @param string $page_url |
||||
1083 | * The computed page url for the current page |
||||
1084 | * @param int $section.id |
||||
1085 | * The id of the section for this url |
||||
1086 | * @param string $section.handle |
||||
1087 | * The handle of the section for this url |
||||
1088 | */ |
||||
1089 | Symphony::ExtensionManager()->notifyMembers('CanAccessPage', '/backend/', array( |
||||
1090 | 'allowed' => &$hasAccess, |
||||
1091 | 'page_limit' => 'author', |
||||
1092 | 'page_url' => $url, |
||||
1093 | 'section' => array( |
||||
1094 | 'id' => $s->get('id'), |
||||
1095 | 'handle' => $s->get('handle') |
||||
1096 | ), |
||||
1097 | )); |
||||
1098 | |||||
1099 | if ($hasAccess) { |
||||
1100 | $nav[$group_index]['children'][] = array( |
||||
1101 | 'link' => $url, |
||||
1102 | 'name' => $s->get('name'), |
||||
1103 | 'type' => 'section', |
||||
1104 | 'section' => array( |
||||
1105 | 'id' => $s->get('id'), |
||||
1106 | 'handle' => $s->get('handle') |
||||
1107 | ), |
||||
1108 | 'visible' => ($s->get('hidden') == 'no' ? 'yes' : 'no') |
||||
0 ignored issues
–
show
|
|||||
1109 | ); |
||||
1110 | } |
||||
1111 | } |
||||
1112 | } |
||||
1113 | } |
||||
1114 | |||||
1115 | /** |
||||
1116 | * This method fills the `$nav` array with value |
||||
1117 | * from each Extension's `fetchNavigation` method |
||||
1118 | * |
||||
1119 | * @since Symphony 2.3.2 |
||||
1120 | * |
||||
1121 | * @param array $nav |
||||
1122 | * The navigation array that will receive nav nodes |
||||
1123 | * @throws Exception |
||||
1124 | * @throws SymphonyErrorPage |
||||
1125 | */ |
||||
1126 | private function buildExtensionsNavigation(&$nav) |
||||
1127 | { |
||||
1128 | // Loop over all the installed extensions to add in other navigation items |
||||
1129 | $extensions = Symphony::ExtensionManager()->listInstalledHandles(); |
||||
1130 | |||||
1131 | foreach ($extensions as $e) { |
||||
1132 | $extension = Symphony::ExtensionManager()->getInstance($e); |
||||
1133 | $extension_navigation = $extension->fetchNavigation(); |
||||
1134 | |||||
1135 | if (is_array($extension_navigation) && !empty($extension_navigation)) { |
||||
1136 | foreach ($extension_navigation as $item) { |
||||
1137 | $type = isset($item['children']) ? Extension::NAV_GROUP : Extension::NAV_CHILD; |
||||
1138 | |||||
1139 | switch ($type) { |
||||
1140 | case Extension::NAV_GROUP: |
||||
1141 | $index = General::array_find_available_index($nav, $item['location']); |
||||
1142 | |||||
1143 | // Actual group |
||||
1144 | $nav[$index] = self::createParentNavItem($index, $item); |
||||
1145 | |||||
1146 | // Render its children |
||||
1147 | foreach ($item['children'] as $child) { |
||||
1148 | $nav[$index]['children'][] = self::createChildNavItem($child, $e); |
||||
1149 | } |
||||
0 ignored issues
–
show
|
|||||
1150 | |||||
1151 | break; |
||||
1152 | |||||
1153 | case Extension::NAV_CHILD: |
||||
1154 | if (!is_numeric($item['location'])) { |
||||
1155 | // is a navigation group |
||||
1156 | $group_name = $item['location']; |
||||
1157 | $group_index = self::__navigationFindGroupIndex($nav, $item['location']); |
||||
1158 | } else { |
||||
1159 | // is a legacy numeric index |
||||
1160 | $group_index = $item['location']; |
||||
1161 | } |
||||
1162 | |||||
1163 | $child = self::createChildNavItem($item, $e); |
||||
1164 | |||||
1165 | if ($group_index === false) { |
||||
1166 | $group_index = General::array_find_available_index($nav, 0); |
||||
1167 | |||||
1168 | $nav_parent = self::createParentNavItem($group_index, $item); |
||||
1169 | $nav_parent['name'] = $group_name; |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
1170 | $nav_parent['children'] = array($child); |
||||
1171 | |||||
1172 | // add new navigation group |
||||
1173 | $nav[$group_index] = $nav_parent; |
||||
1174 | } else { |
||||
1175 | // add new location by index |
||||
1176 | $nav[$group_index]['children'][] = $child; |
||||
1177 | } |
||||
0 ignored issues
–
show
|
|||||
1178 | |||||
1179 | break; |
||||
1180 | } |
||||
1181 | } |
||||
1182 | } |
||||
1183 | } |
||||
1184 | } |
||||
1185 | |||||
1186 | /** |
||||
1187 | * This function builds out a navigation menu item for parents. Parents display |
||||
1188 | * in the top level navigation of the backend and may have children (dropdown menus) |
||||
1189 | * |
||||
1190 | * @since Symphony 2.5.1 |
||||
1191 | * @param integer $index |
||||
1192 | * @param array $item |
||||
1193 | * @return array |
||||
1194 | */ |
||||
1195 | private static function createParentNavItem($index, $item) |
||||
1196 | { |
||||
1197 | $nav_item = array( |
||||
1198 | 'name' => $item['name'], |
||||
1199 | 'type' => isset($item['type']) ? $item['type'] : 'structure', |
||||
1200 | 'index' => $index, |
||||
1201 | 'children' => array(), |
||||
1202 | 'limit' => isset($item['limit']) ? $item['limit'] : null |
||||
1203 | ); |
||||
1204 | |||||
1205 | return $nav_item; |
||||
1206 | } |
||||
1207 | |||||
1208 | /** |
||||
1209 | * This function builds out a navigation menu item for children. Children |
||||
1210 | * live under a parent navigation item and are shown on hover. |
||||
1211 | * |
||||
1212 | * @since Symphony 2.5.1 |
||||
1213 | * @param array $item |
||||
1214 | * @param string $extension_handle |
||||
1215 | * @return array |
||||
1216 | */ |
||||
1217 | private static function createChildNavItem($item, $extension_handle) |
||||
1218 | { |
||||
1219 | if (!isset($item['relative']) || $item['relative'] === true) { |
||||
1220 | $link = '/extension/' . $extension_handle . '/' . ltrim($item['link'], '/'); |
||||
1221 | } else { |
||||
1222 | $link = '/' . ltrim($item['link'], '/'); |
||||
1223 | } |
||||
1224 | |||||
1225 | $nav_item = array( |
||||
1226 | 'link' => $link, |
||||
1227 | 'name' => $item['name'], |
||||
1228 | 'visible' => (isset($item['visible']) && $item['visible'] == 'no') ? 'no' : 'yes', |
||||
1229 | 'limit' => isset($item['limit']) ? $item['limit'] : null, |
||||
1230 | 'target' => isset($item['target']) ? $item['target'] : null |
||||
1231 | ); |
||||
1232 | |||||
1233 | return $nav_item; |
||||
1234 | } |
||||
1235 | |||||
1236 | /** |
||||
1237 | * This function populates the `$_navigation` array with an associative array |
||||
1238 | * of all the navigation groups and their links. Symphony only supports one |
||||
1239 | * level of navigation, so children links cannot have children links. The default |
||||
1240 | * Symphony navigation is found in the `ASSETS/xml/navigation.xml` folder. This is |
||||
1241 | * loaded first, and then the Section navigation is built, followed by the Extension |
||||
1242 | * navigation. Additionally, this function will set the active group of the navigation |
||||
1243 | * by checking the current page against the array of links. |
||||
1244 | * |
||||
1245 | * It fires a delegate, NavigationPostBuild, to allow extensions to manipulate |
||||
1246 | * the navigation. |
||||
1247 | * |
||||
1248 | * @uses NavigationPostBuild |
||||
1249 | * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml |
||||
1250 | * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/lib/toolkit/class.extension.php |
||||
1251 | */ |
||||
1252 | public function __buildNavigation() |
||||
1253 | { |
||||
1254 | $nav = array(); |
||||
1255 | |||||
1256 | $this->buildXmlNavigation($nav); |
||||
1257 | $this->buildSectionNavigation($nav); |
||||
1258 | $this->buildExtensionsNavigation($nav); |
||||
1259 | |||||
1260 | $pageCallback = Administration::instance()->getPageCallback(); |
||||
1261 | |||||
1262 | $pageRoot = $pageCallback['pageroot'] . (isset($pageCallback['context'][0]) ? $pageCallback['context'][0] . '/' : ''); |
||||
1263 | $found = self::__findActiveNavigationGroup($nav, $pageRoot); |
||||
1264 | |||||
1265 | // Normal searches failed. Use a regular expression using the page root. This is less |
||||
1266 | // efficient and should never really get invoked unless something weird is going on |
||||
1267 | if (!$found) { |
||||
1268 | self::__findActiveNavigationGroup($nav, '/^' . str_replace('/', '\/', $pageCallback['pageroot']) . '/i', true); |
||||
1269 | } |
||||
1270 | |||||
1271 | ksort($nav); |
||||
1272 | $this->_navigation = $nav; |
||||
1273 | |||||
1274 | /** |
||||
1275 | * Immediately after the navigation array as been built. Provided with the |
||||
1276 | * navigation array. Manipulating it will alter the navigation for all pages. |
||||
1277 | * Developers can also alter the 'limit' property of each page to allow more |
||||
1278 | * or less access to them. |
||||
1279 | * Preventing a user from accessing the page affects both the navigation and the |
||||
1280 | * page access rights: user will get a 403 Forbidden error if not authorized. |
||||
1281 | * |
||||
1282 | * @delegate NavigationPostBuild |
||||
1283 | * @since Symphony 2.7.0 |
||||
1284 | * @param string $context |
||||
1285 | * '/backend/' |
||||
1286 | * @param array $nav |
||||
1287 | * An associative array of the current navigation, passed by reference |
||||
1288 | */ |
||||
1289 | Symphony::ExtensionManager()->notifyMembers('NavigationPostBuild', '/backend/', array( |
||||
1290 | 'navigation' => &$this->_navigation, |
||||
1291 | )); |
||||
1292 | } |
||||
1293 | |||||
1294 | /** |
||||
1295 | * Given an associative array representing the navigation, and a group, |
||||
1296 | * this function will attempt to return the index of the group in the navigation |
||||
1297 | * array. If it is found, it will return the index, otherwise it will return false. |
||||
1298 | * |
||||
1299 | * @param array $nav |
||||
1300 | * An associative array of the navigation where the key is the group |
||||
1301 | * index, and the value is an associative array of 'name', 'index' and |
||||
1302 | * 'children'. Name is the name of the this group, index is the same as |
||||
1303 | * the key and children is an associative array of navigation items containing |
||||
1304 | * the keys 'link', 'name' and 'visible'. The 'haystack'. |
||||
1305 | * @param string $group |
||||
1306 | * The group name to find, the 'needle'. |
||||
1307 | * @return integer|boolean |
||||
1308 | * If the group is found, the index will be returned, otherwise false. |
||||
1309 | */ |
||||
1310 | private static function __navigationFindGroupIndex(array $nav, $group) |
||||
1311 | { |
||||
1312 | foreach ($nav as $index => $item) { |
||||
1313 | if ($item['name'] === $group) { |
||||
1314 | return $index; |
||||
1315 | } |
||||
1316 | } |
||||
1317 | |||||
1318 | return false; |
||||
1319 | } |
||||
1320 | |||||
1321 | /** |
||||
1322 | * Given the navigation array, this function will loop over all the items |
||||
1323 | * to determine which is the 'active' navigation group, or in other words, |
||||
1324 | * what group best represents the current page `$this->Author` is viewing. |
||||
1325 | * This is done by checking the current page's link against all the links |
||||
1326 | * provided in the `$nav`, and then flagging the group of the found link |
||||
1327 | * with an 'active' CSS class. The current page's link omits any flags or |
||||
1328 | * URL parameters and just uses the root page URL. |
||||
1329 | * |
||||
1330 | * @param array $nav |
||||
1331 | * An associative array of the navigation where the key is the group |
||||
1332 | * index, and the value is an associative array of 'name', 'index' and |
||||
1333 | * 'children'. Name is the name of the this group, index is the same as |
||||
1334 | * the key and children is an associative array of navigation items containing |
||||
1335 | * the keys 'link', 'name' and 'visible'. The 'haystack'. This parameter is passed |
||||
1336 | * by reference to this function. |
||||
1337 | * @param string $pageroot |
||||
1338 | * The current page the Author is the viewing, minus any flags or URL |
||||
1339 | * parameters such as a Symphony object ID. eg. Section ID, Entry ID. This |
||||
1340 | * parameter is also be a regular expression, but this is highly unlikely. |
||||
1341 | * @param boolean $pattern |
||||
1342 | * If set to true, the `$pageroot` represents a regular expression which will |
||||
1343 | * determine if the active navigation item |
||||
1344 | * @return boolean |
||||
1345 | * Returns true if an active link was found, false otherwise. If true, the |
||||
1346 | * navigation group of the active link will be given the CSS class 'active' |
||||
1347 | */ |
||||
1348 | private static function __findActiveNavigationGroup(array &$nav, $pageroot, $pattern = false) |
||||
0 ignored issues
–
show
|
|||||
1349 | { |
||||
1350 | foreach ($nav as $index => $contents) { |
||||
1351 | if (is_array($contents['children']) && !empty($contents['children'])) { |
||||
1352 | foreach ($contents['children'] as $item) { |
||||
1353 | if ($pattern && preg_match($pageroot, $item['link'])) { |
||||
1354 | $nav[$index]['class'] = 'active'; |
||||
1355 | return true; |
||||
1356 | } elseif ($item['link'] == $pageroot) { |
||||
1357 | $nav[$index]['class'] = 'active'; |
||||
1358 | return true; |
||||
1359 | } |
||||
1360 | } |
||||
1361 | } |
||||
1362 | } |
||||
1363 | |||||
1364 | return false; |
||||
1365 | } |
||||
1366 | |||||
1367 | /** |
||||
1368 | * Creates the Symphony footer for an Administration page. By default |
||||
1369 | * this includes the installed Symphony version and the currently logged |
||||
1370 | * in Author. A delegate is provided to allow extensions to manipulate the |
||||
1371 | * footer HTML, which is an XMLElement of a `<ul>` element. |
||||
1372 | * Since Symphony 2.3, it no longer uses the `AddElementToFooter` delegate. |
||||
1373 | */ |
||||
1374 | public function appendUserLinks() |
||||
1375 | { |
||||
1376 | $ul = new XMLElement('ul', null, array('id' => 'session')); |
||||
1377 | |||||
1378 | $li = new XMLElement('li'); |
||||
1379 | $li->appendChild( |
||||
1380 | Widget::Anchor( |
||||
1381 | Symphony::Author()->getFullName(), |
||||
1382 | SYMPHONY_URL . '/system/authors/edit/' . Symphony::Author()->get('id') . '/' |
||||
0 ignored issues
–
show
|
|||||
1383 | ) |
||||
1384 | ); |
||||
1385 | $ul->appendChild($li); |
||||
1386 | |||||
1387 | $li = new XMLElement('li'); |
||||
1388 | $li->appendChild(Widget::Anchor(__('Log out'), SYMPHONY_URL . '/logout/', null, null, null, array('accesskey' => 'l'))); |
||||
1389 | $ul->appendChild($li); |
||||
1390 | |||||
1391 | $this->Header->appendChild($ul); |
||||
1392 | } |
||||
1393 | |||||
1394 | /** |
||||
1395 | * Adds a localized Alert message for failed timestamp validations. |
||||
1396 | * It also adds meta information about the last author and timestamp. |
||||
1397 | * |
||||
1398 | * @since Symphony 2.7.0 |
||||
1399 | * @param string $errorMessage |
||||
1400 | * The error message to display. |
||||
1401 | * @param Entry|Section $existingObject |
||||
1402 | * The Entry or section object that failed validation. |
||||
1403 | * @param string $action |
||||
1404 | * The requested action. |
||||
1405 | */ |
||||
1406 | public function addTimestampValidationPageAlert($errorMessage, $existingObject, $action) |
||||
1407 | { |
||||
1408 | $authorId = $existingObject->get('modification_author_id'); |
||||
1409 | if (!$authorId) { |
||||
1410 | $authorId = $existingObject->get('author_id'); |
||||
1411 | } |
||||
0 ignored issues
–
show
|
|||||
1412 | $author = AuthorManager::fetchByID($authorId); |
||||
0 ignored issues
–
show
It seems like
$authorId can also be of type string ; however, parameter $id of AuthorManager::fetchByID() does only seem to accept array|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
![]() |
|||||
1413 | $formatteAuthorName = $authorId === Symphony::Author()->get('id') |
||||
1414 | ? __('yourself') |
||||
0 ignored issues
–
show
|
|||||
1415 | : (!$author |
||||
1416 | ? __('an unknown user') |
||||
0 ignored issues
–
show
|
|||||
1417 | : $author->get('first_name') . ' ' . $author->get('last_name')); |
||||
1418 | |||||
1419 | $msg = $this->_errors['timestamp'] . ' ' . __( |
||||
1420 | 'made by %s at %s.', array( |
||||
1421 | $formatteAuthorName, |
||||
1422 | Widget::Time($existingObject->get('modification_date'))->generate(), |
||||
0 ignored issues
–
show
It seems like
$existingObject->get('modification_date') can also be of type array ; however, parameter $string of Widget::Time() does only seem to accept string , 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
![]() |
|||||
1423 | ) |
||||
1424 | ); |
||||
1425 | |||||
1426 | $currentUrl = Administration::instance()->getCurrentPageURL(); |
||||
1427 | $overwritelink = Widget::Anchor( |
||||
1428 | __('Replace changes?'), |
||||
1429 | $currentUrl, |
||||
1430 | __('Overwrite'), |
||||
1431 | 'js-tv-overwrite', |
||||
1432 | null, |
||||
1433 | array( |
||||
1434 | 'data-action' => General::sanitize($action) |
||||
1435 | ) |
||||
1436 | ); |
||||
1437 | $ignorelink = Widget::Anchor( |
||||
1438 | __('View changes.'), |
||||
1439 | $currentUrl, |
||||
1440 | __('View the updated entry') |
||||
1441 | ); |
||||
1442 | $actions = $overwritelink->generate() . ' ' . $ignorelink->generate(); |
||||
1443 | |||||
1444 | $this->pageAlert("$msg $actions", Alert::ERROR); |
||||
0 ignored issues
–
show
As per coding-style, please use concatenation or
sprintf for the variable $msg instead of interpolation.
It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings. // Instead of
$x = "foo $bar $baz";
// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
![]() As per coding-style, please use concatenation or
sprintf for the variable $actions instead of interpolation.
It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings. // Instead of
$x = "foo $bar $baz";
// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
![]() |
|||||
1445 | } |
||||
1446 | } |
||||
1447 |