actorAccess = $actorAccess; $this->replacementContent = $replacementContent; $this->preferAdvancedMode = $preferAdvancedMode; if ( $accessProtectionSettings ) { $this->accessProtectionSettings = $accessProtectionSettings; } else { $this->accessProtectionSettings = new AccessProtectionSettings(); } } /** * @param Actor $actor * @param Action $action * @param ContentItemPolicy|null $childObjectPolicy * @param mixed $objectId ID of the object that this policy is evaluated for. Usually a post ID. * @return EvaluationResult|null */ public function evaluate(Actor $actor, Action $action, ?ContentItemPolicy $childObjectPolicy = null, $objectId = null) { $bestMatchEffect = null; $bestMatchPriority = -1; $actionName = $action->getName(); $flattedActors = $actor->flatten(); //flatten() returns actors sorted by priority, from highest to lowest. foreach ($flattedActors as $componentActor) { $actorId = $componentActor->getId(); $priority = $componentActor->getPriority(); //If we already have a match with higher priority than this, we can skip the rest. if ( $bestMatchEffect && ($priority < $bestMatchPriority) ) { break; } if ( isset($this->actorAccess[$actorId][$actionName]) ) { $effect = $this->actorAccess[$actorId][$actionName] ? self::EFFECT_ALLOW : self::EFFECT_DENY; //Prefer "allow" over "deny". An actor with a higher priority could override an "allow" //with a "deny", but if we already got this far, we don't have a match with a higher priority. if ( $effect === self::EFFECT_ALLOW ) { $bestMatchEffect = self::EFFECT_ALLOW; break; } $bestMatchEffect = $effect; $bestMatchPriority = $priority; } } if ( $bestMatchEffect === null ) { return null; } return new EvaluationResult( $bestMatchEffect, $actor, $action, $this, $childObjectPolicy, $objectId ); } public function getReplacementContent() { if ( $this->replacementContent !== '' ) { return $this->replacementContent; } return '
' . static::getDefaultReplacementText() . '
'; } public static function getDefaultReplacementText() { return _x( "You don't have access to this content.", 'content permissions: default replacement text', 'admin-menu-editor' ); } /** * @return AccessProtection|null */ public function getDirectAccessProtection() { return $this->accessProtectionSettings->getActiveProtection(); } /** * Iterates over all entries in the policy that match one of the given actions and * calls the callback for each entry. * * The callback receives the following parameters: * - string $actorId * - string $actionName * - bool $isAllowed * * @param Action[] $actions * @param callable $callback * @return void */ public function forEachActionEntry($actions, $callback) { if ( !isset($this->actorAccess) ) { return; } foreach ($actions as $action) { $actionName = $action->getName(); foreach ($this->actorAccess as $actorId => $actionSettings) { if ( isset($actionSettings[$actionName]) ) { $callback($actorId, $actionName, $actionSettings[$actionName]); } } } } /** * @param Actor $actor * @param Action $action * @param bool|null $isAllowed * @return void */ public function setActorPermission(Actor $actor, Action $action, $isAllowed) { $actorId = $actor->getId(); $actionName = $action->getName(); if ( $isAllowed === null ) { unset($this->actorAccess[$actorId][$actionName]); if ( empty($this->actorAccess[$actorId]) ) { unset($this->actorAccess[$actorId]); } return; } if ( !isset($this->actorAccess[$actorId]) ) { $this->actorAccess[$actorId] = []; } $this->actorAccess[$actorId][$actionName] = $isAllowed; } /** * Check if the policy has a custom permission setting for the given actor and action. * * @param Actor $actor * @param Action $action * @return bool */ public function hasPermissionSettingFor(Actor $actor, Action $action) { return isset($this->actorAccess[$actor->getId()][$action->getName()]); } /** @noinspection PhpLanguageLevelInspection */ #[\ReturnTypeWillChange] public function jsonSerialize() { $data = []; //Only serialize non-empty arrays. $actorAccess = array_filter($this->actorAccess, function ($actions) { return !empty($actions); }); if ( !empty($actorAccess) ) { $data['actorAccess'] = $actorAccess; } if ( $this->replacementContent !== '' ) { $data['replacementContent'] = $this->replacementContent; } if ( $this->accessProtectionSettings ) { $serializedProtection = $this->accessProtectionSettings->jsonSerialize(); if ( !empty($serializedProtection) ) { $data['accessProtection'] = $serializedProtection; } } if ( $this->preferAdvancedMode ) { $data['preferAdvancedMode'] = true; } return $data; } public static function fromArray($properties) { $actorAccess = []; if ( isset($properties['actorAccess']) && is_array($properties['actorAccess']) ) { $actorAccess = $properties['actorAccess']; } $replacementContent = isset($properties['replacementContent']) ? strval($properties['replacementContent']) : ''; $accessProtectionSettings = null; if ( isset($properties['accessProtection']) ) { $accessProtectionSettings = AccessProtectionSettings::fromArray($properties['accessProtection']); } $preferAdvancedMode = isset($properties['preferAdvancedMode']) && $properties['preferAdvancedMode']; return new static($actorAccess, $replacementContent, $accessProtectionSettings, $preferAdvancedMode); } } class PolicyStore { const POST_POLICY_META_KEY = '_ame_cpe_post_policy'; const TERM_POLICY_META_KEY = '_ame_cpe_term_policy'; const HIDDEN_ITEM_LOOKUP_OPTION = 'ame_cpe_restricted_items'; /** * @var RestrictedPostLookup|null */ private $restrictedPostLookup = null; /** * @var ActionRegistry */ private $actionRegistry; /** * @var array Composite key => array of post IDs. */ private $hiddenListItemCache = []; public function __construct(ActionRegistry $actionRegistry) { $this->actionRegistry = $actionRegistry; //Remove deleted posts from the lookup. add_action('deleted_post', [$this, 'forgetDeletedPost']); } /** * @param string $objectType * @param \WP_Post|\WP_Term $nativeObject * @param string|null $postType For terms, the post type to get the policy for. * @return ContentItemPolicy|null */ public function getObjectPolicy(string $objectType, $nativeObject, ?string $postType = null): ?ContentItemPolicy { switch ($objectType) { case Action::OBJECT_TYPE_POST: if ( !($nativeObject instanceof \WP_Post) ) { return null; } return $this->getPostPolicy($nativeObject->ID); case Action::OBJECT_TYPE_TERM: if ( !($nativeObject instanceof \WP_Term) ) { return null; } return $this->getTermPolicy($nativeObject->term_id, $postType); default: return null; } } public function setObjectPolicy( string $objectType, $nativeObject, ContentItemPolicy $policy, ?string $postType = null ): void { switch ($objectType) { case Action::OBJECT_TYPE_POST: if ( !($nativeObject instanceof \WP_Post) ) { return; } $this->setPostPolicy($nativeObject->ID, $policy); break; case Action::OBJECT_TYPE_TERM: if ( !($nativeObject instanceof \WP_Term) ) { return; } $this->setTermPolicy($nativeObject->term_id, $policy, $postType); break; } } /** * @param int $postId * @return ContentItemPolicy|null */ public function getPostPolicy($postId) { $serializedPolicy = get_post_meta($postId, self::POST_POLICY_META_KEY, true); if ( !empty($serializedPolicy) ) { $properties = json_decode($serializedPolicy, true); if ( is_array($properties) ) { return ContentItemPolicy::fromArray($properties); } } return null; } /** * @param int $postId * @param ContentItemPolicy $policy * @return void */ public function setPostPolicy($postId, ContentItemPolicy $policy) { update_post_meta($postId, self::POST_POLICY_META_KEY, wp_slash(wp_json_encode($policy))); //Clear policy related caches. $this->hiddenListItemCache = []; //Update the central lookup that stores hidden posts. $lookup = $this->getRestrictedPostLookup(); $lookup->forgetPost($postId); $postType = get_post_type($postId); if ( $postType ) { $policy->forEachActionEntry( [ $this->actionRegistry->getAction(ActionRegistry::ACTION_READ), $this->actionRegistry->getAction(ActionRegistry::ACTION_VIEW_IN_LISTS), ], function ($actorId, $actionName, $isAllowed) use ($lookup, $postId, $postType) { $action = $this->actionRegistry->getAction($actionName); $lookup->addEntry($actorId, $action, $postType, $postId, $isAllowed); } ); } if ( $lookup->isModified() ) { $this->saveRestrictedPostLookup($lookup); } } public function getTermPolicy($termId, ?string $postType = null, bool $useUniversalOnMiss = false): ?ContentItemPolicy { $collection = $this->getTermPolicyCollection($termId); if ( $collection === null ) { return null; } if ( $postType === null ) { return $collection->getUniversalPolicy(); } else { $policyForPostType = $collection->getPolicyForPostType($postType); if ( $policyForPostType ) { return $policyForPostType; } else if ( $useUniversalOnMiss ) { return $collection->getUniversalPolicy(); } } return null; } private function setTermPolicy($termId, ContentItemPolicy $policy, ?string $postType = null) { //Unlike a post, a term can have multiple policies - one for each associated post type, //and a general one that applies to all post types. So we can't just overwrite the meta //value, we need to merge the new policy into the existing one(s). $collection = $this->getTermPolicyCollection($termId); if ( $collection === null ) { $collection = new TermPolicyCollection(); } if ( $postType === null ) { $collection->setUniversalPolicy($policy); } else { $collection->setPolicyForPostType($postType, $policy); } $this->setTermPolicyCollection($termId, $collection); } private function getTermPolicyCollection($termId): ?TermPolicyCollection { $collectionData = get_term_meta($termId, self::TERM_POLICY_META_KEY, true); if ( !empty($collectionData) ) { $properties = json_decode($collectionData, true); if ( is_array($properties) ) { return TermPolicyCollection::deserializeFromArray($properties); } } return null; } private function setTermPolicyCollection($termId, TermPolicyCollection $collection) { update_term_meta($termId, self::TERM_POLICY_META_KEY, wp_slash(wp_json_encode($collection))); //todo: Probably need to update caches and lookups here. } /** * @param Actor $actor * @param string[]|null $postTypes * @return int[] */ public function getPostsHiddenFromLists($actor, $postTypes = null) { if ( is_array($postTypes) && empty($postTypes) ) { return []; } $cacheKey = $this->getHiddenListItemCacheKey(Action::OBJECT_TYPE_POST, $actor, $postTypes); if ( isset($this->hiddenListItemCache[$cacheKey]) ) { return $this->hiddenListItemCache[$cacheKey]; } $lookup = $this->getRestrictedPostLookup(); $restrictedPosts = $lookup->getRestrictedPosts( $actor, $this->actionRegistry->getAction(ActionRegistry::ACTION_VIEW_IN_LISTS), $postTypes ); $this->hiddenListItemCache[$cacheKey] = $restrictedPosts; return $restrictedPosts; } /** * Check if there might be *any* posts that the current user isn't allowed to read or view in lists. * * @return bool */ public function mightHaveHiddenPosts(Actor $actor) { $lookup = $this->getRestrictedPostLookup(); $actions = [ $this->actionRegistry->getAction(ActionRegistry::ACTION_READ), $this->actionRegistry->getAction(ActionRegistry::ACTION_VIEW_IN_LISTS), ]; return $lookup->hasPotentialRestrictions($actor, $actions); } private function getRestrictedPostLookup() { if ( $this->restrictedPostLookup === null ) { $storedData = get_option(self::HIDDEN_ITEM_LOOKUP_OPTION, '{}'); $parsed = json_decode((string)$storedData, true); if ( !empty($parsed) && is_array($parsed) ) { $this->restrictedPostLookup = new RestrictedPostLookup($parsed); } else { $this->restrictedPostLookup = new RestrictedPostLookup(); } } return $this->restrictedPostLookup; } private function saveRestrictedPostLookup(RestrictedPostLookup $lookup) { update_option(self::HIDDEN_ITEM_LOOKUP_OPTION, wp_json_encode($lookup)); } /** * Remove a post from the restricted post lookup when it's deleted. * * This is a hook callback and should not be called directly. * * @param int $postId * @return void * @internal */ public function forgetDeletedPost($postId) { if ( !is_numeric($postId) || ($postId <= 0) ) { return; } $lookup = $this->getRestrictedPostLookup(); $lookup->forgetPost($postId); if ( $lookup->isModified() ) { $this->saveRestrictedPostLookup($lookup); } } /** * @param string $objectType * @param Actor $actor * @param array|null $objectWpTypes * @return string */ private function getHiddenListItemCacheKey($objectType, Actor $actor, $objectWpTypes) { $key = $objectType . '|' . $actor->getId() . '|'; if ( $objectWpTypes ) { $key .= implode(',', $objectWpTypes); } return $key; } } class Action implements \JsonSerializable { protected $name; /** * @var string|null */ protected $label = null; /** * @var null|callable */ protected $labelGenerator = null; /** * @var string|null */ protected $description = null; /** * @var null|callable */ protected $descriptionGenerator = null; const OBJECT_TYPE_POST = 'post'; const OBJECT_TYPE_TERM = 'term'; protected $supportedObjectTypes = []; /** * @var string|null */ private $postMetaCap; /** * @var string|null */ private $termMetaCap; /** * @var string|null */ private $group; /** * @param string $name * @param array $supportedObjectTypes * @param string|callable $label * @param string|null $group * @param string|callable $description * @param string|null $postMetaCap * @param string|null $termMetaCap */ public function __construct( string $name, array $supportedObjectTypes, $label, ?string $group = null, $description = '', ?string $postMetaCap = null, ?string $termMetaCap = null ) { $this->name = $name; $this->supportedObjectTypes = $supportedObjectTypes; $this->postMetaCap = $postMetaCap; $this->termMetaCap = $termMetaCap; if ( is_string($label) ) { $this->label = $label; } else if ( $label !== null ) { $this->labelGenerator = $label; } if ( is_string($description) ) { $this->description = $description; } else if ( $description !== null ) { $this->descriptionGenerator = $description; } $this->group = $group; } /** * @return string */ public function getName() { return $this->name; } public function getLabel(?\WP_Post_Type $postType = null, ?\WP_Taxonomy $taxonomy = null): string { if ( ($this->label === null) && is_callable($this->labelGenerator) ) { return call_user_func($this->labelGenerator, $postType, $taxonomy); } return $this->label ?? ''; } /** * @return string */ public function getDescription() { if ( ($this->description === null) && is_callable($this->descriptionGenerator) ) { $this->description = call_user_func($this->descriptionGenerator); } return $this->description; } /** * Checks if the given object type is supported by this action. * * @param string $objectType * @param mixed $nativeObject The specific instance of the object type. This can be a post object, term object, etc. * @return bool * @noinspection PhpUnusedParameterInspection ($objectInstance can be used in subclasses) */ public function appliesToObject($objectType, $nativeObject) { return in_array($objectType, $this->supportedObjectTypes, true); } /** * @return string|null */ public function getPostMetaCap() { return $this->postMetaCap; } public function getTermMetaCap(): ?string { return $this->termMetaCap; } /** @noinspection PhpLanguageLevelInspection */ #[\ReturnTypeWillChange] public function jsonSerialize() { return [ 'name' => $this->name, 'description' => $this->getDescription(), 'group' => $this->group, ]; } } class ActionRegistry { const ACTION_EDIT = 'edit'; const ACTION_DELETE = 'delete'; const ACTION_READ = 'read'; const ACTION_VIEW_IN_LISTS = 'view_in_lists'; const ACTION_PUBLISH = 'publish'; const ACTION_READ_ASSOCIATED_OBJECTS = 'read_associated_objects'; const ACTION_EDIT_ASSOCIATED_OBJECTS = 'edit_associated_objects'; const ACTION_DELETE_ASSOCIATED_OBJECTS = 'delete_associated_objects'; const ACTION_VIEW_ASSOCIATED_OBJECTS_IN_LISTS = 'view_associated_objects_in_lists'; const ACTION_ASSIGN_TERM_TO_OBJECTS = 'assign_term_to_objects'; const GROUP_ASSOCIATED_POSTS = 'associated_posts'; const GROUP_THIS_TERM = 'this_term'; /** * @var Action[] */ protected $actions = []; public function __construct() { $this->addAction(new Action( self::ACTION_READ, [Action::OBJECT_TYPE_POST], function () { return _x('Read', 'content permissions: action name', 'admin-menu-editor'); }, null, '', 'read_post' )); $this->addAction(new Action( self::ACTION_VIEW_IN_LISTS, [Action::OBJECT_TYPE_POST], function () { return _x('View in lists', 'content permissions: action name', 'admin-menu-editor'); }, null, function () { return __( 'Applies to places like the post list in the admin dashboard and archive pages on the front end.', 'admin-menu-editor' ); }, 'read_post' )); $this->addAction(new Action( self::ACTION_EDIT, [Action::OBJECT_TYPE_POST], function () { return _x('Edit', 'content permissions: action name', 'admin-menu-editor'); }, null, '', 'edit_post' )); $this->addAction(new Action( self::ACTION_DELETE, [Action::OBJECT_TYPE_POST], function () { return _x('Delete', 'content permissions: action name', 'admin-menu-editor'); }, null, '', 'delete_post' )); $this->addAction(new Action( self::ACTION_PUBLISH, [Action::OBJECT_TYPE_POST], function () { return _x('Publish', 'content permissions: action name', 'admin-menu-editor'); }, null, '', 'publish_post' )); //Term actions for associated objects $this->addAction(new Action( self::ACTION_READ_ASSOCIATED_OBJECTS, [Action::OBJECT_TYPE_TERM], self::labelHelper(function () { /* translators: %1$s: post type label, %2$s: taxonomy label */ return _x('Read %1$s', 'content permissions: action name', 'admin-menu-editor'); }), self::GROUP_ASSOCIATED_POSTS, '', 'read_post' )); //todo: Should disabling this also prevent the term itself from appearing in the term list? // We probably don't want too many granular options here. $this->addAction(new Action( self::ACTION_VIEW_ASSOCIATED_OBJECTS_IN_LISTS, [Action::OBJECT_TYPE_TERM], self::labelHelper(function () { /* translators: %1$s: post type label */ return _x('View %1$s in lists', 'content permissions: action name', 'admin-menu-editor'); }), self::GROUP_ASSOCIATED_POSTS, '', 'read_post' )); $this->addAction(new Action( self::ACTION_EDIT_ASSOCIATED_OBJECTS, [Action::OBJECT_TYPE_TERM], self::labelHelper(function () { /* translators: %1$s: post type label, %2$s: taxonomy label */ return _x('Edit %1$s', 'content permissions: action name', 'admin-menu-editor'); }), self::GROUP_ASSOCIATED_POSTS, '', 'edit_post' )); $this->addAction(new Action( self::ACTION_DELETE_ASSOCIATED_OBJECTS, [Action::OBJECT_TYPE_TERM], self::labelHelper(function () { /* translators: %1$s: post type label, %2$s: taxonomy label */ return _x('Delete %1$s', 'content permissions: action name', 'admin-menu-editor'); }), self::GROUP_ASSOCIATED_POSTS, '', 'delete_post' )); $this->addAction(new Action( self::ACTION_ASSIGN_TERM_TO_OBJECTS, [Action::OBJECT_TYPE_TERM], self::labelHelper(function () { /* translators: %1$s: post type label, %2$s: taxonomy label */ return _x('Assign this %2$s to %1$s', 'content permissions: action name', 'admin-menu-editor'); }), self::GROUP_THIS_TERM, '', null, 'assign_term' )); } public function addAction(Action $action) { $this->actions[$action->getName()] = $action; } /** * @param string $name * @return Action|null */ public function getAction($name) { return isset($this->actions[$name]) ? $this->actions[$name] : null; } /** * @param string $objectType * @param mixed $nativeObject * @return Action[] */ public function getApplicableActions($objectType, $nativeObject = null) { $actions = []; foreach ($this->actions as $action) { if ( $action->appliesToObject($objectType, $nativeObject) ) { $actions[] = $action; } } return $actions; } private static function labelHelper(callable $templateFn): \Closure { return function (?\WP_Post_Type $postType = null, ?\WP_Taxonomy $taxonomy = null) use ($templateFn) { return sprintf( call_user_func($templateFn), $postType ? $postType->labels->name : __('objects', 'admin-menu-editor'), $taxonomy ? $taxonomy->labels->singular_name : __('term', 'admin-menu-editor') ); }; } } class EvaluationResult { protected $effect; protected $actor; protected $action; protected $objectId; /** * @var ContentItemPolicy $usedAccessPolicy The policy that produced this result. * This can be different from $objectPolicy if the policy was inherited from a parent object. */ protected $usedAccessPolicy; /** * @var ContentItemPolicy|null $objectPolicy The policy directly associated with the object. * This can be NULL if the object doesn't have a policy of its own. */ protected $objectPolicy; public function __construct( $effect, Actor $actor, Action $action, ContentItemPolicy $policy, ?ContentItemPolicy $objectPolicy = null, $objectId = null ) { $this->effect = $effect; $this->actor = $actor; $this->action = $action; $this->usedAccessPolicy = $policy; $this->objectPolicy = $objectPolicy; $this->objectId = $objectId; } public function getEffect() { return $this->effect; } /** * @return Actor */ public function getActor() { return $this->actor; } /** * @return Action */ public function getAction() { return $this->action; } /** * @return ContentItemPolicy */ public function getPolicy() { if ( $this->objectPolicy ) { return $this->objectPolicy; } return $this->usedAccessPolicy; } /** * @return ContentItemPolicy */ public function getUsedAccessPolicy() { return $this->usedAccessPolicy; } /** * @return ContentItemPolicy */ public function getObjectPolicy() { return $this->objectPolicy; } /** * @return mixed|null */ public function getObjectId() { return $this->objectId; } public function isDenied() { return $this->effect === ContentItemPolicy::EFFECT_DENY; } public function isAllowed() { return $this->effect === ContentItemPolicy::EFFECT_ALLOW; } } abstract class AccessProtection implements \JsonSerializable { /** * @param \WP_Post $post Current post. * @param \WP_Post[] $posts List of posts for the current query. * @param \WP_Query|null $query The current query, if applicable. * @return \WP_Post[]|null Filtered list of posts, or NULL to leave the list unchanged. */ abstract public function enforce($post, $posts = [], $query = null); abstract protected function toArray(); /** * @param array $properties * @param ContentItemPolicy|null $ownerPolicy The policy that owns the protection that's being deserialized. * @return AccessProtection */ public static function fromArray($properties, ?ContentItemPolicy $ownerPolicy = null) { if ( !isset($properties['tag']) ) { throw new \InvalidArgumentException('Invalid properties array for an access protection object'); } $tag = $properties['tag']; switch ($tag) { case RedirectProtection::$tag: return RedirectProtection::deserializeFromArray($properties); case ErrorMessageProtection::$tag: //If the error message isn't set, it defaults to the replacement content //set for the policy that owns this protection. $messageCallback = null; if ( $ownerPolicy ) { $messageCallback = [$ownerPolicy, 'getReplacementContent']; } return ErrorMessageProtection::deserializeFromArray($properties, $messageCallback); case NotFoundProtection::$tag: return new NotFoundProtection(); case ContentReplacementProtection::$tag: return new ContentReplacementProtection(); default: throw new \InvalidArgumentException('Unknown access protection tag: ' . esc_html($tag)); } } public function getTag() { return static::$tag; } /** @noinspection PhpLanguageLevelInspection */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->toArray(); } } class RedirectProtection extends AccessProtection { protected static $tag = 'redirect'; const REDIRECT_ACTION = 'template_redirect'; const DEFAULT_REDIRECT_CODE = 307; /** * @var string $targetUrl */ private $targetUrl; private $shortcodesEnabled; private $redirectCode; public function __construct($targetUrl, $redirectCode = self::DEFAULT_REDIRECT_CODE, $shortcodesEnabled = false) { $this->targetUrl = is_string($targetUrl) ? $targetUrl : ''; $this->shortcodesEnabled = boolval($shortcodesEnabled); $code = intval($redirectCode); if ( ($code >= 300) && ($code <= 399) ) { $this->redirectCode = $code; } else { $this->redirectCode = self::DEFAULT_REDIRECT_CODE; } } public function enforce($post, $posts = [], $query = null) { if ( did_action(self::REDIRECT_ACTION) ) { $this->performRedirect(); return []; } else { add_action(self::REDIRECT_ACTION, [$this, 'performRedirect']); } return []; } public function performRedirect() { $url = $this->targetUrl; if ( empty($url) ) { $url = $this->getFallbackUrl(); } $parsed = wp_parse_url($url); if ( empty($parsed) ) { $url = $this->getFallbackUrl(); } if ( $this->shortcodesEnabled ) { $url = do_shortcode($url); } //The user provides the redirect URL, and they can choose an external URL if they want. //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect if ( wp_redirect($url, $this->redirectCode) ) { exit(); } else { wp_die('Error: Redirect failed', 500); } } private function getFallbackUrl() { return home_url(); } protected function toArray() { $result = ['tag' => static::$tag]; if ( $this->redirectCode !== self::DEFAULT_REDIRECT_CODE ) { $result['redirectCode'] = $this->redirectCode; } if ( $this->shortcodesEnabled ) { $result['shortcodesEnabled'] = true; } if ( !empty($this->targetUrl) ) { $result['targetUrl'] = $this->targetUrl; } return $result; } protected static function deserializeFromArray($properties) { return new static( isset($properties['targetUrl']) ? strval($properties['targetUrl']) : '', isset($properties['redirectCode']) ? intval($properties['redirectCode']) : self::DEFAULT_REDIRECT_CODE, !empty($properties['shortcodesEnabled']) ); } } class ErrorMessageProtection extends AccessProtection { protected static $tag = 'errorMessage'; const FALLBACK_MESSAGE = 'Access denied'; /** * @var string */ private $errorMessage; /** * @var callable|null */ private $backupMessageCallback; public function __construct($errorMessage = '', $backupMessageCallback = null) { $this->errorMessage = $errorMessage; $this->backupMessageCallback = $backupMessageCallback; } public function enforce($post, $posts = [], $query = null) { $message = ''; if ( !empty($this->errorMessage) ) { $message = $this->errorMessage; } else if ( !empty($this->backupMessageCallback) ) { $message = call_user_func($this->backupMessageCallback); } if ( empty($message) ) { $message = self::FALLBACK_MESSAGE; } wp_die(wp_kses_post($message), 403); } protected function toArray() { $result = ['tag' => static::$tag]; if ( !empty($this->errorMessage) ) { $result['errorMessage'] = $this->errorMessage; } return $result; } protected static function deserializeFromArray($properties, $messageCallback = null) { return new static( isset($properties['errorMessage']) ? strval($properties['errorMessage']) : '', $messageCallback ); } } class NotFoundProtection extends AccessProtection { protected static $tag = 'notFound'; public function enforce($post, $posts = [], $query = null) { if ( $query instanceof \WP_Query ) { $query->set_404(); } return []; } protected function toArray() { return ['tag' => static::$tag]; } } class ContentReplacementProtection extends AccessProtection { protected static $tag = 'replace'; public function enforce($post, $posts = [], $query = null) { //The actual replacement is handled by the ContentPermissionsEnforcer class which //covers both direct access and indirect access (e.g. via category archives). This //class is a placeholder for the protection type, and helps with serialization. return null; } protected function toArray() { return ['tag' => static::$tag]; } } class AccessProtectionSettings implements \JsonSerializable { /** * @var AccessProtection|null */ private $activeProtection; /** * @var array