← All changes
|
jetpack_vendor/automattic/jetpack-videopress/src/class-access-control.php
+169
-16
16.2
→
16.3
View file →
| @@ -135,11 +135,13 @@ | ||
| 135 | 135 | * |
| 136 | 136 | * @return array |
| 137 | 137 | */ |
| 138 | 138 | private function build_restriction_details( $guid, $embedded_post_id, $selected_plan_id ) { |
| 139 | - $post_to_check = get_post( $embedded_post_id ); | |
| 139 | + // A missing post ID does not fall back to the global post, as get_post( 0 ) would. | |
| 140 | + $post_to_check = $embedded_post_id > 0 ? get_post( $embedded_post_id ) : null; | |
| 140 | 141 | |
| 141 | - if ( empty( $post_to_check ) ) { | |
| 142 | + // Only a published embedding post can authorize playback. | |
| 143 | + if ( ! $post_to_check instanceof WP_Post || 'publish' !== $post_to_check->post_status ) { | |
| 142 | 144 | $restriction_details = $this->default_video_restriction_details( false ); |
| 143 | 145 | return $this->filter_video_restriction_details( $restriction_details, $guid, $embedded_post_id, $selected_plan_id ); |
| 144 | 146 | } |
| 145 | 147 | |
| @@ -144,15 +146,21 @@ | ||
| 144 | 146 | } |
| 145 | 147 | |
| 146 | 148 | $default_auth = $this->get_default_user_capability_for_post( $post_to_check ); |
| 147 | 149 | $restriction_details = $this->default_video_restriction_details( $default_auth ); |
| 150 | + // Logged-out visitors never have read_post, so a public post admits them on its status alone. | |
| 151 | + // Neither check knows about post passwords, which the page enforces for everyone. | |
| 152 | + $post_admits_visitor = ( $default_auth || is_post_publicly_viewable( $post_to_check ) ) | |
| 153 | + && ! post_password_required( $post_to_check ); | |
| 148 | 154 | |
| 149 | 155 | if ( $this->jetpack_memberships_available() ) { |
| 150 | 156 | $post_access_level = \Jetpack_Memberships::get_post_access_level( $embedded_post_id ); |
| 151 | 157 | if ( 'everybody' !== $post_access_level ) { |
| 152 | - $memberships_can_view_post = \Jetpack_Memberships::user_can_view_post( $embedded_post_id ); | |
| 158 | + $memberships_can_view_post = is_callable( array( '\Jetpack_Memberships', 'user_has_subscription_access' ) ) | |
| 159 | + && \Jetpack_Memberships::user_has_subscription_access( $embedded_post_id ); | |
| 153 | 160 | $restriction_details = $this->get_subscriber_only_restriction_details( $default_auth ); |
| 154 | 161 | $restriction_details['can_access'] = $memberships_can_view_post; |
| 162 | + $post_admits_visitor = $post_admits_visitor && $memberships_can_view_post; | |
| 155 | 163 | } |
| 156 | 164 | } |
| 157 | 165 | |
| 158 | 166 | return $this->check_block_level_access( |
| @@ -158,9 +166,10 @@ | ||
| 158 | 166 | return $this->check_block_level_access( |
| 159 | 167 | $restriction_details, |
| 160 | 168 | $guid, |
| 161 | 169 | $embedded_post_id, |
| 162 | - $selected_plan_id | |
| 170 | + $selected_plan_id, | |
| 171 | + $post_admits_visitor | |
| 163 | 172 | ); |
| 164 | 173 | } |
| 165 | 174 | |
| 166 | 175 | /** |
| @@ -168,25 +177,29 @@ | ||
| 168 | 177 | * |
| 169 | 178 | * @param array $restriction_details the restriction details array. |
| 170 | 179 | * @param string $guid the video guid. |
| 171 | 180 | * @param int $embedded_post_id the post id. |
| 172 | - * @param int $selected_plan_id the selected plan id if applicable. | |
| 181 | + * @param int $selected_plan_id the plan id sent with the request. Only passed on to the filter below. | |
| 182 | + * @param bool $post_admits_visitor Whether the embedding post itself, and any paywall on it, lets the visitor in. | |
| 173 | 183 | * |
| 174 | 184 | * @return array |
| 175 | 185 | */ |
| 176 | - private function check_block_level_access( $restriction_details, $guid, $embedded_post_id, $selected_plan_id ) { | |
| 177 | - if ( $this->jetpack_subscriptions_available() && $selected_plan_id > 0 ) { | |
| 178 | - $restriction_details = $this->get_subscriber_only_restriction_details( $restriction_details['can_access'] ); | |
| 179 | - $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service(); | |
| 186 | + private function check_block_level_access( $restriction_details, $guid, $embedded_post_id, $selected_plan_id, $post_admits_visitor ) { | |
| 187 | + // Plans come from the stored block; null means no Paid Content block wraps the guid. | |
| 188 | + $required_plan_ids = $this->get_required_plan_ids_for_guid( $embedded_post_id, $guid ); | |
| 180 | 189 | |
| 181 | - // Only paid subscribers should be granted access to the premium content. | |
| 182 | - $access_level = ''; | |
| 183 | - if ( class_exists( Abstract_Token_Subscription_Service::class ) ) { | |
| 184 | - $access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS; | |
| 190 | + if ( $this->jetpack_subscriptions_available() && null !== $required_plan_ids ) { | |
| 191 | + $prior_can_access = $restriction_details['can_access']; | |
| 192 | + $restriction_details = $this->get_subscriber_only_restriction_details( $prior_can_access ); | |
| 193 | + | |
| 194 | + if ( empty( $required_plan_ids ) ) { | |
| 195 | + $restriction_details['can_access'] = false; | |
| 196 | + } else { | |
| 197 | + $can_view = $this->block_gate_grants_access( $required_plan_ids, $embedded_post_id ); | |
| 198 | + | |
| 199 | + // A block grant also requires the post-level decision. | |
| 200 | + $restriction_details['can_access'] = $post_admits_visitor && $can_view; | |
| 185 | 201 | } |
| 186 | - | |
| 187 | - $can_view = $paywall->visitor_can_view_content( array( $selected_plan_id ), $access_level ); | |
| 188 | - $restriction_details['can_access'] = $can_view || current_user_can( 'edit_post', $embedded_post_id ); // Editors can always view the content. | |
| 189 | 202 | } |
| 190 | 203 | |
| 191 | 204 | return $this->filter_video_restriction_details( |
| 192 | 205 | $restriction_details, |
| @@ -196,8 +209,50 @@ | ||
| 196 | 209 | ); |
| 197 | 210 | } |
| 198 | 211 | |
| 199 | 212 | /** |
| 213 | + * Ask the premium-content block's own gate whether the visitor may view content behind these plans. | |
| 214 | + * | |
| 215 | + * Sharing the gate keeps playback and the page in agreement, including on tier upgrades. | |
| 216 | + * | |
| 217 | + * @param int[] $required_plan_ids Plan ids derived from the block wrapping the guid. | |
| 218 | + * @param int $embedded_post_id The post the block lives in; there is no loop post to fall back on here. | |
| 219 | + * | |
| 220 | + * @return bool | |
| 221 | + */ | |
| 222 | + private function block_gate_grants_access( $required_plan_ids, $embedded_post_id ) { | |
| 223 | + // The gate normally loads with the Paid Content block, which a site can have switched off. | |
| 224 | + if ( | |
| 225 | + ! function_exists( '\Automattic\Jetpack\Extensions\Premium_Content\visitor_has_subscription_access_to_plan_ids' ) | |
| 226 | + && defined( 'JETPACK__PLUGIN_DIR' ) | |
| 227 | + ) { | |
| 228 | + $access_check_path = JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/access-check.php'; | |
| 229 | + if ( file_exists( $access_check_path ) ) { | |
| 230 | + require_once $access_check_path; | |
| 231 | + } | |
| 232 | + } | |
| 233 | + | |
| 234 | + if ( function_exists( '\Automattic\Jetpack\Extensions\Premium_Content\visitor_has_subscription_access_to_plan_ids' ) ) { | |
| 235 | + return (bool) \Automattic\Jetpack\Extensions\Premium_Content\visitor_has_subscription_access_to_plan_ids( $required_plan_ids, $embedded_post_id ); | |
| 236 | + } | |
| 237 | + | |
| 238 | + // A newer standalone VideoPress can run beside an older Jetpack that lacks the shared gate. | |
| 239 | + // Exact plan matching is stricter than the gate for tiers, never more permissive. | |
| 240 | + $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service(); | |
| 241 | + | |
| 242 | + // Only paid subscribers should be granted access to the premium content. | |
| 243 | + $access_level = ''; | |
| 244 | + if ( class_exists( Abstract_Token_Subscription_Service::class ) ) { | |
| 245 | + $access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS; | |
| 246 | + } | |
| 247 | + | |
| 248 | + // Deny when the service has no entitlement-only check. | |
| 249 | + return is_callable( array( $paywall, 'visitor_has_subscription_access' ) ) | |
| 250 | + // @phan-suppress-next-line PhanUndeclaredMethod -- Optional method is checked above to support older services. | |
| 251 | + && $paywall->visitor_has_subscription_access( $required_plan_ids, $access_level, $embedded_post_id ); | |
| 252 | + } | |
| 253 | + | |
| 254 | + /** | |
| 200 | 255 | * Returns the default restriction_details for a video. |
| 201 | 256 | * |
| 202 | 257 | * @param bool $default_can_access The default auth. |
| 203 | 258 | * |
| @@ -438,8 +493,106 @@ | ||
| 438 | 493 | } |
| 439 | 494 | } |
| 440 | 495 | |
| 441 | 496 | return $guids; |
| 497 | + } | |
| 498 | + | |
| 499 | + /** | |
| 500 | + * Read the plan ids gating a video guid from the Paid Content block that wraps it in the stored post. | |
| 501 | + * | |
| 502 | + * Playback is authorized outside any render, so the post's blocks are re-parsed here. | |
| 503 | + * | |
| 504 | + * @param int $embedded_post_id The post the video is embedded in. | |
| 505 | + * @param string $guid The video guid. | |
| 506 | + * | |
| 507 | + * @return int[]|null Plan ids that gate the guid, an empty array when it is wrapped in a premium-content | |
| 508 | + * block that configures no plan, or null when it is not wrapped in one at all. | |
| 509 | + */ | |
| 510 | + private function get_required_plan_ids_for_guid( $embedded_post_id, $guid ) { | |
| 511 | + $embedded_post_id = (int) $embedded_post_id; | |
| 512 | + if ( ! $embedded_post_id ) { | |
| 513 | + return null; | |
| 514 | + } | |
| 515 | + | |
| 516 | + $post = get_post( $embedded_post_id ); | |
| 517 | + if ( ! $post || false === strpos( (string) $post->post_content, 'wp:premium-content/container' ) ) { | |
| 518 | + return null; | |
| 519 | + } | |
| 520 | + | |
| 521 | + return $this->find_gating_plan_ids( parse_blocks( $post->post_content ), $guid ); | |
| 522 | + } | |
| 523 | + | |
| 524 | + /** | |
| 525 | + * Recursively locate the premium-content/container block that wraps the given guid and return its plan ids. | |
| 526 | + * | |
| 527 | + * @param array $blocks Parsed blocks (parse_blocks() output or an innerBlocks array). | |
| 528 | + * @param string $guid The video guid to match. | |
| 529 | + * | |
| 530 | + * @return int[]|null Plan ids of the wrapping premium-content block (possibly an empty array when it | |
| 531 | + * configures none), or null when no premium-content block wraps the guid. | |
| 532 | + */ | |
| 533 | + private function find_gating_plan_ids( $blocks, $guid ) { | |
| 534 | + foreach ( $blocks as $block ) { | |
| 535 | + if ( empty( $block['blockName'] ) ) { | |
| 536 | + continue; | |
| 537 | + } | |
| 538 | + | |
| 539 | + // Descend first so the innermost container, the one directly gating the video, wins. | |
| 540 | + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { | |
| 541 | + $found = $this->find_gating_plan_ids( $block['innerBlocks'], $guid ); | |
| 542 | + if ( null !== $found ) { | |
| 543 | + return $found; | |
| 544 | + } | |
| 545 | + } | |
| 546 | + | |
| 547 | + if ( | |
| 548 | + is_string( $block['blockName'] ) && 'premium-content/container' === $block['blockName'] | |
| 549 | + && $this->container_gates_guid( $block, $guid ) | |
| 550 | + ) { | |
| 551 | + return $this->extract_plan_ids( $block['attrs'] ?? array() ); | |
| 552 | + } | |
| 553 | + } | |
| 554 | + | |
| 555 | + return null; | |
| 556 | + } | |
| 557 | + | |
| 558 | + /** | |
| 559 | + * Determine whether a premium-content/container block wraps the given guid. | |
| 560 | + * | |
| 561 | + * Uses the same scan as the per-post GUID cache, so every embed form it recognises counts. | |
| 562 | + * | |
| 563 | + * @param array $block A parsed premium-content/container block. | |
| 564 | + * @param string $guid The video guid to match. | |
| 565 | + * | |
| 566 | + * @return bool | |
| 567 | + */ | |
| 568 | + private function container_gates_guid( $block, $guid ) { | |
| 569 | + $visited_refs = array(); | |
| 570 | + | |
| 571 | + return in_array( $guid, self::collect_guids_from_content( serialize_blocks( array( $block ) ), $visited_refs ), true ); | |
| 572 | + } | |
| 573 | + | |
| 574 | + /** | |
| 575 | + * Normalise a premium-content/container block's plan attributes into a list of plan ids. | |
| 576 | + * | |
| 577 | + * Mirrors the precedence used when rendering the block: the current `selectedPlanIds` array | |
| 578 | + * wins, falling back to the legacy single `selectedPlanId`. | |
| 579 | + * | |
| 580 | + * @param array $attrs The block attributes. | |
| 581 | + * | |
| 582 | + * @return int[] Plan ids, with zero/empty values removed. | |
| 583 | + */ | |
| 584 | + private function extract_plan_ids( $attrs ) { | |
| 585 | + if ( isset( $attrs['selectedPlanIds'] ) && is_array( $attrs['selectedPlanIds'] ) ) { | |
| 586 | + return array_values( array_filter( array_map( 'intval', $attrs['selectedPlanIds'] ) ) ); | |
| 587 | + } | |
| 588 | + | |
| 589 | + if ( isset( $attrs['selectedPlanId'] ) ) { | |
| 590 | + $plan_id = (int) $attrs['selectedPlanId']; | |
| 591 | + return $plan_id ? array( $plan_id ) : array(); | |
| 592 | + } | |
| 593 | + | |
| 594 | + return array(); | |
| 442 | 595 | } |
| 443 | 596 | |
| 444 | 597 | /** |
| 445 | 598 | * Check if a post contains a VideoPress GUID, using the cached GUID list for fast lookup. |