is_active() || ! $this->is_supported_post_type( $post_id ) ) { return $has_owner_access; } $resolved = $this->resolve( $post_id ); return $resolved['allowed']; } /** * Filter whether the current user has access to a specific podcast or episode. * * @since 1.0.3 * @param bool $has_access True if the user has access, false otherwise. * @param string $capability Capability being checked. * @param int $post_id Optional. ID of the post to check access against. * @return bool True if the current user has access, false otherwise. */ public function filter_user_access( $has_access, $capability, $post_id = 0 ) { // Never grant back access another access-control mechanism has already denied if ( 'view_aiopod_podcast' !== $capability || ! $has_access ) { return $has_access; } $post_id = (int) $post_id; if ( $post_id <= 0 || ! $this->is_active() || ! $this->is_supported_post_type( $post_id ) ) { return $has_access; } $resolved = $this->resolve( $post_id ); return $resolved['allowed']; } /** * Filter whether a podcast or episode is restricted to a limited audience. * * Answered from the membership levels stored against the post, never from the current * user, so the result is the same for every visitor. Paid Memberships Pro returns those * levels alongside its access decision, so this costs no extra query. * * @since 1.0.3 * @param bool $is_restricted True if the content is already known to be restricted. * @param int $post_id Podcast or episode ID. * @return bool True if the content is restricted, false if not. */ public function filter_is_content_restricted( $is_restricted, $post_id ) { if ( $is_restricted ) { return $is_restricted; } $post_id = (int) $post_id; if ( $post_id <= 0 || ! $this->is_active() || ! $this->is_supported_post_type( $post_id ) ) { return $is_restricted; } $access = $this->get_access( $post_id ); // Membership levels stored against the post itself if ( ! empty( $access['ids'] ) ) { return true; } // Otherwise the parent podcast, when an episode inherits from one $parent_id = aiopod_get_inherited_podcast_id( $post_id ); if ( $parent_id > 0 ) { $parent_access = $this->get_access( $parent_id ); if ( ! empty( $parent_access['ids'] ) ) { return true; } } return false; } /** * Filter the access-denied output so Paid Memberships Pro shows its own message. * * Its message is returned as-is. It is built and escaped by Paid Memberships Pro and * carries markup, such as the lock icon, that `wp_kses_post()` would strip. * * @since 1.0.3 * @param string $html Rendered access-denied HTML. * @param int $post_id Post ID of the restricted podcast or episode. * @param string $context Context of the check, either 'podcast' or 'episode'. * @param string $content Content already produced upstream, or an empty string. * @return string Filtered access-denied HTML. */ public function filter_access_denied_html( $html, $post_id, $context, $content = '' ) { $post_id = (int) $post_id; if ( $post_id <= 0 || ! $this->is_active() || ! $this->is_supported_post_type( $post_id ) ) { return $html; } $resolved = $this->resolve( $post_id ); // Access granted here, so something else denied it and its own message stands if ( $resolved['allowed'] ) { return $html; } // Paid Memberships Pro filters the content of a restricted post before we do, so // its output is already in place and is returned untouched. An inherited // restriction never qualifies, the content is still the full episode at that point if ( ! $resolved['inherited'] && '' !== $content && doing_filter( 'the_content' ) ) { return $content; } return pmpro_get_no_access_message( '', $resolved['ids'], $resolved['names'] ); } /** * Add our post types to the ones Paid Memberships Pro filters out of archives and searches. * * Once the WordPress taxonomies setting puts podcasts and episodes into the core category * and tag archives, those archives are rendered by the theme rather than by us, so our own * access checks never run for the items listed there. Paid Memberships Pro already solves * this for posts and pages with `pmpro_search_filter()`, behind its own "Filter Searches and * Archives" option — off by default, which matches what the site owner asked for. * * So we hand the decision to that switch instead of building a second one. An owner who has * it on gets restricted podcasts and episodes withheld from those archives on the same terms * as their posts; an owner who has it off gets them listed with title, excerpt and image, the * content still protected on the single view. One control, and it follows whatever they set. * * Gated on the setting because that is what puts our post types in those archives in the * first place. With it off, nothing about the previous behaviour changes. * * @since 1.0.4 * @param array $post_types Post types Paid Memberships Pro filters. Defaults to page and post. * @return array The same list, with ours added when the setting calls for it. */ public function filter_search_filter_post_types( $post_types ) { $taxonomies_settings = aiopod_get_option( 'aiopod_taxonomies_settings' ); if ( empty( $taxonomies_settings['wp_taxonomies'] ) || empty( $taxonomies_settings['include_in_core_archives'] ) ) { return $post_types; } if ( ! is_array( $post_types ) ) { $post_types = array( $post_types ); } return array_values( array_unique( array_merge( $post_types, array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) ); } /** * Whether the Paid Memberships Pro functions we rely on are available. * * @since 1.0.3 * @access private * @return bool True if Paid Memberships Pro is active, false if not. */ private function is_active() { return function_exists( 'pmpro_has_membership_access' ) && function_exists( 'pmpro_get_no_access_message' ); } /** * Whether a post is one of ours, and so one we hand the access decision over for. * * Deliberately not gated on the `pmpro_restrictable_post_types` filter the Custom Post * Type Membership Access add-on supplies. That filter only decides which post types get * the "Require Membership" box in the editor, it does not limit what Paid Memberships * Pro enforces: membership levels already stored against a post keep being enforced even * once the post type is deselected. Gating on it would leave those posts hidden by Paid * Memberships Pro while we kept rendering the player around its restriction message. * * Posts with no membership levels are no concern here, `pmpro_has_membership_access()` * grants access to those, so unrestricted content is never affected. * * @since 1.0.3 * @access private * @param int $post_id Post ID to check. * @return bool True for a podcast or an episode, false for anything else. */ private function is_supported_post_type( $post_id ) { $post_type = get_post_type( $post_id ); return ( 'aiopod_podcasts' === $post_type || 'aiopod_episodes' === $post_type ); } /** * Get the Paid Memberships Pro access decision for a single post. * * Results are cached for the request because the membership lookup is one uncached * query per post, and a single episodes grid checks access many times over. * * Cached against the user as well as the post. The obvious key is the post alone, since * one visitor makes one request — but that stopped being true when the Member RSS * integration began authenticating a member part-way through a request with * `wp_set_current_user()`. Nothing currently asks this question before that happens, so * a post-only key would work today and fail silently the day something did, by answering * for the visitor who arrived rather than the member who was let in. * * @since 1.0.3 * @access private * @param int $post_id Post ID to check. * @return array Access decision with `allowed`, `ids` and `names` keys. */ private function get_access( $post_id ) { $post_id = (int) $post_id; $cache_key = get_current_user_id() . ':' . $post_id; if ( ! isset( $this->access_cache[ $cache_key ] ) ) { $access = pmpro_has_membership_access( $post_id, null, true ); // Only returned as an array when membership levels were requested if ( ! is_array( $access ) ) { $access = array( ! empty( $access ), array(), array() ); } $this->access_cache[ $cache_key ] = array( 'allowed' => ! empty( $access[0] ), 'ids' => ! empty( $access[1] ) ? (array) $access[1] : array(), 'names' => ! empty( $access[2] ) ? (array) $access[2] : array() ); } return $this->access_cache[ $cache_key ]; } /** * Resolve the effective Paid Memberships Pro decision for a post. * * The membership levels returned are the ones to build a restriction message from, * always the most specific available: the post's own levels when it is restricted * directly, the parent podcast's levels when the restriction is inherited. * * @since 1.0.3 * @access private * @param int $post_id Post ID to resolve. * @return array Decision with `allowed`, `inherited`, `ids` and `names` keys. */ private function resolve( $post_id ) { $post_id = (int) $post_id; $access = $this->get_access( $post_id ); // Restricted in its own right, so report against its own membership levels if ( ! $access['allowed'] ) { return array( 'allowed' => false, 'inherited' => false, 'ids' => $access['ids'], 'names' => $access['names'] ); } // Otherwise fall back to the parent podcast, when an episode inherits from one $parent_id = aiopod_get_inherited_podcast_id( $post_id ); if ( $parent_id > 0 ) { $parent_access = $this->get_access( $parent_id ); if ( ! $parent_access['allowed'] ) { return array( 'allowed' => false, 'inherited' => true, 'ids' => $parent_access['ids'], 'names' => $parent_access['names'] ); } } return array( 'allowed' => true, 'inherited' => false, 'ids' => array(), 'names' => array() ); } }