PluginProbe
Advanced Access Manager – Access Governance for WordPress / 7.1.2
Advanced Access Manager – Access Governance for WordPress v7.1.2
7.1.4 7.1.2 7.1.3 6.8.4 6.8.5 6.9.0 6.9.1 6.9.10 6.9.11 6.9.12 6.9.13 6.9.14 6.9.15 6.9.16 6.9.17 6.9.18 6.9.19 6.9.2 6.9.20 6.9.21 6.9.22 6.9.23 6.9.24 6.9.25 6.9.26 All 210 releases
advanced-access-manager / application / Framework / Service / BackendMenu.php
BackendMenu.php
688 lines 17.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * ======================================================================
5 * LICENSE: This file is subject to the terms and conditions defined in *
6 * file 'license.txt', which is part of this source code package. *
7 * ======================================================================
8 */
9
10 /**
11 * Framework service to manage access to the backend (admin) menu
12 *
13 * @package AAM
14 * @version 7.0.0
15 */
16 class AAM_Framework_Service_BackendMenu
17 {
18
19 use AAM_Framework_Service_BaseTrait;
20
21 /**
22 * DB cache option
23 *
24 * @version 7.0.0
25 */
26 const CACHE_OPTION = 'aam_menu';
27
28 /**
29 * Return the complete backend menu list with permissions
30 *
31 * @return array
32 * @access public
33 *
34 * @version 7.0.0
35 * @todo - Move to AAM_Service_BackendMenu
36 */
37 public function get_items()
38 {
39 try {
40 $result = [];
41
42 // Getting the menu cache so we can build the list
43 $menu = $this->_get_raw_menu();
44
45 if (!empty($menu)) {
46 foreach ($menu['menu'] as $item) {
47 if (preg_match('/^separator/', $item[2])) {
48 continue; //skip separator
49 }
50
51 array_push($result, $this->_prepare_menu_item($item));
52 }
53 }
54 } catch (Exception $e) {
55 $result = $this->_handle_error($e);
56 }
57
58 return $result;
59 }
60
61 /**
62 * Alias for the get_items method
63 *
64 * @return array
65 * @access public
66 *
67 * @version 7.0.0
68 * @todo - Move to AAM_Service_BackendMenu
69 */
70 public function items()
71 {
72 return $this->get_items();
73 }
74
75 /**
76 * Get existing menu by ID
77 *
78 * @param string $menu_slug
79 *
80 * @return array
81 * @access public
82 *
83 * @version 7.0.0
84 * @todo - Move to AAM_Service_BackendMenu
85 */
86 public function get_item($menu_slug)
87 {
88 try {
89 $result = false;
90 $menu_slug = $this->_normalize_resource_identifier($menu_slug);
91
92 foreach($this->get_items() as $item) {
93 if ($item['slug'] === $menu_slug) {
94 $result = $item;
95 } elseif (isset($item['children'])) {
96 foreach($item['children'] as $child) {
97 if ($child['slug'] === $menu_slug) {
98 $result = $child;
99 }
100 }
101 }
102
103 // If we found menu, just break the search
104 if ($result !== false) { break; }
105 }
106
107 if ($result === false) {
108 throw new OutOfRangeException('Backend menu item does not exist');
109 }
110 } catch (Exception $e) {
111 $result = $this->_handle_error($e);
112 }
113
114 return $result;
115 }
116
117 /**
118 * An alias for the get_item method
119 *
120 * @param string $menu_slug
121 *
122 * @return array
123 * @access public
124 *
125 * @version 7.0.0
126 * @todo - Move to AAM_Service_BackendMenu
127 */
128 public function item($menu_slug)
129 {
130 return $this->get_item($menu_slug);
131 }
132
133 /**
134 * Restrict access to a given menu item
135 *
136 * @param string $menu_slug
137 *
138 * @return bool|WP_Error
139 * @access public
140 *
141 * @version 7.0.0
142 */
143 public function deny($menu_slug)
144 {
145 try {
146 $result = $this->_set_item_permission($menu_slug, 'deny');
147 } catch (Exception $e) {
148 $result = $this->_handle_error($e);
149 }
150
151 return $result;
152 }
153
154 /**
155 * Allow access to a given menu item
156 *
157 * @param string $menu_slug
158 *
159 * @return bool|WP_Error
160 * @access public
161 *
162 * @version 7.0.0
163 */
164 public function allow($menu_slug)
165 {
166 try {
167 $result = $this->_set_item_permission($menu_slug, 'allow');
168 } catch (Exception $e) {
169 $result = $this->_handle_error($e);
170 }
171
172 return $result;
173 }
174
175 /**
176 * Reset all backend menu permissions
177 *
178 * @param string $menu_slug [Optional]
179 *
180 * @return bool|WP_Error
181 * @access public
182 *
183 * @version 7.0.0
184 */
185 public function reset($menu_slug = null)
186 {
187 try {
188 $resource = $this->_get_resource();
189
190 if (!empty($menu_slug)) {
191 $result = $resource->remove_permission(
192 $this->_normalize_resource_identifier($menu_slug),
193 'access'
194 );
195 } else {
196 $result = $resource->reset();
197 }
198 } catch (Exception $e) {
199 $result = $this->_handle_error($e);
200 }
201
202 return $result;
203 }
204
205 /**
206 * Check if menu item is restricted
207 *
208 * @param string $menu_slug
209 *
210 * @return bool|WP_Error
211 * @access public
212 *
213 * @version 7.0.0
214 */
215 public function is_denied($menu_slug)
216 {
217 try {
218 $result = null;
219
220 // Normalize the input data based on top level flat
221 $slug = $this->_normalize_resource_identifier($menu_slug);
222 $parent_slug = null;
223
224 // The default dashboard landing page is always excluded
225 if ($slug !== 'index.php') {
226 $resource = $this->_get_resource();
227
228 // Check if menu is explicitly allowed
229 $permission = $resource->get_permission($slug, 'access');
230
231 if (!empty($permission)) {
232 $result = $permission['effect'] !== 'allow';
233 }
234
235 // If menu is not top level item, assume that this is a submenu item
236 // and check if parent menu item is restricted
237 if (is_null($result) && strpos($slug, 'menu/') !== 0) {
238 if ($slug === 'post.php') { // Submitting post
239 $post_type = $this->misc->get($_POST, 'post_type');
240 $parent_slug = 'menu/edit.php';
241
242 // Here we are covering the post management screens. WP core
243 // recycles the "edit.php" screen to manage all post types.
244 // However, if "Posts" (default WP posts) get restricted, it
245 // creates an issues for all other custom post type screens.
246 // This is why we are taking extra steps to ensure proper
247 // access controls
248 if(!empty($post_type) && $post_type !== 'post') {
249 $parent_slug .= '?post_type=' . $post_type;
250 } elseif (isset($_GET['post'])) {
251 $post = get_post(filter_input(INPUT_GET, 'post'));
252
253 if (is_a($post, WP_Post::class)
254 && $post->post_type !== 'post'
255 ) {
256 $parent_slug .= '?post_type=' . $post->post_type;
257 }
258 }
259 }
260
261 if (empty($parent_slug)){
262 $parent_slug = $this->_get_parent_slug($slug);
263 }
264
265 // If we found a parent menu item, check permissions
266 if (!empty($parent_slug)) {
267 $permission = $resource->get_permission(
268 $parent_slug, 'access'
269 );
270
271 if (!empty($permission)) {
272 $result = $permission['effect'] !== 'allow';
273 }
274 }
275 }
276
277 // Step #3. Allow third-party services to hook into the decision
278 // process
279 $result = apply_filters(
280 'aam_backend_menu_is_denied_filter',
281 $result,
282 $slug, // Note! Passing already normalized menu slug
283 $resource,
284 $parent_slug
285 );
286
287 // Prepare the final answer
288 $result = is_bool($result) ? $result : false;
289 } else {
290 $result = false;
291 }
292 } catch (Exception $e) {
293 $result = $this->_handle_error($e);
294 }
295
296 return $result;
297 }
298
299 /**
300 * Check if menu item is allowed
301 *
302 * @param string $menu_slug
303 *
304 * @return bool|WP_Error
305 * @access public
306 *
307 * @version 7.0.0
308 */
309 public function is_allowed($menu_slug)
310 {
311 $result = $this->is_denied($menu_slug);
312
313 return is_bool($result) ? !$result : $result;
314 }
315
316 /**
317 * Set permissions for a given menu slug
318 *
319 * @param string $menu_slug
320 * @param string $effect
321 *
322 * @return bool
323 * @access private
324 *
325 * @version 7.0.0
326 */
327 private function _set_item_permission($menu_slug, $effect)
328 {
329 return $this->_get_resource()->set_permission(
330 $this->_normalize_resource_identifier($menu_slug),
331 'access',
332 $effect
333 );
334 }
335
336 /**
337 * Get backend menu resource
338 *
339 * @return AAM_Framework_Resource_BackendMenu
340 * @access private
341 *
342 * @version 7.0.0
343 */
344 private function _get_resource()
345 {
346 return $this->_get_access_level()->get_resource(
347 AAM_Framework_Type_Resource::BACKEND_MENU
348 );
349 }
350
351 /**
352 * Get raw Admin Menu
353 *
354 * This method also caches the admin menu for future usage
355 *
356 * @return array
357 * @access private
358 *
359 * @version 7.0.2
360 */
361 private function _get_raw_menu()
362 {
363 static $_cache = [];
364 global $menu, $submenu;
365
366 if ((defined('DOING_AJAX') && DOING_AJAX)
367 || $this->misc->get_current_area() === 'api'
368 ) {
369 $result = $_cache = $this->cache->get(self::CACHE_OPTION);
370 } elseif (empty($_cache)) {
371 $result = [];
372 $persist_cache = false;
373
374 if (!empty($menu)) {
375 $result['menu'] = $this->_prepare_menu_items($menu);
376 $persist_cache = true;
377 }
378
379 if (!empty($submenu)) {
380 $result['submenu'] = $this->_prepare_submenu_items($submenu);
381 $persist_cache = true;
382 }
383
384 if ($persist_cache) {
385 $this->cache->set(self::CACHE_OPTION, $result, 31536000);
386 }
387
388 $_cache = $result; // Avoid doing the same thing over & over again
389 } else {
390 $result = $_cache;
391 }
392
393 return is_array($result) ? $result : [];
394 }
395
396 /**
397 * Prepare menu items to be cached
398 *
399 * @param array $items
400 *
401 * @return array
402 * @access private
403 *
404 * @version 7.0.2
405 */
406 private function _prepare_menu_items($items)
407 {
408 $response = [];
409
410 if (is_array($items)) {
411 foreach($items as $i => $item) {
412 $response[$i] = $this->_get_menu_item_attributes($item);
413 }
414 }
415
416 return $response;
417 }
418
419 /**
420 * Prepare submenu item list
421 *
422 * @param array $items
423 *
424 * @return array
425 * @access private
426 *
427 * @version 7.0.2
428 */
429 private function _prepare_submenu_items($items)
430 {
431 $response = [];
432
433 if (is_array($items)) {
434 foreach($items as $menu_id => $sub_level) {
435 $response[$menu_id] = [];
436
437 foreach($sub_level as $i => $item) {
438 $response[$menu_id][$i] = $this->_get_menu_item_attributes($item);
439 }
440 }
441 }
442
443 return $response;
444 }
445
446 /**
447 * Get menu item attributes
448 *
449 * Return only attributes we are interested in
450 *
451 * @param array $item
452 *
453 * @return array
454 * @access private
455 *
456 * @version 7.0.0
457 */
458 private function _get_menu_item_attributes($item)
459 {
460 return [
461 // Name
462 base64_encode(is_string($item[0]) ? $item[0] : __('No Label', 'advanced-access-manager')),
463 // Capability
464 $item[1],
465 // Slug
466 $item[2]
467 ];
468 }
469
470 /**
471 * Normalize and prepare the menu item model
472 *
473 * @param array $menu_item
474 * @param bool $is_top_level
475 *
476 * @return array
477 * @access private
478 *
479 * @version 7.0.0
480 */
481 private function _prepare_menu_item($menu_item, $is_top_level = true)
482 {
483 $normalized = $this->_normalize_resource_identifier($menu_item[2]);
484 $slug = $is_top_level ? 'menu/' . $normalized : $normalized;
485
486 $response = array(
487 'slug' => $slug,
488 'path' => $this->_prepare_admin_uri($menu_item[2]),
489 'name' => $this->_filter_menu_name($menu_item[0]),
490 'capability' => $menu_item[1],
491 'is_restricted' => $this->is_denied($slug)
492 );
493
494 if ($is_top_level) {
495 $menu = $this->_get_raw_menu();
496
497 $response['children'] = $this->_get_submenu(
498 $menu_item[2],
499 isset($menu['submenu']) ? $menu['submenu'] : []
500 );
501 }
502
503 return $response;
504 }
505
506 /**
507 * Normalize the menu slug
508 *
509 * @param string $resource_identifier
510 *
511 * @return string
512 * @access private
513 *
514 * @version 7.0.0
515 */
516 private function _normalize_resource_identifier($resource_identifier)
517 {
518 if (strpos($resource_identifier, '.php') !== false) {
519 $parsed_url = wp_parse_url($resource_identifier);
520 $parsed_slug = $parsed_url['path'];
521
522 if (isset($parsed_url['query'])) {
523 parse_str(html_entity_decode($parsed_url['query']), $query_params);
524
525 // Removing some redundant query params
526 $redundant_params = apply_filters(
527 'aam_ignored_backend_menu_item_query_params_filter',
528 ['return', 'path']
529 );
530
531 foreach($redundant_params as $to_remove) {
532 if (array_key_exists($to_remove, $query_params)) {
533 unset($query_params[$to_remove]);
534 }
535 }
536
537 // Finally, sort the list of query params in alphabetical order to
538 // ensure consistent order
539 ksort($query_params);
540
541 if (count($query_params)) {
542 $parsed_slug .= '?' . http_build_query($query_params);
543 }
544 }
545 } else {
546 $parsed_slug = trim($resource_identifier);
547 }
548
549 return urldecode($parsed_slug);
550 }
551
552 /**
553 * Get parent menu
554 *
555 * @param string $slug
556 *
557 * @return string|null
558 * @access private
559 * @global array $submenu
560 *
561 * @version 7.0.0
562 */
563 private function _get_parent_slug($search)
564 {
565 global $submenu;
566
567 $result = $this->_find_parent(
568 is_array($submenu) ? $submenu : [],
569 $search
570 );
571
572 // If we cannot find parent menu in current $submenu array, try to find it
573 // in the cached menu generated by super admin. This is important to cover
574 // scenarios where submenus bubble up to menu. E.g. Profile
575 if (is_null($result)) {
576 $menu = $this->_get_raw_menu();
577 $result = $this->_find_parent(
578 isset($menu['submenu']) ? $menu['submenu'] : [],
579 $search
580 );
581 }
582
583 return $result;
584 }
585
586 /**
587 * Find parent menu from the array of menu items
588 *
589 * @param array $array
590 * @param string $search
591 *
592 * @return null|string
593 * @access private
594 *
595 * @version 7.0.0
596 */
597 private function _find_parent($array, $search)
598 {
599 $result = null;
600
601 foreach ($array as $parent => $subs) {
602 foreach ($subs as $sub) {
603 $slug = $this->_normalize_resource_identifier($sub[2]);
604
605 if ($slug === $search) {
606 $result = 'menu/' . $parent;
607 }
608 }
609
610 if ($result !== null) {
611 break;
612 }
613 }
614
615 return $result;
616 }
617
618 /**
619 * Prepare admin URI for the menu item
620 *
621 * @param string $menu_slug
622 *
623 * @return string
624 * @access private
625 *
626 * @version 7.0.0
627 */
628 private function _prepare_admin_uri($menu_slug)
629 {
630 if (strpos($menu_slug, '.php') === false) {
631 $uri = admin_url('admin.php?page=' . $menu_slug);
632 } else {
633 $uri = '/wp-admin/' . trim($menu_slug, '/');
634 }
635
636 // Only prepare the relative path
637 return $this->misc->sanitize_url($uri);
638 }
639
640 /**
641 * Filter menu name
642 *
643 * Strip any HTML tags from the menu name and also remove the trailing
644 * numbers in case of Plugin or Comments menu name.
645 *
646 * @param string $name
647 *
648 * @return string
649 * @access private
650 *
651 * @version 7.0.0
652 */
653 private function _filter_menu_name($name)
654 {
655 if (is_string($name)) {
656 $filtered = trim(wp_strip_all_tags(base64_decode($name), true));
657 } else {
658 $filtered = '';
659 }
660
661 return preg_replace('/([\d]+)$/', '', $filtered);
662 }
663
664 /**
665 * Prepare filtered submenu
666 *
667 * @param string $menu
668 * @param array $submenu,
669 *
670 * @return array
671 * @access private
672 *
673 * @version 7.0.0
674 */
675 private function _get_submenu($parent_slug, $submenu)
676 {
677 $response = [];
678
679 if (array_key_exists($parent_slug, $submenu)) {
680 foreach ($submenu[$parent_slug] as $item) {
681 array_push($response, $this->_prepare_menu_item($item, false));
682 }
683 }
684
685 return $response;
686 }
687
688 }