PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← 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.