PluginProbe
Advanced Access Manager – Access Governance for WordPress / 6.9.0
Advanced Access Manager – Access Governance for WordPress v6.9.0
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 / Service / Content.php
Content.php
1,513 lines 45.4 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 * Posts & Terms service
12 *
13 * @since 6.7.7 https://github.com/aamplugin/advanced-access-manager/issues/184
14 * @since 6.6.1 https://github.com/aamplugin/advanced-access-manager/issues/137
15 * @since 6.5.1 https://github.com/aamplugin/advanced-access-manager/issues/115
16 * @since 6.4.0 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/71
17 * @since 6.2.0 Enhanced HIDDEN option with more granular access controls
18 * @since 6.1.0 Multiple bug fixed
19 * @since 6.0.4 Fixed incompatibility with some quite aggressive plugins
20 * @since 6.0.2 Refactored the way access to posts is managed. No more pseudo caps
21 * aam|...
22 * @since 6.0.1 Bug fixing
23 * @since 6.0.0 Initial implementation of the class
24 *
25 * @package AAM
26 * @version 6.7.7
27 */
28 class AAM_Service_Content
29 {
30 use AAM_Core_Contract_RequestTrait,
31 AAM_Core_Contract_ServiceTrait;
32
33 /**
34 * Service alias
35 *
36 * Is used to get service instance if it is enabled
37 *
38 * @version 6.4.0
39 */
40 const SERVICE_ALIAS = 'content';
41
42 /**
43 * AAM configuration setting that is associated with the service
44 *
45 * @version 6.0.0
46 */
47 const FEATURE_FLAG = 'core.service.content.enabled';
48
49 /**
50 * Post view counter
51 *
52 * @version 6.0.0
53 */
54 const POST_COUNTER_DB_OPTION = 'aam_post_%s_access_counter';
55
56 /**
57 * Collection of post type caps
58 *
59 * This is a collection of post type capabilities for optimization reasons. It
60 * is used by filterMetaMaps method to determine if additional check needs to be
61 * perform
62 *
63 * @var array
64 *
65 * @access protected
66 * @version 6.0.2
67 */
68 protected $postTypeCaps = array(
69 'edit_post', 'edit_page', 'read_post', 'read_page', 'publish_post'
70 );
71
72 /**
73 * Constructor
74 *
75 * @return void
76 *
77 * @since 6.5.1 https://github.com/aamplugin/advanced-access-manager/issues/115
78 * @since 6.0.0 Initial implementation of the method
79 *
80 * @access protected
81 * @version 6.5.1
82 */
83 protected function __construct()
84 {
85 if (is_admin()) {
86 // Hook that initialize the AAM UI part of the service
87 if (AAM_Core_Config::get(self::FEATURE_FLAG, true)) {
88 add_action('aam_init_ui_action', function () {
89 AAM_Backend_Feature_Main_Post::register();
90 });
91
92 // Check if Access Manager metabox feature is enabled
93 $metaboxEnabled = AAM_Core_Config::get('ui.settings.renderAccessMetabox', true);
94
95 if ($metaboxEnabled && current_user_can('aam_manage_content')) {
96 // Make sure that all already registered taxonomies are hooked
97 foreach(get_taxonomies() as $taxonomy) {
98 add_action(
99 "{$taxonomy}_edit_form_fields",
100 array($this, 'renderAccessTermMetabox')
101 );
102 }
103
104 // Hook into still up-coming taxonomies down the pipeline
105 add_action('registered_taxonomy', function($taxonomy) {
106 add_action(
107 "{$taxonomy}_edit_form_fields",
108 array($this, 'renderAccessTermMetabox')
109 );
110 });
111
112 //register custom access control metabox
113 add_action('add_meta_boxes', array($this, 'registerAccessPostMetabox'));
114 }
115 }
116
117 // Hook that returns the detailed information about the nature of the
118 // service. This is used to display information about service on the
119 // Settings->Services tab
120 add_filter('aam_service_list_filter', function ($services) {
121 $services[] = array(
122 'title' => __('Posts & Terms', AAM_KEY),
123 'description' => __('Manage access to your website content for any user, role or visitor. This include access to posts, pages, media attachment, custom post types, categories, tags, custom taxonomies and terms.', AAM_KEY),
124 'setting' => self::FEATURE_FLAG
125 );
126
127 return $services;
128 }, 20);
129 }
130
131 if (AAM_Core_Config::get(self::FEATURE_FLAG, true)) {
132 $this->initializeHooks();
133 }
134 }
135
136 /**
137 * Render Access Manager metabox on term edit screen
138 *
139 * @param WP_Term $term
140 *
141 * @return void
142 *
143 * @access public
144 * @version 6.0.0
145 */
146 public function renderAccessTermMetabox($term)
147 {
148 if (is_a($term, 'WP_Term')) {
149 echo AAM_Backend_View::getInstance()->renderTermMetabox($term);
150 }
151 }
152
153 /**
154 * Register Access Manager metabox on post edit screen
155 *
156 * @return void
157 *
158 * @access public
159 * @version 6.0.0
160 */
161 public function registerAccessPostMetabox()
162 {
163 global $post;
164
165 if (is_a($post, 'WP_Post')) {
166 add_meta_box(
167 'aam-access-manager',
168 __('Access Manager', AAM_KEY),
169 function () {
170 global $post;
171
172 echo AAM_Backend_View::renderPostMetabox($post);
173 },
174 null,
175 'advanced',
176 'high'
177 );
178 }
179 }
180
181 /**
182 * Initialize Content service hooks
183 *
184 * @return void
185 *
186 * @since 6.4.0 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/71
187 * @since 6.1.0 Fixed the bug where `do_not_allow` capability was mapped to the
188 * list of post type capabilities
189 * @since 6.0.2 Removed invocation for the pseudo-cap mapping for post types
190 * @since 6.0.1 Fixed bug related to enabling commenting on all posts
191 * @since 6.0.0 Initial implementation of the method
192 *
193 * @access protected
194 * @version 6.4.0
195 */
196 protected function initializeHooks()
197 {
198 if (!is_admin()) {
199 // Password protected filter
200 add_filter('post_password_required', array($this, 'isPasswordRequired'), 10, 2);
201
202 // Manage password check expiration
203 add_filter('post_password_expires', array($this, 'checkPassExpiration'));
204
205 // Filter navigation pages & taxonomies
206 add_filter('wp_get_nav_menu_items', array($this, 'getNavigationMenu'), 999);
207
208 // Filter navigation pages & taxonomies
209 add_filter('get_pages', array($this, 'filterPages'), 999);
210
211 // Manage access to frontend posts & pages
212 add_action('wp', array($this, 'wp'), 999);
213 }
214
215 // Control post visibility
216 add_filter('posts_clauses_request', array($this, 'filterPostQuery'), 10, 2);
217
218 // Filter post content
219 add_filter('the_content', array($this, 'filterPostContent'), 999);
220
221 // Check if user has ability to perform certain task based on provided
222 // capability and meta data
223 add_filter('map_meta_cap', array($this, 'filterMetaMaps'), 999, 4);
224
225 // Get control over commenting stuff
226 add_filter('comments_open', function ($open, $id) {
227 $object = AAM::getUser()->getObject('post', $id);
228
229 // If Leave Comments option is defined then override the default status.
230 // Otherwise keep it as-is
231 if ($object->isDefined('comment')) {
232 $open = $object->isAllowedTo('comment');
233 }
234
235 return $open;
236 }, 10, 2);
237
238 // REST API action authorization. Triggered before call is dispatched
239 add_filter('rest_request_before_callbacks', array($this, 'beforeDispatch'), 10, 3);
240
241 // REST API. Control if user is allowed to publish content
242 add_action('registered_post_type', function ($post_type, $obj) {
243 add_filter("rest_pre_insert_{$post_type}", function ($post, $request) {
244 $status = (isset($request['status']) ? $request['status'] : null);
245
246 if (in_array($status, array('publish', 'future'), true)) {
247 if ($this->isAuthorizedToPublishPost($request['id']) === false) {
248 $post = new WP_Error(
249 'rest_cannot_publish',
250 __('You are not allowed to publish this content', AAM_KEY),
251 array('status' => rest_authorization_required_code())
252 );
253 }
254 }
255
256 return $post;
257 }, 10, 2);
258
259 // Populate the collection of post type caps
260 foreach ($obj->cap as $cap) {
261 if (
262 !in_array($cap, $this->postTypeCaps, true)
263 && ($cap !== 'do_not_allow')
264 ) {
265 $this->postTypeCaps[] = $cap;
266 }
267 }
268 }, 10, 2);
269
270 // Policy generation hook
271 add_filter(
272 'aam_generated_policy_filter', array($this, 'generatePolicy'), 10, 4
273 );
274
275 // Share post access settings conversion with add-ons and other third-party
276 // solutions
277 add_filter('aam_post_policy_generator_filter', function($list, $res, $opts) {
278 return array_merge(
279 $list, $this->_convertToPostStatements($res, $opts)
280 );
281 }, 10, 3);
282
283 // Service fetch
284 $this->registerService();
285 }
286
287 /**
288 * Generate Post policy statements
289 *
290 * @param array $policy
291 * @param string $resource_type
292 * @param array $options
293 * @param AAM_Core_Policy_Generator $generator
294 *
295 * @return array
296 *
297 * @access public
298 * @version 6.4.0
299 */
300 public function generatePolicy($policy, $resource_type, $options, $generator)
301 {
302 if ($resource_type === AAM_Core_Object_Post::OBJECT_TYPE) {
303 if (!empty($options)) {
304 $statements = array();
305
306 foreach($options as $id => $data) {
307 $parts = explode('|', $id);
308 $post = get_post($parts[0]);
309
310 if (is_a($post, 'WP_Post')) {
311 $resource = "Post:{$parts[1]}:{$post->post_name}";
312
313 $statements = array_merge(
314 $statements,
315 $this->_convertToPostStatements($resource, $data)
316 );
317 }
318 }
319
320 $policy['Statement'] = array_merge($policy['Statement'], $statements);
321 }
322 }
323
324 return $policy;
325 }
326
327 /**
328 * Convert post settings to policy format
329 *
330 * @param string $resource
331 * @param array $options
332 *
333 * @return array
334 *
335 * @since 6.4.0 Moved this method from AAM_Core_Policy_Generator
336 * @since 6.3.0 Fixed bug https://github.com/aamplugin/advanced-access-manager/issues/22
337 * @since 6.2.2 Fixed bug that caused fatal error for PHP lower than 7.0.0
338 * @since 6.2.0 Initial implementation of the method
339 *
340 * @access private
341 * @version 6.4.0
342 */
343 private function _convertToPostStatements($resource, $options)
344 {
345 $tree = (object) array(
346 'allowed' => array(),
347 'denied' => array(),
348 'statements' => array()
349 );
350
351 foreach($options as $option => $settings) {
352 // Compute Effect property
353 if (is_bool($settings)) {
354 $effect = ($settings === true ? 'denied' : 'allowed');
355 } else {
356 $effect = (!empty($settings['enabled']) ? 'denied' : 'allowed');
357 }
358
359 $action = null;
360
361 switch($option) {
362 case 'restricted':
363 $action = 'Read';
364 break;
365
366 case 'comment':
367 case 'edit':
368 case 'delete':
369 case 'publish':
370 case 'create':
371 $action = ucfirst($option);
372 break;
373
374 case 'hidden':
375 $item = array(
376 'Effect' => ($effect === 'denied' ? 'deny' : 'allow'),
377 'Action' => 'List',
378 'Resource' => $resource
379 );
380
381 $conditions = array();
382
383 if (is_array($settings)) {
384 if (!empty($settings['frontend'])) {
385 $conditions['(*boolean)${CALLBACK.is_admin}'] = false;
386 }
387 if (!empty($settings['backend'])) {
388 $conditions['(*boolean)${CALLBACK.is_admin}'] = true;
389 }
390 if (!empty($settings['api'])) {
391 $conditions['(*boolean)${CONST.REST_REQUEST}'] = true;
392 }
393 }
394
395 if (!empty($conditions)) {
396 $item['Condition']['Equals'] = $conditions;
397 }
398
399 $tree->statements[] = $item;
400 break;
401
402 case 'teaser':
403 $tree->statements[] = array(
404 'Effect' => ($effect === 'denied' ? 'deny' : 'allow'),
405 'Action' => 'Read',
406 'Resource' => $resource,
407 'Metadata' => array(
408 'Teaser' => array(
409 'Value' => esc_js($settings['message'])
410 )
411 )
412 );
413 break;
414
415 case 'limited':
416 $tree->statements[] = array(
417 'Effect' => ($effect === 'denied' ? 'deny' : 'allow'),
418 'Action' => 'Read',
419 'Resource' => $resource,
420 'Metadata' => array(
421 'Limited' => array(
422 'Threshold' => intval($settings['threshold'])
423 )
424 )
425 );
426 break;
427
428 case 'redirected':
429 $metadata = array(
430 'Type' => $settings['type'],
431 'Code' => intval(isset($settings['httpCode']) ? $settings['httpCode'] : 307)
432 );
433
434 if ($settings['type'] === 'page') {
435 $metadata['Id'] = intval($settings['destination']);
436 } elseif ($settings['type'] === 'url') {
437 $metadata['URL'] = trim($settings['destination']);
438 } elseif ($settings['type'] === 'callback') {
439 $metadata['Callback'] = trim($settings['destination']);
440 }
441
442 $tree->statements[] = array(
443 'Effect' => ($effect === 'denied' ? 'deny' : 'allow'),
444 'Action' => 'Read',
445 'Resource' => $resource,
446 'Metadata' => array(
447 'Redirect' => $metadata
448 )
449 );
450 break;
451
452 case 'protected':
453 $tree->statements[] = array(
454 'Effect' => ($effect === 'denied' ? 'deny' : 'allow'),
455 'Action' => 'Read',
456 'Resource' => $resource,
457 'Metadata' => array(
458 'Password' => array(
459 'Value' => $settings['password']
460 )
461 )
462 );
463 break;
464
465 case 'ceased':
466 $tree->statements[] = array(
467 'Effect' => ($effect === 'denied' ? 'deny' : 'allow'),
468 'Action' => 'Read',
469 'Resource' => $resource,
470 'Condition' => array(
471 'Greater' => array(
472 '(*int)${DATETIME.U}' => intval($settings['after'])
473 )
474 )
475 );
476 break;
477
478 default:
479 do_action(
480 'aam_post_option_to_policy_action',
481 $resource,
482 $option,
483 $effect,
484 $settings,
485 $tree
486 );
487 break;
488 }
489
490 if ($action !== null) {
491 if ($effect === 'allowed') {
492 $tree->allowed[] = $resource . ':' . $action;
493 } else {
494 $tree->denied[] = $resource . ':' . $action;
495 }
496 }
497 }
498
499 // Finally prepare the consolidated statements
500 if (!empty($tree->denied)) {
501 $tree->statements[] = array(
502 'Effect' => 'deny',
503 'Resource' => $tree->denied
504 );
505 }
506
507 if (!empty($tree->allowed)) {
508 $tree->statements[] = array(
509 'Effect' => 'allow',
510 'Resource' => $tree->allowed
511 );
512 }
513
514 return $tree->statements;
515 }
516
517 /**
518 * Authorize RESTful action before it is dispatched by RESTful Server
519 *
520 * @param mixed $response
521 * @param object $handler
522 * @param object $request
523 *
524 * @return mixed
525 *
526 * @since 6.6.1 https://github.com/aamplugin/advanced-access-manager/issues/137
527 * @since 6.1.0 Fixed bug that causes fatal error when callback is Closure
528 * @since 6.0.2 Making sure that get_post returns actual post object
529 * @since 6.0.0 Initial implementation of the method
530 *
531 * @access public
532 * @version 6.6.1
533 */
534 public function beforeDispatch($response, $handler, $request)
535 {
536 // Register hooks that check post access
537 foreach (get_post_types(array('show_in_rest' => true)) as $type) {
538 add_filter("rest_prepare_{$type}", array($this, 'authPostAccess'), 10, 3);
539 }
540
541 // Override the password authentication handling ONLY for posts
542 $attrs = $request->get_attributes();
543 $callback = $attrs['callback'];
544 $controller = (is_array($callback) ? array_shift($callback) : null);
545
546 if (is_a($controller, 'WP_REST_Posts_Controller')) {
547 $post = get_post($request['id']);
548 $has_pass = isset($request['password']);
549
550 // Honor the manually defined password on the post
551 if (is_a($post, 'WP_Post')
552 && empty($post->post_password)
553 && $has_pass
554 && ($request->get_method() === 'GET')
555 ) {
556 $request['_password'] = $request['password'];
557 unset($request['password']);
558 }
559 }
560
561 return $response;
562 }
563
564 /**
565 * Check if post is allowed to be viewed
566 *
567 * @param WP_REST_Response $response
568 * @param WP_Post $post
569 * @param WP_REST_Request $request
570 *
571 * @access public
572 * @version 6.0.0
573 */
574 public function authPostAccess($response, $post, $request)
575 {
576 $object = AAM::getUser()->getObject(
577 AAM_Core_Object_Post::OBJECT_TYPE,
578 $post->ID
579 );
580
581 $auth = $this->isAuthorizedToReadPost(
582 $object,
583 (isset($request['_password']) ? $request['_password'] : null)
584 );
585
586 if (is_wp_error($auth)) {
587 $data = $auth->get_error_data();
588 $data['reason'] = $auth->get_error_message();
589 $data['code'] = $auth->get_error_code();
590 $status = $data['status'];
591
592 unset($data['status']); // No need to duplicate the status code
593
594 $response = new WP_REST_Response($data, $status);
595
596 if ($auth->get_error_code() === 'post_access_redirected') {
597 $response->set_headers(array('Location' => $data['url']));
598 }
599 } else {
600 $this->incrementPostReadCounter($object);
601 }
602
603 return $response;
604 }
605
606 /**
607 * Check if post is password protected
608 *
609 * This callback is used by the Frontend to determine if current post requires
610 * password in order to see its content
611 *
612 * @param boolean $result
613 * @param WP_Post $post
614 *
615 * @return boolean
616 *
617 * @access public
618 * @version 6.0.0
619 */
620 public function isPasswordRequired($result, $post)
621 {
622 // Honor the manually set password on the post
623 if (($result === false) && is_a($post, 'WP_Post')) {
624 $check = $this->checkPostPassword(
625 AAM::getUser()->getObject('post', $post->ID)
626 );
627
628 $result = is_wp_error($check);
629 }
630
631 return $result;
632 }
633
634 /**
635 * Redefine entered password TTL
636 *
637 * @param int $expire
638 *
639 * @return int
640 *
641 * @access public
642 * @version 6.0.0
643 */
644 public function checkPassExpiration($expire)
645 {
646 $overwrite = AAM_Core_Config::get('feature.post.password.expires', null);
647
648 if (!is_null($overwrite)) {
649 $expire = ($overwrite ? time() + strtotime($overwrite) : 0);
650 }
651
652 return $expire;
653 }
654
655 /**
656 * Filter Navigation menu
657 *
658 * @param array $pages
659 *
660 * @return array
661 *
662 * @since 6.2.0 Enhanced HIDDEN option to be more granular
663 * @since 6.0.0 Initial implementation of the method
664 *
665 * @access public
666 * @version 6.2.0
667 */
668 public function getNavigationMenu($pages)
669 {
670 if (is_array($pages)) {
671 foreach ($pages as $i => $page) {
672 if (in_array($page->type, array('post_type', 'custom'), true)) {
673 $object = AAM::getUser()->getObject('post', $page->object_id);
674 if ($this->_isHidden($object->getOption())) {
675 unset($pages[$i]);
676 }
677 }
678 }
679 }
680
681 return $pages;
682 }
683
684 /**
685 * Main frontend access control hook
686 *
687 * @return void
688 *
689 * @access public
690 * @global WP_Query $wp_query
691 * @version 6.0.0
692 */
693 public function wp()
694 {
695 global $wp_query;
696
697 if ($wp_query->is_single || $wp_query->is_page) {
698 $post = AAM_Core_API::getCurrentPost();
699
700 if (is_a($post, 'AAM_Core_Object_Post')) {
701 $error = $this->isAuthorizedToReadPost($post);
702
703 if (is_wp_error($error)) {
704 if ($error->get_error_code() === 'post_access_redirected') {
705 AAM_Core_Redirect::execute('url', $error->get_error_data());
706 } elseif ($error->get_error_code() !== 'post_access_protected') {
707 wp_die($error->get_error_message(), 'aam_access_denied');
708 }
709 } else {
710 $this->incrementPostReadCounter($post);
711 }
712 }
713 }
714 }
715
716 /**
717 * Filter posts from the list
718 *
719 * @param array $pages
720 *
721 * @return array
722 *
723 * @since 6.2.0 Enhanced HIDDEN option to be more granular
724 * @since 6.0.0 Initial implementation of the method
725 *
726 * @access public
727 * @version 6.2.0
728 */
729 public function filterPages($pages)
730 {
731 if (is_array($pages)) {
732 $current = AAM_Core_API::getCurrentPost();
733
734 foreach ($pages as $i => $post) {
735 if ($current && ($current->ID === $post->ID)) {
736 continue;
737 }
738
739 $object = AAM::getUser()->getObject('post', $post->ID);
740 if ($this->_isHidden($object->getOption())) {
741 unset($pages[$i]);
742 }
743 }
744
745 $pages = array_values($pages);
746 }
747
748 return $pages;
749 }
750
751 /**
752 * After post SELECT query
753 *
754 * @param array $clauses
755 * @param WP_Query $wpQuery
756 *
757 * @return array
758 *
759 * @since 6.0.4 Fixed incompatibility with some quite aggressive plugins that
760 * mutate global state of the WP_Query args
761 * @since 6.0.0 Initial implementation of the method
762 *
763 * @access public
764 * @version 6.0.4
765 */
766 public function filterPostQuery($clauses, $wp_query)
767 {
768 static $executing = false;
769
770 if (!$wp_query->is_singular && !$executing) {
771 $executing = true;
772
773 $object = AAM::getUser()->getObject(
774 AAM_Core_Object_Visibility::OBJECT_TYPE
775 );
776
777 $query = $this->preparePostQuery($object->getSegment('post'), $wp_query);
778
779 $clauses['where'] .= apply_filters(
780 'aam_content_visibility_where_clause_filter',
781 $query,
782 $wp_query
783 );
784
785 $executing = false;
786 }
787
788 return $clauses;
789 }
790
791 /**
792 * Prepare post query
793 *
794 * @param array $visibility
795 * @param WP_Query $wpQuery
796 *
797 * @return string
798 *
799 * @since 6.2.0 Enhanced HIDDEN option to be more granular
800 * @since 6.0.0 Initial implementation of the method
801 *
802 * @access protected
803 * @global WPDB $wpdb
804 * @version 6.2.0
805 */
806 protected function preparePostQuery($visibility, $wpQuery)
807 {
808 global $wpdb;
809
810 $postTypes = $this->getQueryingPostType($wpQuery);
811 $excluded = array();
812
813 foreach ($visibility as $id => $access) {
814 $chunks = explode('|', $id);
815
816 if (in_array($chunks[1], $postTypes, true) && $this->_isHidden($access)) {
817 $excluded[] = $chunks[0];
818 }
819 }
820
821 if (!empty($excluded)) {
822 $query = " AND {$wpdb->posts}.ID NOT IN (" . implode(',', $excluded) . ")";
823 } else {
824 $query = '';
825 }
826
827 return $query;
828 }
829
830 /**
831 * Determine if object is hidden based on access settings
832 *
833 * @param array $options
834 *
835 * @return boolean
836 *
837 * @access private
838 * @version 6.2.0
839 */
840 private function _isHidden($options)
841 {
842 $hidden = false;
843
844 // Determine current area
845 if (is_admin()) {
846 $area = 'backend';
847 } elseif (defined('REST_REQUEST') && REST_REQUEST) {
848 $area = 'api';
849 } else {
850 $area = 'frontend';
851 }
852
853 if (isset($options['hidden'])) {
854 if (
855 is_array($options['hidden'])
856 && !empty($options['hidden']['enabled'])
857 && !empty($options['hidden'][$area])
858 ) {
859 $hidden = true;
860 } elseif (is_bool($options['hidden']) && ($options['hidden'] === true)) {
861 $hidden = true;
862 }
863 }
864
865 return $hidden;
866 }
867
868 /**
869 * Get querying post type
870 *
871 * @param WP_Query $wpQuery
872 *
873 * @return array
874 *
875 * @since 6.0.3 Fetch list of all possible post types
876 * @since 6.0.0 Initial implementation of the method
877 *
878 * @access protected
879 * @version 6.0.3
880 */
881 protected function getQueryingPostType($wpQuery)
882 {
883 if (!empty($wpQuery->query['post_type'])) {
884 $postType = $wpQuery->query['post_type'];
885 } elseif (!empty($wpQuery->query_vars['post_type'])) {
886 $postType = $wpQuery->query_vars['post_type'];
887 } elseif ($wpQuery->is_attachment) {
888 $postType = 'attachment';
889 } elseif ($wpQuery->is_page) {
890 $postType = 'page';
891 } else {
892 $postType = 'any';
893 }
894
895 if ($postType === 'any') {
896 $postType = array_keys(get_post_types(array(), 'names'));
897 }
898
899 return (array) $postType;
900 }
901
902 /**
903 * Filter post content
904 *
905 * @param string $content
906 *
907 * @return string
908 *
909 * @access public
910 * @version 6.0.0
911 */
912 public function filterPostContent($content)
913 {
914 $post = AAM_Core_API::getCurrentPost();
915
916 if (is_a($post, 'AAM_Core_Object_Post') && $post->has('teaser')) {
917 $teaser = $post->get('teaser');
918
919 if (!empty($teaser['message'])) {
920 $message = $teaser['message'];
921 } else {
922 $message = __('[No teaser message provided]', AAM_KEY);
923 }
924
925 // Replace the [excerpt] placeholder with posts excerpt and do
926 // short-code evaluation
927 $content = do_shortcode(
928 str_replace('[excerpt]', $post->post_excerpt, $message)
929 );
930 }
931
932 return $content;
933 }
934
935 /**
936 * Check user capability
937 *
938 * This is a hack function that add additional layout on top of WordPress
939 * core functionality. Based on the capability passed in the $args array as
940 * "0" element, it performs additional check on user's capability to manage
941 * post, users etc.
942 *
943 * @param array $caps
944 * @param string $cap
945 * @param int $user_id
946 * @param array $args
947 *
948 * @return array
949 *
950 * @since 6.7.7 https://github.com/aamplugin/advanced-access-manager/issues/184
951 * @since 6.1.0 Added internal cache to optimize performance for posts that no
952 * longer exist but still referenced one way or another
953 * @since 6.0.2 Completely rewrote this method to fixed loop caused by mapped
954 * aam|... post type capability
955 * @since 6.0.0 Initial implementation of the method
956 *
957 * @access public
958 * @version 6.7.7
959 */
960 public function filterMetaMaps($caps, $cap, $user_id, $args)
961 {
962 // Internal cache to optimize search for no longer existing posts
963 static $post_cache = array();
964
965 global $post;
966
967 // For optimization reasons, check only caps that belong to registered post
968 // types
969 if (in_array($cap, $this->postTypeCaps, true)) {
970 // Critical part of the implementation. We do not know ahead what
971 // capability is responsible for what action when it comes to post types.
972 if (isset($args[0]) && is_scalar($args[0])) {
973 $objectId = intval($args[0]);
974 } elseif (is_a($post, 'WP_Post')) {
975 $objectId = $post->ID;
976 } else {
977 $objectId = null;
978 }
979
980 // If object ID is not empty, then, potentially we are checking for perms
981 // to perform one of the action against a post
982 if (!empty($objectId) && !in_array($objectId, $post_cache, true)) {
983 $requested = get_post($objectId);
984
985 if (is_a($requested, 'WP_Post')) {
986 $post_type = get_post_type_object($requested->post_type);
987
988 if (is_a($post_type, 'WP_Post_Type')) {
989 $caps = $this->__mapPostTypeCaps(
990 $post_type,
991 $cap,
992 $caps,
993 $requested,
994 $args
995 );
996 }
997 } else {
998 $post_cache[] = $objectId;
999 }
1000 }
1001 }
1002
1003 return $caps;
1004 }
1005
1006 /**
1007 * Map post type capability based on set permissions
1008 *
1009 * @param WP_Post_Type $post_type
1010 * @param string $cap
1011 * @param array $caps
1012 * @param WP_Post $post
1013 * @param array $args
1014 *
1015 * @return array
1016 *
1017 * @access private
1018 * @version 6.0.2
1019 */
1020 private function __mapPostTypeCaps(
1021 WP_Post_Type $post_type,
1022 $cap,
1023 $caps,
1024 WP_Post $post,
1025 $args
1026 ) {
1027
1028 // Cover the scenario when $cap is not part of the post type capabilities
1029 // There is a bug in the WP core when user is checked for 'publish_post'
1030 // capability
1031 $primitive_cap = array_search($cap, (array) $post_type->cap);
1032
1033 if ($primitive_cap === false) {
1034 $primitive_cap = $cap;
1035 }
1036
1037 switch ($primitive_cap) {
1038 case 'edit_post':
1039 case 'edit_page':
1040 // Cover the scenario when user uses Bulk Action or Quick Edit to
1041 // change the Status to Published and post is not allowed to be
1042 // published
1043 $action = AAM_Core_Request::request('action');
1044 $status = AAM_Core_Request::request('_status');
1045
1046 if (
1047 in_array($action, array('edit', 'inline-save', true))
1048 && $status === 'publish'
1049 ) {
1050 $caps = $this->mapPublishPostCaps($caps, $post->ID);
1051 } else {
1052 $caps = $this->mapEditPostCaps($caps, $post->ID);
1053 }
1054 break;
1055
1056 case 'delete_post':
1057 case 'delete_page':
1058 $caps = $this->mapDeletePostCaps($caps, $post->ID);
1059 break;
1060
1061 case 'read_post':
1062 case 'read_page':
1063 $password = (isset($args[1]) ? $args[1] : null);
1064 $caps = $this->mapReadPostCaps($caps, $post->ID, $password);
1065 break;
1066
1067 case 'publish_post':
1068 case 'publish_page':
1069 case 'publish_posts':
1070 $caps = $this->mapPublishPostCaps($caps, $post->ID);
1071 break;
1072
1073 default:
1074 break;
1075 }
1076
1077 return $caps;
1078 }
1079
1080 /**
1081 * Mutate capability meta map based on ability to publish the post
1082 *
1083 * @param array $caps
1084 * @param WP_Post|int $post
1085 *
1086 * @return array
1087 *
1088 * @access protected
1089 * @version 6.0.0
1090 */
1091 protected function mapPublishPostCaps($caps, $post)
1092 {
1093 if ($this->isAuthorizedToPublishPost($post) === false) {
1094 $caps[] = 'do_not_allow';
1095 }
1096
1097 return $caps;
1098 }
1099
1100 /**
1101 * Authorize the post publishing action
1102 *
1103 * @param WP_Post|int $post
1104 *
1105 * @return boolean
1106 *
1107 * @access public
1108 * @version 6.0.0
1109 */
1110 public function isAuthorizedToPublishPost($post)
1111 {
1112 return AAM::getUser()->getObject(
1113 'post',
1114 (is_a($post, 'WP_Post') ? $post->ID : $post)
1115 )->isAllowedTo('publish');
1116 }
1117
1118 /**
1119 * Mutate capability meta map based on ability to edit/update the post
1120 *
1121 * @param array $caps
1122 * @param WP_Post|int $post
1123 *
1124 * @return array
1125 *
1126 * @access protected
1127 * @version 6.0.0
1128 */
1129 protected function mapEditPostCaps($caps, $post)
1130 {
1131 if ($this->isAuthorizedToEditPost($post) === false) {
1132 $caps[] = 'do_not_allow';
1133 }
1134
1135 return $caps;
1136 }
1137
1138 /**
1139 * Check if current user is allowed to edit post
1140 *
1141 * Draft posts have to be omitted to avoid "egg-chicken" problem
1142 *
1143 * @param WP_Post|int $post
1144 *
1145 * @return boolean
1146 *
1147 * @access public
1148 * @version 6.0.0
1149 */
1150 public function isAuthorizedToEditPost($post)
1151 {
1152 $object = AAM::getUser()->getObject(
1153 'post',
1154 (is_a($post, 'WP_Post') ? $post->ID : $post)
1155 );
1156 $isDraft = $object->post_status === 'auto-draft';
1157
1158 return $isDraft || $object->isAllowedTo('edit');
1159 }
1160
1161 /**
1162 * Mutate capability meta map based on ability to trash/delete the post
1163 *
1164 * @param array $caps
1165 * @param WP_Post|int $post
1166 *
1167 * @return array
1168 *
1169 * @access protected
1170 * @version 6.0.0
1171 */
1172 protected function mapDeletePostCaps($caps, $post)
1173 {
1174 if ($this->isAuthorizedToDeletePost($post) === false) {
1175 $caps[] = 'do_not_allow';
1176 }
1177
1178 return $caps;
1179 }
1180
1181 /**
1182 * Check if current user is authorized to trash or permanently delete the post
1183 *
1184 * @param WP_Post|int $post
1185 *
1186 * @return boolean
1187 *
1188 * @access public
1189 * @version 6.0.0
1190 */
1191 public function isAuthorizedToDeletePost($post)
1192 {
1193 return AAM::getUser()->getObject(
1194 'post',
1195 (is_a($post, 'WP_Post') ? $post->ID : $post)
1196 )->isAllowedTo('delete');
1197 }
1198
1199 /**
1200 * Mutate capability meta map based on ability to edit/update the post
1201 *
1202 * @param array $caps
1203 * @param WP_Post|int $post
1204 * @param string $password
1205 *
1206 * @return array
1207 *
1208 * @access protected
1209 * @version 6.0.0
1210 */
1211 protected function mapReadPostCaps($caps, $post, $password = null)
1212 {
1213 if ($this->isAuthorizedToReadPost($post, $password) !== true) {
1214 $caps[] = 'do_not_allow';
1215 }
1216
1217 return $caps;
1218 }
1219
1220 /**
1221 * Increment user view counter is tracking is defined
1222 *
1223 * @param AAM_Core_Object_Post $post
1224 *
1225 * @return void
1226 *
1227 * @access protected
1228 * @version 6.0.0
1229 */
1230 protected function incrementPostReadCounter($post)
1231 {
1232 if (is_user_logged_in() && $post->is('limited')) {
1233 $option = sprintf(self::POST_COUNTER_DB_OPTION, $post->ID);
1234 $counter = intval(get_user_option($option, get_current_user_id()));
1235 update_user_option(get_current_user_id(), $option, ++$counter);
1236 }
1237 }
1238
1239 /**
1240 * Check if current user is authorized to read the post
1241 *
1242 * If post requires, password, also path this as the second optional parameter
1243 *
1244 * @param mixed $post
1245 * @param string $password
1246 *
1247 * @return boolean|WP_Error
1248 *
1249 * @access public
1250 * @version 6.0.0
1251 */
1252 public function isAuthorizedToReadPost($post, $password = null)
1253 {
1254 if (is_a($post, 'AAM_Core_Object_Post')) {
1255 $object = $post;
1256 } else {
1257 $object = AAM::getUser()->getObject(
1258 'post',
1259 (is_a($post, 'WP_Post') ? $post->ID : $post)
1260 );
1261 }
1262
1263 // Prepare the pipeline of steps that AAM core will perform to check post's
1264 // accessibility
1265 $pipeline = apply_filters('aam_post_read_access_pipeline_filter', array(
1266 // Step #1. Check if access expired to the post
1267 array($this, 'checkPostExpiration'),
1268 // Step #2. Check if user has access to read the post
1269 array($this, 'checkPostReadAccess'),
1270 // Step #3. Check if counter exceeded max allowed views
1271 array($this, 'checkPostLimitCounter'),
1272 // Step #4. Check if redirect is defined for the post
1273 array($this, 'checkPostRedirect'),
1274 // Step #5. Check if post is password protected
1275 array($this, 'checkPostPassword')
1276 ));
1277
1278 // Execute the collection of steps and stop when first restriction captured
1279 $result = true;
1280 foreach ($pipeline as $callback) {
1281 $result = call_user_func($callback, $object, $password);
1282
1283 if (is_wp_error($result)) {
1284 break;
1285 }
1286 }
1287
1288 return $result;
1289 }
1290
1291 /**
1292 * Check CEASED access option
1293 *
1294 * If access is expired, return WP_Error object with the reason
1295 *
1296 * @param AAM_Core_Object_Post $post
1297 *
1298 * @return boolean|WP_Error
1299 *
1300 * @access public
1301 * @version 6.0.0
1302 */
1303 public function checkPostExpiration(AAM_Core_Object_Post $post)
1304 {
1305 $result = true;
1306
1307 if ($post->is('ceased')) {
1308 $ceased = $post->get('ceased');
1309 $now = (new DateTime('now', new DateTimeZone('UTC')))->getTimestamp();
1310
1311 if ($ceased['after'] <= $now) {
1312 $result = new WP_Error(
1313 'post_access_expired',
1314 'User is unauthorized to access this post. Access Expired.',
1315 array('status' => 401)
1316 );
1317 }
1318 }
1319
1320 return $result;
1321 }
1322
1323 /**
1324 * Check RESTRICTED options
1325 *
1326 * If access is explicitly restricted, return WP_Error object with the reason
1327 *
1328 * @param AAM_Core_Object_Post $post
1329 *
1330 * @return boolean|WP_Error
1331 *
1332 * @access public
1333 * @version 6.0.0
1334 */
1335 public function checkPostReadAccess(AAM_Core_Object_Post $post)
1336 {
1337 $result = true;
1338
1339 if ($post->is('restricted')) {
1340 $result = new WP_Error(
1341 'post_access_restricted',
1342 "User is unauthorized to access this post. Access denied.",
1343 array('status' => 401)
1344 );
1345 }
1346
1347 return $result;
1348 }
1349
1350 /**
1351 * Check LIMITED access option
1352 *
1353 * The counter is stored per each user for every individual post that has LIMITED
1354 * access option enabled. The WP_Error object will be returned if access counter
1355 * exceeded maximum allowed threshold.
1356 *
1357 * @param AAM_Core_Object_Post $post
1358 *
1359 * @return boolean|WP_Error
1360 *
1361 * @since 6.2.0 Simplified implementation
1362 * @since 6.0.0 Initial implementation of the method
1363 *
1364 * @access public
1365 * @version 6.0.0
1366 */
1367 public function checkPostLimitCounter(AAM_Core_Object_Post $post)
1368 {
1369 $result = true;
1370
1371 // Check current access counter only for authenticated users
1372 if (is_user_logged_in() && $post->is('limited')) {
1373 $limited = $post->get('limited');
1374
1375 $option = sprintf(self::POST_COUNTER_DB_OPTION, $post->ID);
1376 $counter = intval(get_user_option($option, get_current_user_id()));
1377
1378 if ($counter >= $limited['threshold']) {
1379 $result = new WP_Error(
1380 'post_access_exceeded_limit',
1381 "User exceeded allowed access number. Access denied.",
1382 array('status' => 401)
1383 );
1384 }
1385 }
1386
1387 return $result;
1388 }
1389
1390 /**
1391 * Check REDIRECTED access option
1392 *
1393 * Do not allow direct access to the post and return WP_Error object with details
1394 * for the location where user has to be redirected.
1395 *
1396 * @param AAM_Core_Object_Post $post
1397 *
1398 * @return boolean|WP_Error
1399 *
1400 * @access public
1401 * @version 6.0.0
1402 */
1403 public function checkPostRedirect(AAM_Core_Object_Post $post)
1404 {
1405 $result = true;
1406
1407 if ($post->is('redirected')) {
1408 $redirect = $post->get('redirected');
1409 $location = null;
1410
1411 switch ($redirect['type']) {
1412 case 'page':
1413 $location = get_page_link($redirect['destination']);
1414 break;
1415
1416 case 'login':
1417 $location = add_query_arg(
1418 'reason',
1419 'restricted',
1420 wp_login_url($this->getFromServer('REQUEST_URI'))
1421 );
1422 break;
1423
1424 case 'url':
1425 $location = $redirect['destination'];
1426 break;
1427
1428 case 'callback':
1429 if (is_callable($redirect['destination'])) {
1430 $location = call_user_func($redirect['destination'], $post);
1431 } else {
1432 _doing_it_wrong(
1433 __CLASS__ . '::' . __METHOD__,
1434 'Callback is not invocable',
1435 AAM_VERSION
1436 );
1437 }
1438 break;
1439
1440 default:
1441 break;
1442 }
1443
1444 $result = new WP_Error(
1445 'post_access_redirected',
1446 'Direct access is not allowed. Follow the provided redirect rule.',
1447 array(
1448 'url' => $location,
1449 'status' => $redirect['httpCode']
1450 )
1451 );
1452 }
1453
1454 return $result;
1455 }
1456
1457 /**
1458 * Check PASSWORD PROTECTED access option
1459 *
1460 * If post has password set, return WP_Error so the application can do further
1461 * authorization process.
1462 *
1463 * @param AAM_Core_Object_Post $post
1464 * @param string $password
1465 *
1466 * @return boolean|WP_Error
1467 *
1468 * @access public
1469 * @version 6.0.0
1470 */
1471 public function checkPostPassword(AAM_Core_Object_Post $post, $password = null)
1472 {
1473 $result = true;
1474
1475 if ($post->is('protected')) {
1476 // Get password values
1477 $protected = $post->get('protected');
1478
1479 // If password is empty or not provided, try to read it from the cookie.
1480 // This is the default WordPress behavior when it comes to password
1481 // protected posts/pages
1482 if (empty($password)) {
1483 $password = wp_unslash(
1484 $this->getFromCookie('wp-postpass_' . COOKIEHASH)
1485 );
1486
1487 $isMatched = AAM_Core_API::prepareHasher()->CheckPassword(
1488 $protected['password'],
1489 $password
1490 );
1491 } else {
1492 $isMatched = $protected['password'] === $password;
1493 }
1494
1495 if ($isMatched === false) {
1496 $result = new WP_Error(
1497 'post_access_protected',
1498 'The post is password protected. Invalid password provided.',
1499 array(
1500 'status' => 401
1501 )
1502 );
1503 }
1504 }
1505
1506 return $result;
1507 }
1508
1509 }
1510
1511 if (defined('AAM_KEY')) {
1512 AAM_Service_Content::bootstrap();
1513 }