PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / extensions / blocks / premium-content / _inc / access-check.php
jetpack / extensions / blocks / premium-content / _inc Last commit date
subscription-service 2 weeks ago access-check.php 2 weeks ago legacy-buttons.php 5 years ago
access-check.php
329 lines
1 <?php
2 /**
3 * Determine access to premium content.
4 *
5 * @package Automattic\Jetpack\Extensions\Premium_Content
6 */
7
8 namespace Automattic\Jetpack\Extensions\Premium_Content;
9
10 use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
11 use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\WPCOM_Online_Subscription_Service;
12
13 require_once __DIR__ . '/subscription-service/include.php';
14
15 /**
16 * Determines if the memberships module is set up.
17 *
18 * @return bool Whether the memberships module is set up.
19 */
20 function membership_checks() {
21 // If Jetpack is not yet configured, don't show anything ...
22 if ( ! class_exists( '\Jetpack_Memberships' ) ) {
23 return false;
24 }
25 // if stripe not connected don't show anything...
26 if ( ! \Jetpack_Memberships::has_connected_account() ) {
27 return false;
28 }
29 return true;
30 }
31
32 /**
33 * Determines if the site has a plan that supports the
34 * Premium Content block.
35 *
36 * @return bool
37 */
38 function required_plan_checks() {
39 $availability = \Jetpack_Gutenberg::get_cached_availability();
40 $slug = 'premium-content/container';
41 return ( isset( $availability[ $slug ] ) && $availability[ $slug ]['available'] );
42 }
43
44 /**
45 * Determines if the block should be rendered. Returns true
46 * if the block passes all required checks, or if the user is
47 * an editor.
48 *
49 * @return bool Whether the block should be rendered.
50 */
51 function pre_render_checks() {
52 return ( current_user_can_edit() || membership_checks() );
53 }
54
55 /**
56 * Determines whether the current user can edit.
57 *
58 * @return bool Whether the user can edit.
59 */
60 function current_user_can_edit() {
61 $user = wp_get_current_user();
62
63 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
64 }
65
66 /**
67 * Mint and persist a fresh premium-content session cookie for the current visitor.
68 *
69 * When the WPCOM Memberships filter returns authoritative subscriptions for a logged-in
70 * visitor but no JWT cookie is present (e.g. the previous one expired or was cleared),
71 * we wrap those subscriptions in a JWT signed with the site's Jetpack blog token and
72 * persist it as the standard `wp-jp-premium-content-session` cookie. Subsequent requests
73 * within the cookie TTL take the fast cached path instead of the WPCOM round-trip.
74 *
75 * Returns the minted JWT so callers can act on it; returns null when no token should be
76 * minted because there are no subscriptions, a valid cookie already exists, or the
77 * signing key is unavailable. The cookie itself is set on a best-effort basis: if headers
78 * have already been sent, the token is still returned so the caller can decide what to do.
79 *
80 * @param object $paywall Subscription service (provides the signing key).
81 * @param int $user_id User id the subscriptions belong to (WPCOM id when available, otherwise local).
82 * @param array $abbreviated_subscriptions Subscriptions after `abbreviate_subscriptions()`.
83 * @return string|null The encoded JWT, or null when no cookie was minted.
84 */
85 function maybe_renew_session_cookie( $paywall, $user_id, $abbreviated_subscriptions ) {
86 if ( empty( $abbreviated_subscriptions ) ) {
87 return null;
88 }
89 if ( Abstract_Token_Subscription_Service::has_token_from_cookie() ) {
90 $session_cookie_name = Abstract_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME;
91 $existing_payload = null;
92 if ( isset( $_COOKIE[ $session_cookie_name ] ) ) {
93 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
94 $existing_payload = $paywall->decode_token( $_COOKIE[ $session_cookie_name ] );
95 }
96 if ( ! empty( $existing_payload ) ) {
97 return null;
98 }
99 }
100
101 $key = $paywall->get_key();
102 if ( ! $key ) {
103 return null;
104 }
105
106 $payload_subscriptions = array();
107 foreach ( $abbreviated_subscriptions as $pid => $sub ) {
108 $sub = (array) $sub;
109 $payload_subscriptions[ $pid ] = array(
110 'status' => 'active',
111 'end_date' => $sub['end_date'] ?? gmdate( 'Y-m-d H:i:s', time() + MONTH_IN_SECONDS ),
112 'product_id' => (int) $pid,
113 );
114 if ( ! empty( $sub['is_comp'] ) ) {
115 $payload_subscriptions[ $pid ]['is_comp'] = true;
116 }
117 }
118
119 $token = JWT::encode(
120 array(
121 'user_id' => $user_id,
122 'blog_sub' => 'active',
123 'subscriptions' => $payload_subscriptions,
124 ),
125 $key
126 );
127
128 if ( ! ( defined( 'TESTING_IN_JETPACK' ) && TESTING_IN_JETPACK ) && ! headers_sent() ) {
129 // phpcs:ignore Jetpack.Functions.SetCookie.FoundNonHTTPOnlyFalse
130 setcookie( Abstract_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME, $token, strtotime( '+1 month' ), '/', '', is_ssl(), false );
131 }
132
133 // Reflect the cookie in the current request so the render path (and any later code in
134 // this request) reads the freshly minted token instead of re-querying the filter.
135 $_COOKIE[ Abstract_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME ] = $token;
136
137 return $token;
138 }
139
140 /**
141 * Resolve the current logged-in visitor's subscriptions from the authoritative
142 * WordPress.com Memberships filter.
143 *
144 * Translates the local user id to the WordPress.com user id (via the `wpcom_user_id`
145 * user_meta populated by Jetpack SSO) and queries the filter with the paywall's
146 * WordPress.com blog id (not get_current_blog_id(), which is the independent local id
147 * on Atomic/WoA). Shared by the access decision and the cookie self-heal.
148 *
149 * @param object $paywall Subscription service (provides the WordPress.com blog id).
150 * @return array{0:int,1:array,2:array} [ user_id (WPCOM id when available, otherwise local), raw_subscriptions, abbreviated_subscriptions ].
151 */
152 function get_subscriptions_for_logged_in_user( $paywall ) {
153 $local_user_id = wp_get_current_user()->ID;
154 $wpcom_user_id = (int) get_user_meta( $local_user_id, 'wpcom_user_id', true );
155 $user_id = $wpcom_user_id > 0 ? $wpcom_user_id : $local_user_id;
156 $site_id = $paywall->get_site_id();
157
158 /**
159 * Filter the subscriptions attached to a specific user on a given site.
160 *
161 * @since 9.4.0
162 *
163 * @param array $subscriptions Array of subscriptions.
164 * @param int $user_id The user's ID.
165 * @param int $site_id ID of the current site.
166 */
167 $raw_subscriptions = apply_filters( 'earn_get_user_subscriptions_for_site_id', array(), $user_id, $site_id );
168 $abbreviated_subscriptions = WPCOM_Online_Subscription_Service::abbreviate_subscriptions( $raw_subscriptions );
169
170 return array( $user_id, $raw_subscriptions, $abbreviated_subscriptions );
171 }
172
173 /**
174 * Pre-warm the premium-content session cookie before output starts.
175 *
176 * `current_visitor_can_access()` runs during `the_content`, by which point headers are
177 * already sent and `setcookie()` is a no-op. This `template_redirect` callback runs earlier —
178 * after the main query resolves but before the template renders — so the self-heal cookie can
179 * actually persist for subsequent requests. It does work only on front-end page views that
180 * contain the block, for logged-in visitors who do not already have a valid cookie.
181 *
182 * @return void
183 */
184 function prewarm_premium_content_session_cookie() {
185 if ( ! is_user_logged_in() ) {
186 return;
187 }
188
189 // Front-end page views only — skip admin, REST, feeds and cron.
190 if ( is_admin() || is_feed() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
191 return;
192 }
193
194 // Only on singular content that actually contains the block, so we never pay the filter
195 // round-trip on unrelated pages.
196 $queried = get_queried_object();
197 if ( ! ( $queried instanceof \WP_Post ) || ! has_block( 'premium-content/container', $queried ) ) {
198 return;
199 }
200
201 // Editors/authors of this post already bypass gating in current_visitor_can_access(), so
202 // there is no cookie to pre-warm for them — skip the Memberships filter round-trip.
203 if ( current_user_can( 'edit_post', $queried->ID ) ) {
204 return;
205 }
206
207 $paywall = subscription_service();
208
209 // Already have a valid cached session? Nothing to heal — and don't query the filter.
210 $existing_payload = $paywall->decode_token( $paywall->get_and_set_token_from_request() );
211 if ( ! empty( $existing_payload ) ) {
212 return;
213 }
214
215 list( $user_id, , $abbreviated_subscriptions ) = get_subscriptions_for_logged_in_user( $paywall );
216 maybe_renew_session_cookie( $paywall, $user_id, $abbreviated_subscriptions );
217 }
218
219 /**
220 * Determines if the current user can view the protected content of the given block.
221 *
222 * @param array $attributes Block attributes.
223 * @param object $block Block to check.
224 *
225 * @return bool Whether the use can view the content.
226 */
227 function current_visitor_can_access( $attributes, $block ) {
228 /**
229 * If the current WordPress install has as signed in user
230 * they can see the content.
231 */
232
233 if ( current_user_can_edit() ) {
234 return true;
235 }
236
237 $selected_plan_ids = array();
238
239 if ( isset( $attributes['selectedPlanIds'] ) ) {
240 $selected_plan_ids = $attributes['selectedPlanIds'];
241 } elseif ( isset( $attributes['selectedPlanId'] ) ) {
242 $selected_plan_ids = array( $attributes['selectedPlanId'] );
243 }
244
245 if ( isset( $block ) && ! empty( $block->context['premium-content/planId'] ) ) {
246 $selected_plan_ids = array( $block->context['premium-content/planId'] );
247 } elseif ( isset( $block ) && ! empty( $block->context['premium-content/planIds'] ) ) {
248 $selected_plan_ids = $block->context['premium-content/planIds'];
249 }
250
251 if ( empty( $selected_plan_ids ) ) {
252 return false;
253 }
254
255 $can_view = false;
256 $paywall = subscription_service();
257 $access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS; // Only paid subscribers should be granted access to the premium content
258 $tier_ids = \Jetpack_Memberships::get_all_newsletter_plan_ids();
259 $tier_ids = array_intersect( $tier_ids, $selected_plan_ids );
260 if ( ! empty( $tier_ids ) ) {
261 // If the selected plan is a tier, we want to check directly if user has a higher "tier".
262 // This is to prevent situation where the user upgrades and lose access to premium-gated content
263
264 $subscriptions = array();
265
266 // Cookie/token first (fast cached path, no WPCOM round-trip). Also covers anonymous
267 // visitors arriving via a `?token=` magic link, which takes precedence over the cookie.
268 $token = $paywall->get_and_set_token_from_request();
269 $payload = $paywall->decode_token( $token );
270 if ( ! empty( $payload ) ) {
271 $subscriptions = (array) $payload['subscriptions'];
272 }
273
274 // No valid cookie yet, but logged in: consult the authoritative WPCOM filter. This is
275 // the first-visit / expired-cookie path; the template_redirect pre-warm hook mints the
276 // cookie for subsequent requests. Keeps CM-584 fixed (expired cookie still grants access).
277 if ( empty( $subscriptions ) && is_user_logged_in() ) {
278 list( , , $subscriptions ) = get_subscriptions_for_logged_in_user( $paywall );
279 }
280
281 foreach ( $tier_ids as $tier_id ) {
282 $can_view = ! $paywall->maybe_gate_access_for_user_if_tier( $tier_id, $subscriptions );
283 if ( $can_view ) {
284 break;
285 }
286 }
287
288 // Refresh-before-deny for the tier path. If the tier check denied access but the
289 // token already references a subscription whose product_id maps to one of the
290 // requested tiers, the most likely cause is a stale end_date from a renewal —
291 // attempt a single refresh against the WordPress.com token-refresh endpoint and
292 // re-check with the fresh subscriptions. Mirrors the same gate inside
293 // visitor_can_view_content() for the non-tier path.
294 if (
295 ! $can_view
296 && method_exists( $paywall, 'token_has_matching_product' )
297 && $paywall->token_has_matching_product( $tier_ids, $subscriptions )
298 ) {
299 $fresh_payload = $paywall->refresh_token_payload();
300 if ( ! empty( $fresh_payload ) ) {
301 $subscriptions = isset( $fresh_payload['subscriptions'] ) ? (array) $fresh_payload['subscriptions'] : array();
302 foreach ( $tier_ids as $tier_id ) {
303 $can_view = ! $paywall->maybe_gate_access_for_user_if_tier( $tier_id, $subscriptions );
304 if ( $can_view ) {
305 break;
306 }
307 }
308 }
309 }
310 }
311
312 $non_tier_ids = array_diff( $selected_plan_ids, $tier_ids );
313 if ( ! $can_view ) {
314 // For selected plans that are not tiers, we want to check if the user has any of the selected plans.
315 $can_view = $paywall->visitor_can_view_content( $non_tier_ids, $access_level );
316 }
317
318 if ( $can_view ) {
319 /**
320 * Fires when a visitor can view protected content on a site.
321 *
322 * @since 9.4.0
323 */
324 do_action( 'jetpack_earn_remove_cache_headers' );
325 }
326
327 return $can_view;
328 }
329