PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-videopress / src / class-access-control.php

class-access-control.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-videopress/src/class-access-control.php

602 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * VideoPress Access Control.
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8 namespace Automattic\Jetpack\VideoPress;
9
10 use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
11 use Automattic\Jetpack\Modules;
12 use VIDEOPRESS_PRIVACY;
13 use WP_Post;
14
15 /**
16 * VideoPress video access control utilities.
17 *
18 * Note: this is also being used on WordPress.com.
19 * Use IS_WPCOM checks for functionality that is specific to WPCOM/Jetpack.
20 */
21 class Access_Control {
22
23 /**
24 * Singleton Access_Control instance.
25 *
26 * @var Access_Control
27 **/
28 private static $instance = null;
29
30 /**
31 * Guid to subscription plan, store, for when used inline on a page.
32 *
33 * @var array
34 */
35 private $guids_to_subscriptions = array();
36
37 /**
38 * Set that this guid is controlled by a subscription.
39 *
40 * @param string $guid The guid to set.
41 * @param string|int $subscription_id The subscription to set.
42 *
43 * @return Access_Control
44 */
45 public function set_guid_subscription( $guid, $subscription_id ) {
46 $this->guids_to_subscriptions[ $guid ] = $subscription_id;
47 return $this;
48 }
49
50 /**
51 * Get the subscription for a guid.
52 *
53 * @param string $guid The guid to get.
54 *
55 * @return string|int|false
56 */
57 public function get_subscription_plan_id( $guid ) {
58 return $this->guids_to_subscriptions[ $guid ] ?? false;
59 }
60
61 /**
62 * Get the singleton instance.
63 *
64 * @return self
65 */
66 public static function instance() {
67 if ( null === self::$instance ) {
68 self::$instance = new self();
69 }
70
71 return self::$instance;
72 }
73
74 /**
75 * Determines if Jetpack Memberships are available.
76 *
77 * @return bool
78 */
79 private function jetpack_memberships_available() {
80 return class_exists( '\Jetpack_Memberships' );
81 }
82
83 /**
84 * Determines if Jetpack Subscriptions are available.
85 *
86 * @return bool
87 */
88 private function jetpack_subscriptions_available() {
89 $is_module_active = ( new Modules() )->is_active( 'subscriptions' );
90 if ( ! $is_module_active ) {
91 return false;
92 }
93
94 if ( function_exists( '\Automattic\Jetpack\Extensions\Premium_Content\subscription_service' ) ) {
95 return true;
96 }
97
98 if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
99 return false;
100 }
101
102 $subscription_service_file_path = JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
103 if ( ! file_exists( $subscription_service_file_path ) ) {
104 return false;
105 }
106
107 require_once $subscription_service_file_path;
108
109 return function_exists( '\Automattic\Jetpack\Extensions\Premium_Content\subscription_service' );
110 }
111
112 /**
113 * Check default user access. By default, subscribers or higher can view videos.
114 *
115 * @param WP_Post $post_to_check The post to check.
116 *
117 * @return bool
118 **/
119 private function get_default_user_capability_for_post( $post_to_check ) {
120 if ( ! isset( $post_to_check->ID ) ) {
121 return false;
122 }
123
124 $default_auth = current_user_can( 'read_post', $post_to_check->ID );
125
126 return $default_auth;
127 }
128
129 /**
130 * Determines if the current user can access restricted content and builds the restriction_details array.
131 *
132 * @param string $guid the video guid.
133 * @param int $embedded_post_id the post id.
134 * @param int $selected_plan_id the selected plan id if applicable.
135 *
136 * @return array
137 */
138 private function build_restriction_details( $guid, $embedded_post_id, $selected_plan_id ) {
139 $post_to_check = get_post( $embedded_post_id );
140
141 if ( empty( $post_to_check ) ) {
142 $restriction_details = $this->default_video_restriction_details( false );
143 return $this->filter_video_restriction_details( $restriction_details, $guid, $embedded_post_id, $selected_plan_id );
144 }
145
146 $default_auth = $this->get_default_user_capability_for_post( $post_to_check );
147 $restriction_details = $this->default_video_restriction_details( $default_auth );
148
149 if ( $this->jetpack_memberships_available() ) {
150 $post_access_level = \Jetpack_Memberships::get_post_access_level( $embedded_post_id );
151 if ( 'everybody' !== $post_access_level ) {
152 $memberships_can_view_post = \Jetpack_Memberships::user_can_view_post( $embedded_post_id );
153 $restriction_details = $this->get_subscriber_only_restriction_details( $default_auth );
154 $restriction_details['can_access'] = $memberships_can_view_post;
155 }
156 }
157
158 return $this->check_block_level_access(
159 $restriction_details,
160 $guid,
161 $embedded_post_id,
162 $selected_plan_id
163 );
164 }
165
166 /**
167 * Determines if the current user can access restricted block content and updates the restriction_details array.
168 *
169 * @param array $restriction_details the restriction details array.
170 * @param string $guid the video guid.
171 * @param int $embedded_post_id the post id.
172 * @param int $selected_plan_id the selected plan id if applicable.
173 *
174 * @return array
175 */
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();
180
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;
185 }
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 }
190
191 return $this->filter_video_restriction_details(
192 $restriction_details,
193 $guid,
194 $embedded_post_id,
195 $selected_plan_id
196 );
197 }
198
199 /**
200 * Returns the default restriction_details for a video.
201 *
202 * @param bool $default_can_access The default auth.
203 *
204 * @return array
205 **/
206 private function get_subscriber_only_restriction_details( $default_can_access = false ) {
207 return array(
208 'provider' => 'jetpack_memberships',
209 'title' => __( 'This video is subscriber-only', 'jetpack-videopress-pkg' ),
210 'unauthorized_message' => __( 'You need to be subscribed to view this video', 'jetpack-videopress-pkg' ),
211 'can_access' => $default_can_access,
212 );
213 }
214
215 /**
216 * Filters restriction details.
217 *
218 * @param array $video_restriction_details The restriction details.
219 * @param string $guid The video guid.
220 * @param int $embedded_post_id The post id.
221 * @param int $selected_plan_id The selected plan id if applicable.
222 *
223 * @return array
224 */
225 private function filter_video_restriction_details( $video_restriction_details, $guid, $embedded_post_id, $selected_plan_id ) {
226 /**
227 * Filters the video restriction details.
228 *
229 * @param array $video_restriction_details The restriction details.
230 * @param string $guid The video guid.
231 * @param int $embedded_post_id The post id.
232 * @param int $selected_plan_id The selected plan id if applicable.
233 *
234 * @return array
235 */
236 return (array) apply_filters( 'videopress_video_restriction_details', $video_restriction_details, $guid, $embedded_post_id, $selected_plan_id );
237 }
238
239 /**
240 * Returns the default restriction_details for a video.
241 *
242 * @param bool $default_can_access The default auth.
243 *
244 * @return array
245 **/
246 private function default_video_restriction_details( $default_can_access = false ) {
247 $restriction_details = array(
248 'version' => '1',
249 'provider' => 'auth',
250 'title' => __( 'Unauthorized', 'jetpack-videopress-pkg' ),
251 'unauthorized_message' => __( 'Unauthorized', 'jetpack-videopress-pkg' ),
252 'can_access' => $default_can_access,
253 );
254
255 return $restriction_details;
256 }
257
258 /**
259 * How long the per-post GUID list stays cached.
260 *
261 * @var int
262 */
263 const GUID_CACHE_EXPIRATION = 12 * HOUR_IN_SECONDS;
264
265 /**
266 * Maximum number of synced-pattern (wp_block) refs resolved per scan.
267 *
268 * @var int
269 */
270 const MAX_PATTERN_REFS = 20;
271
272 /**
273 * Build and cache the list of VideoPress GUIDs present in a post.
274 *
275 * Scans the post content for VideoPress video and playlist blocks, shortcodes, and
276 * URLs, then caches the GUID list in a transient for fast lookup during authorization
277 * checks. Synced pattern (core/block) refs are resolved manually: parse_blocks() does
278 * NOT expand them — their content lives in the referenced wp_block post and is only
279 * expanded by WordPress at render time.
280 *
281 * @param int $post_id The post ID to scan.
282 * @return array Array of VideoPress GUIDs found in the post.
283 */
284 public static function build_and_cache_post_guids( $post_id ) {
285 if ( empty( $post_id ) ) {
286 return array();
287 }
288
289 $post_id = absint( $post_id );
290 $transient_key = "videopress_guids_{$post_id}";
291
292 // Check if already cached.
293 $cached_guids = get_transient( $transient_key );
294 if ( false !== $cached_guids ) {
295 return (array) $cached_guids;
296 }
297
298 $post = get_post( $post_id );
299 if ( ! $post instanceof WP_Post || empty( $post->post_content ) ) {
300 set_transient( $transient_key, array(), self::GUID_CACHE_EXPIRATION );
301 return array();
302 }
303
304 $visited_refs = array();
305 $guids = self::collect_guids_from_content( $post->post_content, $visited_refs );
306
307 // Cache and return unique GUIDs.
308 $unique_guids = array_values( array_unique( array_filter( $guids ) ) );
309 set_transient( $transient_key, $unique_guids, self::GUID_CACHE_EXPIRATION );
310
311 return $unique_guids;
312 }
313
314 /**
315 * Ensure the given rendered GUIDs are present in a post's cached GUID list.
316 *
317 * Called from block render callbacks, where the block has already been expanded by
318 * WordPress (synced patterns, templates, template parts, widget areas…): the render
319 * itself is proof the video is embedded in the page being served for this post, so
320 * the GUID can be added even when the static content scan cannot see it.
321 *
322 * @param int $post_id The post ID the block is rendering on.
323 * @param string|string[] $guids The GUID(s) being rendered.
324 */
325 public static function ensure_post_guids_cached( $post_id, $guids ) {
326 $post_id = absint( $post_id );
327 $guids = array_filter( (array) $guids, 'is_string' );
328 if ( ! $post_id || ! $guids ) {
329 return;
330 }
331
332 $cached = self::build_and_cache_post_guids( $post_id );
333 $missing = array_diff( $guids, $cached );
334 if ( $missing ) {
335 set_transient(
336 "videopress_guids_{$post_id}",
337 array_values( array_merge( $cached, $missing ) ),
338 self::GUID_CACHE_EXPIRATION
339 );
340 }
341 }
342
343 /**
344 * Collect VideoPress GUIDs from a chunk of post content: blocks (with synced pattern
345 * refs resolved recursively), VideoPress URLs, and legacy shortcodes.
346 *
347 * @param string $content The post content to scan.
348 * @param array $visited_refs Accumulator of wp_block ref ids already resolved, keyed by id.
349 * @return array Array of VideoPress GUIDs found.
350 */
351 private static function collect_guids_from_content( $content, &$visited_refs ) {
352 $guids = self::collect_guids_from_blocks( parse_blocks( $content ), $visited_refs );
353
354 // Scan for VideoPress URLs (oEmbed, core/embed, core/video sources).
355 if ( preg_match_all( '#https?://[^\s"\'<>)]+#i', $content, $matches ) ) {
356 foreach ( $matches[0] as $url ) {
357 $guid = Utils::extract_videopress_guid_from_url( $url );
358 if ( $guid ) {
359 $guids[] = $guid;
360 }
361 }
362 }
363
364 // Scan for [videopress] and [wpvideo] shortcodes.
365 $pattern = get_shortcode_regex( array( 'videopress', 'wpvideo' ) );
366 $count = preg_match_all( '/' . $pattern . '/', $content, $matches, PREG_SET_ORDER );
367 if ( false !== $count && $count > 0 ) {
368 foreach ( $matches as $match ) {
369 $atts = shortcode_parse_atts( $match[3] );
370 // Only the positional argument identifies the video; named attributes must not satisfy the binding check.
371 if ( is_array( $atts ) && isset( $atts[0] ) && is_string( $atts[0] ) ) {
372 $guids[] = $atts[0];
373 }
374 }
375 }
376
377 return $guids;
378 }
379
380 /**
381 * Recursively collect VideoPress GUIDs from parsed blocks.
382 *
383 * Handles videopress/video blocks, videopress/playlist entries, and synced patterns:
384 * a core/block ref is resolved by loading the referenced wp_block post and scanning
385 * its content, mirroring render_block_core_block()'s constraints (published, unlocked
386 * wp_block posts only) with a visited guard against cycles.
387 *
388 * @param array $blocks Array of parsed blocks.
389 * @param array $visited_refs Accumulator of wp_block ref ids already resolved, keyed by id.
390 * @return array Array of VideoPress GUIDs found.
391 */
392 private static function collect_guids_from_blocks( $blocks, &$visited_refs ) {
393 $guids = array();
394
395 foreach ( $blocks as $block ) {
396 $block_name = $block['blockName'] ?? null;
397 $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
398 $attr_guid = $attrs['guid'] ?? null;
399 $attr_videos = $attrs['videos'] ?? null;
400 $attr_ref = $attrs['ref'] ?? null;
401
402 // A VideoPress video block with a GUID.
403 if ( 'videopress/video' === $block_name && is_string( $attr_guid ) ) {
404 $guids[] = $attr_guid;
405 }
406
407 // A VideoPress playlist block: each entry carries its own GUID.
408 if ( 'videopress/playlist' === $block_name && is_array( $attr_videos ) ) {
409 foreach ( $attr_videos as $entry ) {
410 if ( is_array( $entry ) && isset( $entry['guid'] ) && is_string( $entry['guid'] ) ) {
411 $guids[] = $entry['guid'];
412 }
413 }
414 }
415
416 // A synced pattern: resolve the wp_block ref, which parse_blocks() leaves unexpanded.
417 if ( 'core/block' === $block_name && ! empty( $attr_ref ) ) {
418 $ref = absint( $attr_ref );
419 if ( $ref && ! isset( $visited_refs[ $ref ] ) && count( $visited_refs ) < self::MAX_PATTERN_REFS ) {
420 $visited_refs[ $ref ] = true;
421
422 $pattern = get_post( $ref );
423 if (
424 $pattern instanceof WP_Post
425 && 'wp_block' === $pattern->post_type
426 && 'publish' === $pattern->post_status
427 && empty( $pattern->post_password )
428 && ! empty( $pattern->post_content )
429 ) {
430 $guids = array_merge( $guids, self::collect_guids_from_content( $pattern->post_content, $visited_refs ) );
431 }
432 }
433 }
434
435 // Recursively check inner blocks.
436 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
437 $guids = array_merge( $guids, self::collect_guids_from_blocks( $block['innerBlocks'], $visited_refs ) );
438 }
439 }
440
441 return $guids;
442 }
443
444 /**
445 * Check if a post contains a VideoPress GUID, using the cached GUID list for fast lookup.
446 *
447 * Used to prevent the embedded post id — which arrives from request input — from being
448 * treated as an authorization context when it has no relationship to the requested video.
449 * Matching the attachment id itself is not treated as proof of embedding: attachment ids
450 * are enumerable via the media REST route and would otherwise provide a second path around
451 * this check whenever the attachment has no parent and falls back to the `read` capability.
452 *
453 * @param int $embedded_post_id The post id to check.
454 * @param string $guid The video guid to find.
455 *
456 * @return bool
457 */
458 private function post_embeds_videopress_guid_cached( $embedded_post_id, $guid ) {
459 if ( empty( $embedded_post_id ) || empty( $guid ) ) {
460 return false;
461 }
462
463 // Try fast lookup from cache.
464 $transient_key = "videopress_guids_{$embedded_post_id}";
465 $cached_guids = get_transient( $transient_key );
466
467 if ( false !== $cached_guids ) {
468 return in_array( $guid, (array) $cached_guids, true );
469 }
470
471 // Cache miss: build and cache the GUID list, then check.
472 $guids = self::build_and_cache_post_guids( $embedded_post_id );
473 return in_array( $guid, $guids, true );
474 }
475
476 /**
477 * Determines if the current user can view the provided video. Only ever gets fired if site-wide private videos are enabled.
478 *
479 * Filterable for 3rd party plugins.
480 *
481 * @param string $guid The video id being checked.
482 * @param int $embedded_post_id The post id the video is embedded in or 0.
483 * @param int $selected_plan_id The plan id the earn block this video is embedded in has.
484 */
485 public function is_current_user_authed_for_video( $guid, $embedded_post_id, $selected_plan_id = 0 ) {
486 if ( current_user_can( 'upload_files' ) ) {
487 return $this->filter_is_current_user_authed_for_video( true, $guid, $embedded_post_id );
488 }
489
490 $attachment = false;
491 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
492 $video_info = video_get_info_by_guid( $guid );
493 if ( ! empty( $video_info ) ) {
494 $attachment = get_blog_post( $video_info->blog_id, $video_info->post_id );
495 }
496 } else {
497 $attachment = videopress_get_post_by_guid( $guid );
498 }
499
500 if ( ! $attachment ) {
501 return false;
502 }
503
504 $video_info = video_get_info_by_blogpostid( get_current_blog_id(), $attachment->ID );
505 if ( null === $video_info->guid ) {
506 return false;
507 }
508
509 /*
510 * Default missing privacy_setting to SITE_DEFAULT to avoid an
511 * undefined-property warning and make the site-level fallback explicit.
512 */
513 $privacy_setting = $video_info->privacy_setting ?? VIDEOPRESS_PRIVACY::SITE_DEFAULT;
514
515 $embedded_post_id = (int) $embedded_post_id;
516 if (
517 $embedded_post_id
518 && VIDEOPRESS_PRIVACY::IS_PUBLIC !== $privacy_setting
519 && ! $this->post_embeds_videopress_guid_cached( $embedded_post_id, $guid )
520 ) {
521 $embedded_post_id = 0;
522 }
523
524 $is_user_authed = false;
525
526 // Determine if video is public, private or use site default.
527 switch ( $privacy_setting ) {
528 case VIDEOPRESS_PRIVACY::IS_PUBLIC:
529 $is_user_authed = true;
530 break;
531 case VIDEOPRESS_PRIVACY::IS_PRIVATE:
532 $restriction_details = $this->build_restriction_details( $guid, $embedded_post_id, $selected_plan_id );
533 $is_user_authed = $restriction_details['can_access'];
534 break;
535 case VIDEOPRESS_PRIVACY::SITE_DEFAULT:
536 default:
537 $is_videopress_private_for_site = Data::get_videopress_videos_private_for_site();
538 $is_user_authed = true;
539 if ( $is_videopress_private_for_site ) {
540 $restriction_details = $this->build_restriction_details( $guid, $embedded_post_id, $selected_plan_id );
541 $is_user_authed = $restriction_details['can_access'];
542 }
543 }
544
545 /**
546 * Overrides video view authorization for current user.
547 *
548 * Example of making all videos public:
549 *
550 * function jp_example_override_video_auth( $is_user_authed, $guid ) {
551 * return true
552 * };
553 * add_filter( 'videopress_is_current_user_authed_for_video', 'jp_example_override_video_auth', 10, 2 );
554 *
555 * @param bool $is_user_authed The current user authorization state.
556 * @param string $guid The video's unique identifier.
557 * @param int|null $embedded_post_id The post the video is embedded..
558 *
559 * @return bool
560 */
561 return $this->filter_is_current_user_authed_for_video( $is_user_authed, $guid, $embedded_post_id );
562 }
563
564 /**
565 * Overrides video view authorization for current user.
566 *
567 * @param bool $is_user_authed The current user authorization state.
568 * @param string $guid The video's unique identifier.
569 * @param int|null $embedded_post_id The post the video is embedded..
570 *
571 * @return bool
572 */
573 private function filter_is_current_user_authed_for_video( $is_user_authed, $guid, $embedded_post_id ) {
574 /**
575 * Overrides video view authorization for current user.
576 *
577 * Example of making all videos public:
578 *
579 * function jp_example_override_video_auth( $is_user_authed, $guid ) {
580 * return true
581 * };
582 * add_filter( 'videopress_is_current_user_authed_for_video', 'jp_example_override_video_auth', 10, 2 );
583 *
584 * @param bool $is_user_authed The current user authorization state.
585 * @param string $guid The video's unique identifier.
586 * @param int|null $embedded_post_id The post the video is embedded..
587 *
588 * @return bool
589 */
590 return (bool) apply_filters( 'videopress_is_current_user_authed_for_video', $is_user_authed, $guid, $embedded_post_id );
591 }
592
593 /**
594 * Returns the proper blog id depending on Jetpack or WP.com
595 *
596 * @return int the blog id
597 */
598 public function get_videopress_blog_id() {
599 return \Jetpack_Options::get_option( 'id' );
600 }
601 }
602