PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.6
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.6
3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 All 199 releases
← All changes | includes/class-convertkit-output-restrict-content.php +1601 -168 2.2.0 → 3.4.6 View file →
@@ -14,17 +14,8 @@
14 14 */
15 15 class ConvertKit_Output_Restrict_Content {
16 16
17 17 /**
18 - * Holds the success message to display on screen as a notification.
19 - *
20 - * @since 2.1.0
21 - *
22 - * @var bool|string
23 - */
24 - private $success = false; // @phpstan-ignore-line.
25 -
26 - /**
27 18 * Holds the WP_Error object if an API call / authentication failed,
28 19 * to display on screen as a notification.
29 20 *
30 21 * @since 2.1.0
@@ -30,9 +21,9 @@
30 21 * @since 2.1.0
31 22 *
32 23 * @var bool|WP_Error
33 24 */
34 - private $error = false; // @phpstan-ignore-line.
25 + public $error = false;
35 26
36 27 /**
37 28 * Holds the ConvertKit Plugin Settings class
38 29 *
@@ -39,9 +30,9 @@
39 30 * @since 2.1.0
40 31 *
41 32 * @var bool|ConvertKit_Settings
42 33 */
43 - private $settings = false;
34 + public $settings = false;
44 35
45 36 /**
46 37 * Holds the ConvertKit Restrict Content Settings class
47 38 *
@@ -48,9 +39,9 @@
48 39 * @since 2.1.0
49 40 *
50 41 * @var bool|ConvertKit_Settings_Restrict_Content
51 42 */
52 - private $restrict_content_settings = false;
43 + public $restrict_content_settings = false;
53 44
54 45 /**
55 46 * Holds the ConvertKit Post Settings class
56 47 *
@@ -57,11 +48,31 @@
57 48 * @since 2.1.0
58 49 *
59 50 * @var bool|ConvertKit_Post
60 51 */
61 - private $post_settings = false;
52 + public $post_settings = false;
62 53
63 54 /**
55 + * Holds the Resource Type (product|tag) that must be subscribed to in order
56 + * to grant access to the Post.
57 + *
58 + * @since 2.3.8
59 + *
60 + * @var bool|string
61 + */
62 + public $resource_type = false;
63 +
64 + /**
65 + * Holds the Resource ID that must be subscribed to in order
66 + * to grant access to the Post.
67 + *
68 + * @since 2.3.8
69 + *
70 + * @var bool|int
71 + */
72 + public $resource_id = false;
73 +
74 + /**
64 75 * Holds the Post ID
65 76 *
66 77 * @since 2.1.0
67 78 *
@@ -66,9 +77,9 @@
66 77 * @since 2.1.0
67 78 *
68 79 * @var bool|int
69 80 */
70 - private $post_id = false;
81 + public $post_id = false;
71 82
72 83 /**
73 84 * Holds the ConvertKit API class
74 85 *
@@ -73,11 +84,11 @@
73 84 * Holds the ConvertKit API class
74 85 *
75 86 * @since 2.1.0
76 87 *
77 - * @var bool|ConvertKit_API
88 + * @var bool|ConvertKit_API_V4
78 89 */
79 - private $api = false;
90 + public $api = false;
80 91
81 92 /**
82 93 * Holds the token returned from calling the subscriber_authentication_send_code API endpoint.
83 94 *
@@ -84,11 +95,20 @@
84 95 * @since 2.1.0
85 96 *
86 97 * @var bool|string
87 98 */
88 - private $token = false;
99 + public $token = false;
89 100
90 101 /**
102 + * Whether the login modal has been output.
103 + *
104 + * @since 3.4.2
105 + *
106 + * @var bool
107 + */
108 + public $login_modal_output = false;
109 +
110 + /**
91 111 * Constructor. Registers actions and filters to possibly limit output of a Page/Post/CPT's
92 112 * content on the frontend site.
93 113 *
94 114 * @since 2.1.0
@@ -94,20 +114,14 @@
94 114 * @since 2.1.0
95 115 */
96 116 public function __construct() {
97 117
98 - // Initialize classes that will be used.
99 - $this->settings = new ConvertKit_Settings();
100 - $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content();
101 -
102 - // Bail if Restrict Content isn't enabled.
103 - if ( ! $this->restrict_content_settings->enabled() ) {
104 - return;
105 - }
106 -
107 - add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 1 );
108 - add_action( 'init', array( $this, 'maybe_run_subscriber_verification' ), 2 );
109 - add_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
118 + add_action( 'rest_api_init', array( $this, 'register_routes' ) );
119 + add_action( 'init', array( $this, 'initialize_classes' ), 2 );
120 + add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 );
121 + add_action( 'wp', array( $this, 'maybe_run_subscriber_logout' ), 3 );
122 + add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 );
123 + add_action( 'wp', array( $this, 'register_content_filter' ), 5 );
110 124 add_filter( 'get_previous_post_where', array( $this, 'maybe_change_previous_post_where_clause' ), 10, 5 );
111 125 add_filter( 'get_next_post_where', array( $this, 'maybe_change_next_post_where_clause' ), 10, 5 );
112 126 add_filter( 'get_previous_post_sort', array( $this, 'maybe_change_previous_next_post_order_by_clause' ), 10, 3 );
113 127 add_filter( 'get_next_post_sort', array( $this, 'maybe_change_previous_next_post_order_by_clause' ), 10, 3 );
@@ -114,63 +128,367 @@
114 128
115 129 }
116 130
117 131 /**
118 - * Checks if the request is a Restrict Content login request with an email address,
119 - * calling the API to send the subscriber a magic link by email.
132 + * Register REST API routes.
120 133 *
121 - * Once they click the link in the email, maybe_run_subscriber_verification() will run.
134 + * @since 3.1.0
135 + */
136 + public function register_routes() {
137 +
138 + // Register route to run subscriber authentication.
139 + register_rest_route(
140 + 'kit/v1',
141 + '/restrict-content/subscriber-authentication',
142 + array(
143 + 'methods' => WP_REST_Server::CREATABLE,
144 + 'args' => array(
145 + // Email: Validate email is included in the request, is a valid email address
146 + // and sanitize the email address.
147 + 'convertkit_email' => array(
148 + 'required' => true,
149 + 'validate_callback' => function ( $param ) {
150 +
151 + return is_string( $param ) && is_email( $param );
152 +
153 + },
154 + 'sanitize_callback' => 'sanitize_email',
155 + ),
156 +
157 + // Post ID: Validate post ID is included in the request and is an integer.
158 + 'convertkit_post_id' => array(
159 + 'required' => true,
160 + 'validate_callback' => function ( $param ) {
161 +
162 + return is_numeric( $param );
163 +
164 + },
165 + 'sanitize_callback' => 'absint',
166 + ),
167 +
168 + // Resource Type: Validate resource type is a string, if included in the request.
169 + // It's not included when logging in using the Member Content Login block.
170 + 'convertkit_resource_type' => array(
171 + 'required' => false,
172 + 'validate_callback' => function ( $param ) {
173 +
174 + return is_string( $param );
175 +
176 + },
177 + 'sanitize_callback' => 'sanitize_text_field',
178 + ),
179 +
180 + // Resource ID: Validate resource ID is an integer, if included in the request.
181 + // It's not included when logging in using the Member Content Login block.
182 + 'convertkit_resource_id' => array(
183 + 'required' => false,
184 + 'validate_callback' => function ( $param ) {
185 +
186 + return is_numeric( $param );
187 +
188 + },
189 + 'sanitize_callback' => 'absint',
190 + ),
191 +
192 + // Spam protection response, if a spam protection provider is enabled.
193 + 'spam_protection_response' => array(
194 + 'required' => false,
195 + 'validate_callback' => function ( $param ) {
196 +
197 + return is_string( $param );
198 +
199 + },
200 + 'sanitize_callback' => 'sanitize_text_field',
201 + ),
202 +
203 + // Whether to display the heading above the login form.
204 + // It's not displayed by the Member Content Login block, as it refers to
205 + // reading the Member Content the subscriber is logging in to view.
206 + 'display_heading' => array(
207 + 'required' => false,
208 + 'default' => true,
209 + 'validate_callback' => function ( $param ) {
210 +
211 + return is_bool( $param );
212 +
213 + },
214 + ),
215 + ),
216 + 'callback' => function ( $request ) {
217 +
218 + // Initialize classes that will be used.
219 + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' );
220 + $output_restrict_content->initialize_classes();
221 +
222 + // Fetch Post ID, Resource Type and Resource ID for the view.
223 + $email = $request->get_param( 'convertkit_email' );
224 + $post_id = $request->get_param( 'convertkit_post_id' );
225 + $resource_type = $request->get_param( 'convertkit_resource_type' );
226 + $resource_id = $request->get_param( 'convertkit_resource_id' );
227 +
228 + // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings).
229 + $result = $output_restrict_content->verify_spam_protection( $request->get_param( 'spam_protection_response' ) );
230 +
231 + // If spam protection failed, build the email form view with the error message.
232 + if ( is_wp_error( $result ) ) {
233 + $output_restrict_content->error = $result;
234 +
235 + ob_start();
236 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/' . ( $request->get_param( 'display_heading' ) ? 'login-modal-content-email.php' : 'login-email.php' );
237 + $output = trim( ob_get_clean() );
238 + return rest_ensure_response(
239 + array(
240 + 'success' => false,
241 + 'data' => $output,
242 + )
243 + );
244 + }
245 +
246 + // Run subscriber authentication.
247 + $result = $output_restrict_content->subscriber_authentication_send_code(
248 + $email,
249 + $post_id
250 + );
251 +
252 + // If an error occurred, build the email form view with the error message.
253 + if ( is_wp_error( $result ) ) {
254 + // Set error to display on screen.
255 + $output_restrict_content->error = $result;
256 +
257 + // Build email form view to return for output with error message.
258 + ob_start();
259 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/' . ( $request->get_param( 'display_heading' ) ? 'login-modal-content-email.php' : 'login-email.php' );
260 + $output = trim( ob_get_clean() );
261 + return rest_ensure_response(
262 + array(
263 + 'success' => false,
264 + 'data' => $output,
265 + )
266 + );
267 + }
268 +
269 + // Set token and Post ID for authentication code view.
270 + $output_restrict_content->token = $result;
271 + $output_restrict_content->post_id = $post_id;
272 +
273 + // Build authentication code view to return for output.
274 + ob_start();
275 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php';
276 + $output = trim( ob_get_clean() );
277 + return rest_ensure_response(
278 + array(
279 + 'success' => true,
280 + 'data' => $output,
281 + )
282 + );
283 + },
284 +
285 + // No authentication required, as this is on the frontend site.
286 + 'permission_callback' => '__return_true',
287 + )
288 + );
289 +
290 + // Register route to run subscriber verification.
291 + register_rest_route(
292 + 'kit/v1',
293 + '/restrict-content/subscriber-verification',
294 + array(
295 + 'methods' => WP_REST_Server::CREATABLE,
296 + 'args' => array(
297 + // Post ID: Validate post ID is an integer if included in the request.
298 + 'convertkit_post_id' => array(
299 + 'required' => false,
300 + 'validate_callback' => function ( $param ) {
301 +
302 + return is_numeric( $param );
303 +
304 + },
305 + 'sanitize_callback' => 'absint',
306 + ),
307 +
308 + // Token: Validate token is included in the request and is a string.
309 + 'token' => array(
310 + 'required' => true,
311 + 'validate_callback' => function ( $param ) {
312 +
313 + return is_string( $param );
314 +
315 + },
316 + 'sanitize_callback' => 'sanitize_text_field',
317 + ),
318 +
319 + // Subscriber Code: Validate subscriber code is included in the request and is a string.
320 + 'subscriber_code' => array(
321 + 'required' => true,
322 + 'validate_callback' => function ( $param ) {
323 +
324 + return is_string( $param );
325 +
326 + },
327 + 'sanitize_callback' => 'sanitize_text_field',
328 + ),
329 + ),
330 + 'callback' => function ( $request ) {
331 +
332 + // Initialize classes that will be used.
333 + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' );
334 + $output_restrict_content->initialize_classes();
335 +
336 + // Fetch Post ID, Resource Type and Resource ID for the view.
337 + $post_id = $request->get_param( 'convertkit_post_id' );
338 + $token = $request->get_param( 'token' );
339 + $subscriber_code = $request->get_param( 'subscriber_code' );
340 +
341 + // Run subscriber authentication.
342 + $result = $output_restrict_content->subscriber_authentication_verify( $post_id, $token, $subscriber_code );
343 +
344 + // If an error occurred, build the code form view with the error message.
345 + if ( is_wp_error( $result ) ) {
346 + // Set error to display on screen.
347 + $output_restrict_content->error = $result;
348 +
349 + // Set token and post ID for authentication code view.
350 + $output_restrict_content->token = $token;
351 + $output_restrict_content->post_id = $post_id;
352 +
353 + // Build code form view to return for output with error message.
354 + ob_start();
355 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php';
356 + $output = trim( ob_get_clean() );
357 + return rest_ensure_response(
358 + array(
359 + 'success' => false,
360 + 'data' => $output,
361 + )
362 + );
363 + }
364 +
365 + // Return success with the URL to the Post, including the `ck-cache-bust` parameter.
366 + return rest_ensure_response(
367 + array(
368 + 'success' => true,
369 + 'url' => $output_restrict_content->get_url( $post_id, true ),
370 + )
371 + );
372 + },
373 +
374 + // No authentication required, as this is on the frontend site.
375 + 'permission_callback' => '__return_true',
376 + )
377 + );
378 + }
379 +
380 + /**
381 + * Initialize classes that will be used.
122 382 *
383 + * @since 3.1.0
384 + */
385 + public function initialize_classes() {
386 +
387 + $this->settings = new ConvertKit_Settings();
388 + $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content();
389 + $this->api = new ConvertKit_API_V4(
390 + CONVERTKIT_OAUTH_CLIENT_ID,
391 + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
392 + $this->settings->get_access_token(),
393 + $this->settings->get_refresh_token(),
394 + $this->settings->debug_enabled(),
395 + 'restrict_content'
396 + );
397 +
398 + }
399 +
400 + /**
401 + * If the user isn't using JavaScript, or the Plugin's Disable JS is enabled, checks if the request is a Restrict Content request with an email address.
402 + * If so, calls the API depending on the Restrict Content resource that's required:
403 + * - tag: subscribes the email address to the tag, and calls the API to send the subscriber a magic link by email containing a code.
404 + * - form + product: calls the API to send the subscriber a magic link by email containing a code.
405 + *
406 + * See maybe_run_subscriber_verification() for logic once they click the link in the email or enter the code on screen.
407 + *
123 408 * @since 2.1.0
124 409 */
125 410 public function maybe_run_subscriber_authentication() {
126 411
127 - // Bail if no nonce was specified.
412 + // Bail if no nonce was specified via form submission.
128 413 if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) {
129 414 return;
130 415 }
131 416
132 - // Bail if the nonce failed validation.
417 + // Bail if the request is a form submission and the nonce failed validation.
133 418 if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) {
134 - $this->error = new WP_Error( 'convertkit_output_restrict_content_error', __( 'Invalid nonce specified. Please try again.', 'convertkit' ) );
135 419 return;
136 420 }
137 421
138 - // If the Plugin API keys have not been configured, we can't get this subscriber's ID by email.
139 - if ( ! $this->settings->has_api_key_and_secret() ) {
422 + // Bail if the expected email or Post ID are missing from the request.
423 + if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) {
140 424 return;
141 425 }
426 + if ( ! array_key_exists( 'convertkit_post_id', $_REQUEST ) ) {
427 + return;
428 + }
142 429
143 - // Initialize the API.
144 - $this->api = new ConvertKit_API( $this->settings->get_api_key(), $this->settings->get_api_secret(), $this->settings->debug_enabled() );
430 + // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email.
431 + if ( ! $this->settings->has_access_and_refresh_token() ) {
432 + return;
433 + }
145 434
146 - // Send email to subscriber with a link to authenticate they have access to the email address submitted.
147 - $result = $this->api->subscriber_authentication_send_code(
148 - sanitize_text_field( $_REQUEST['convertkit_email'] ),
149 - $this->get_url()
150 - );
435 + // Sanitize inputs.
436 + $email = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_email'] ) );
437 + $this->resource_type = ( array_key_exists( 'convertkit_resource_type', $_REQUEST ) ? sanitize_text_field( wp_unslash( $_REQUEST['convertkit_resource_type'] ) ) : '' );
438 + $this->resource_id = ( array_key_exists( 'convertkit_resource_id', $_REQUEST ) ? absint( $_REQUEST['convertkit_resource_id'] ) : 0 );
439 + $this->post_id = absint( $_REQUEST['convertkit_post_id'] );
151 440
152 - // Bail if an error occured.
441 + // If Restrict Content is by tag, tag the subscriber.
442 + if ( $this->resource_type === 'tag' ) {
443 + // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings).
444 + $spam_protection = new ConvertKit_Spam_Protection();
445 + $spam_check = $spam_protection->verify( 'convertkit_restrict_content_tag' );
446 +
447 + // Bail if spam protection failed.
448 + if ( is_wp_error( $spam_check ) ) {
449 + $this->error = $spam_check;
450 + return;
451 + }
452 +
453 + // Tag subscriber.
454 + $result = $this->api->tag_subscribe( $this->resource_id, $email );
455 +
456 + // Bail if an error occurred.
457 + if ( is_wp_error( $result ) ) {
458 + $this->error = $result;
459 + return;
460 + }
461 + } else {
462 + // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings).
463 + $spam_check = $this->verify_spam_protection();
464 +
465 + // Bail if spam protection failed.
466 + if ( is_wp_error( $spam_check ) ) {
467 + $this->error = $spam_check;
468 + return;
469 + }
470 + }
471 +
472 + // Run subscriber authentication.
473 + $result = $this->subscriber_authentication_send_code( $email, $this->post_id );
474 +
475 + // Bail if an error occurred.
153 476 if ( is_wp_error( $result ) ) {
154 477 $this->error = $result;
155 478 return;
156 479 }
157 480
158 - // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email.
159 - $subscriber = new ConvertKit_Subscriber();
160 - $subscriber->forget();
161 -
162 481 // Store the token so it's included in the subscriber code form.
163 482 $this->token = $result;
164 483
165 - // Show a message telling the subscriber to check their email and click the link in the email.
166 - $this->success = $this->restrict_content_settings->get_by_key( 'email_check_text' );
167 -
168 484 }
169 485
170 486 /**
171 - * Checks if the request contains a token and subscriber_code i.e. the subscriber clicked
172 - * the link in the email sent by the maybe_run_subscriber_authentication() function above.
487 + * If the user isn't using JavaScript, or the Plugin's Disable JS is enabled, checks if the request contains a token and subscriber_code,
488 + * which happens when the subscriber either:
489 + * - clicked the link in the email sent by run_subscriber_authentication(), or
490 + * - entered the code from the email on the screen
173 491 *
174 492 * This calls the API to verify the token and subscriber code, which tells us that the email
175 493 * address supplied truly belongs to the user, and that we can safely trust their subscriber ID
176 494 * to be valid.
@@ -178,9 +496,8 @@
178 496 * @since 2.1.0
179 497 */
180 498 public function maybe_run_subscriber_verification() {
181 499
182 - // phpcs:disable WordPress.Security.NonceVerification.Recommended
183 500 // Bail if the expected token and subscriber code is missing.
184 501 if ( ! array_key_exists( 'token', $_REQUEST ) ) {
185 502 return;
186 503 }
@@ -187,60 +504,268 @@
187 504 if ( ! array_key_exists( 'subscriber_code', $_REQUEST ) ) {
188 505 return;
189 506 }
190 507
191 - // If the Plugin API keys have not been configured, we can't get this subscriber's ID by email.
192 - if ( ! $this->settings->has_api_key_and_secret() ) {
508 + // If a nonce was specified, validate it now.
509 + // It won't be provided if clicking the link in the magic link email.
510 + if ( array_key_exists( '_wpnonce', $_REQUEST ) && ! is_null( $_REQUEST['_wpnonce'] ) ) {
511 + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_subscriber_code' ) ) {
512 + return;
513 + }
514 + }
515 +
516 + // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email.
517 + if ( ! $this->settings->has_access_and_refresh_token() ) {
193 518 return;
194 519 }
195 520
196 521 // Store the token so it's included in the subscriber code form if verification fails.
197 - $this->token = sanitize_text_field( $_REQUEST['token'] );
522 + $this->token = sanitize_text_field( wp_unslash( $_REQUEST['token'] ) );
198 523
199 - // Initialize the API.
200 - $this->api = new ConvertKit_API( $this->settings->get_api_key(), $this->settings->get_api_secret(), $this->settings->debug_enabled() );
524 + // Store the post ID if this is an AJAX request.
525 + // This won't be included if clicking the link in the magic link email, so fall back to using
526 + // get_the_ID() to get the post ID.
527 + if ( array_key_exists( 'convertkit_post_id', $_REQUEST ) ) {
528 + $this->post_id = absint( wp_unslash( $_REQUEST['convertkit_post_id'] ) );
529 + } else {
530 + $this->post_id = get_the_ID();
531 + }
201 532
202 - // Verify the token and subscriber code.
203 - $subscriber_id = $this->api->subscriber_authentication_verify(
204 - sanitize_text_field( $_REQUEST['token'] ),
205 - sanitize_text_field( $_REQUEST['subscriber_code'] )
206 - );
207 - // phpcs:enable
533 + // Run subscriber verification.
534 + $subscriber_id = $this->subscriber_authentication_verify( $this->post_id, sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ), sanitize_text_field( wp_unslash( $_REQUEST['subscriber_code'] ) ) );
208 535
209 - // Bail if an error occured.
536 + // Bail if an error occurred.
210 537 if ( is_wp_error( $subscriber_id ) ) {
211 538 $this->error = $subscriber_id;
212 539 return;
213 540 }
214 541
215 - // Store subscriber ID in cookie.
216 - // We don't need to use validate_and_store_subscriber_id() as we just validated the subscriber via authentication above.
542 + // Redirect now to reload the Post.
543 + $this->redirect( $this->post_id );
544 +
545 + }
546 +
547 + /**
548 + * Logs the subscriber out by deleting their subscriber ID cookie, when the
549 + * log out button is clicked in the Member Content Login block.
550 + *
551 + * @since 3.4.2
552 + */
553 + public function maybe_run_subscriber_logout() {
554 +
555 + // Bail if no logout request was made.
556 + if ( ! array_key_exists( 'convertkit_logout', $_REQUEST ) ) {
557 + return;
558 + }
559 +
560 + // Bail if no nonce was specified.
561 + if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) {
562 + return;
563 + }
564 +
565 + // Bail if the nonce failed validation.
566 + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_member_content_logout' ) ) {
567 + return;
568 + }
569 +
570 + // Delete the subscriber ID cookie.
217 571 $subscriber = new ConvertKit_Subscriber();
218 - $subscriber->set( $subscriber_id );
572 + $subscriber->forget();
219 573
220 - // We append a query parameter to the URL to prevent caching plugins and
221 - // aggressive cache hosting configurations from serving a cached page, which would
222 - // result in maybe_restrict_content() not showing an error message or permitting
223 - // access to the content.
224 - $url = add_query_arg(
574 + // Reload the Post, so the login form displays.
575 + wp_safe_redirect( $this->get_url( get_the_ID(), true ) );
576 + exit();
577 +
578 + }
579 +
580 + /**
581 + * Verifies the spam protection response for the login form, using the spam
582 + * protection provider enabled in the Plugin's settings.
583 + *
584 + * @since 3.4.2
585 + *
586 + * @param bool|string $response Spam protection response, if supplied by a REST API request.
587 + * @return bool|WP_Error
588 + */
589 + public function verify_spam_protection( $response = false ) {
590 +
591 + $spam_protection = new ConvertKit_Spam_Protection();
592 + $provider = $spam_protection->get_active_provider();
593 +
594 + // Return true if no spam protection provider is enabled.
595 + if ( $provider === false ) {
596 + return true;
597 + }
598 +
599 + // Verify the response included in the REST API request.
600 + if ( ! empty( $response ) ) {
601 + return $provider->verify( $response, 'convertkit_member_content_login' );
602 + }
603 +
604 + // Verify the response included in the form submission.
605 + return $spam_protection->verify( 'convertkit_member_content_login' );
606 +
607 + }
608 +
609 + /**
610 + * Enqueues the CSS and JS required by the login form and modal.
611 + *
612 + * @since 3.4.2
613 + */
614 + public function enqueue_scripts_and_styles() {
615 +
616 + // Only load styles if the Disable CSS option is off.
617 + if ( ! $this->settings->css_disabled() ) {
618 + convertkit_enqueue_frontend_css();
619 + }
620 +
621 + // Bail if scripts are disabled.
622 + if ( $this->settings->scripts_disabled() ) {
623 + return;
624 + }
625 +
626 + // Enqueue scripts.
627 + convertkit_enqueue_frontend_js();
628 +
629 + // Define variables.
630 + wp_localize_script(
631 + 'convertkit-js',
632 + 'convertkit_restrict_content',
225 633 array(
226 - 'ck-cache-bust' => microtime(),
227 - ),
228 - $this->get_url()
634 + 'nonce' => wp_create_nonce( 'wp_rest' ),
635 + 'subscriber_authentication_url' => rest_url( 'kit/v1/restrict-content/subscriber-authentication' ),
636 + 'subscriber_verification_url' => rest_url( 'kit/v1/restrict-content/subscriber-verification' ),
637 + 'debug' => $this->settings->debug_enabled(),
638 + )
229 639 );
230 640
231 - // Redirect to the Post without the token and subscriber parameters.
232 - // This will then run maybe_restrict_content() to get the subscriber's ID from the cookie,
233 - // and determine if the content can be displayed.
234 - wp_safe_redirect( $url );
235 - exit;
641 + // Enqueue the active spam protection provider's client-side script, used by the login form.
642 + $spam_protection = new ConvertKit_Spam_Protection();
643 + $spam_provider = $spam_protection->get_active_provider();
644 + if ( $spam_provider !== false ) {
645 + $spam_provider->enqueue_scripts();
646 + }
236 647
237 648 }
238 649
239 650 /**
651 + * Outputs the login modal in the footer, ensuring it is only output once
652 + * when a Post contains multiple Member Content Login blocks.
653 + *
654 + * @since 3.4.2
655 + *
656 + * @param int $post_id Post ID.
657 + * @param bool|int $resource_id Resource ID.
658 + * @param bool|string $resource_type Resource Type.
659 + */
660 + public function output_login_modal( $post_id, $resource_id = 0, $resource_type = '' ) {
661 +
662 + if ( $this->login_modal_output ) {
663 + return;
664 + }
665 +
666 + $this->login_modal_output = true;
667 +
668 + add_action(
669 + 'wp_footer',
670 + function () use ( $post_id, $resource_id, $resource_type ) {
671 +
672 + include_once CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal.php';
673 +
674 + }
675 + );
676 +
677 + }
678 +
679 + /**
680 + * Sends an email to the subscriber with a code and link to authenticate they have access to the email address submitted.
681 + *
682 + * @since 3.1.0
683 + *
684 + * @param string $email Email address.
685 + * @param int $post_id Post ID.
686 + *
687 + * @return WP_Error|string Error or Token.
688 + */
689 + public function subscriber_authentication_send_code( $email, $post_id ) {
690 +
691 + // Send email to subscriber with a link to authenticate they have access to the email address submitted.
692 + $token = $this->api->subscriber_authentication_send_code(
693 + $email,
694 + $this->get_url( $post_id )
695 + );
696 +
697 + // Bail if an error occurred.
698 + if ( is_wp_error( $token ) ) {
699 + return $token;
700 + }
701 +
702 + // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email.
703 + $subscriber = new ConvertKit_Subscriber();
704 + $subscriber->forget();
705 +
706 + // Return the token.
707 + return $token;
708 +
709 + }
710 +
711 + /**
712 + * Verifies the token and subscriber code, which tells us that the email
713 + * address supplied truly belongs to the user, and that we can safely
714 + * trust their subscriber ID to be valid.
715 + *
716 + * @since 3.1.0
717 + *
718 + * @param int $post_id Post ID.
719 + * @param string $token Token.
720 + * @param string $subscriber_code Subscriber code.
721 + *
722 + * @return WP_Error|string Error or Signed Subscriber ID.
723 + */
724 + public function subscriber_authentication_verify( $post_id, $token, $subscriber_code ) {
725 +
726 + // Verify the token and subscriber code.
727 + $subscriber_id = $this->api->subscriber_authentication_verify( $token, $subscriber_code );
728 +
729 + // Bail if an error occurred.
730 + if ( is_wp_error( $subscriber_id ) ) {
731 + return $subscriber_id;
732 + }
733 +
734 + // Store subscriber ID in cookie.
735 + $this->store_subscriber_id_in_cookie( $subscriber_id );
736 +
737 + // Return signed subscriber ID.
738 + return $subscriber_id;
739 +
740 + }
741 +
742 + /**
743 + * Registers the applicable content filter for maybe restricting content, depending
744 + * on the Theme or Page Builder used.
745 + *
746 + * @since 2.7.7
747 + */
748 + public function register_content_filter() {
749 +
750 + // Use the standard `the_content` filter, which works for most Themes
751 + // and Page Builders.
752 + add_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
753 +
754 + /**
755 + * Allow specific Themes and Page Builders to use a different filter
756 + * for Restrict Content functionality.
757 + *
758 + * @since 2.7.7
759 + */
760 + do_action( 'convertkit_restrict_content_register_content_filter' );
761 +
762 + }
763 +
764 + /**
240 765 * Displays (or hides) content on a singular Page, Post or Custom Post Type's Content,
241 766 * depending on whether the visitor is an authenticated ConvertKit subscriber and has
242 - * subscribed to the ConvertKit Product.
767 + * subscribed to the ConvertKit Product or Tag.
243 768 *
244 769 * @since 2.1.0
245 770 *
246 771 * @param string $content Post Content.
@@ -252,29 +777,42 @@
252 777 if ( ! $this->is_restricted_content() ) {
253 778 return $content;
254 779 }
255 780
256 - // Get resource type (Product) that the visitor must be subscribed against to access this content.
257 - $resource_type = $this->get_resource_type( $this->post_id );
781 + // Bail if the Page is being edited in a frontend Page Builder / Editor by a logged
782 + // in WordPress user who has the capability to edit the Page.
783 + // This ensures the User can view all content to edit it, instead of seeing the Restrict Content
784 + // view.
785 + if ( current_user_can( 'edit_post', get_the_ID() ) && WP_ConvertKit()->is_admin_or_frontend_editor() ) {
786 + return $content;
787 + }
258 788
789 + // Get resource type (Product or Tag) that the visitor must be subscribed against to access this content.
790 + $this->resource_type = $this->get_resource_type();
791 +
259 792 // Return the Post Content, unedited, if the Resource Type is false.
260 - if ( ! $resource_type ) {
793 + if ( ! $this->resource_type ) {
261 794 return $content;
262 795 }
263 796
264 - // Get resource ID (Product ID) that the visitor must be subscribed against to access this content.
265 - $resource_id = $this->get_resource_id( $this->post_id );
797 + // Get resource ID (Product ID or Tag ID) that the visitor must be subscribed against to access this content.
798 + $this->resource_id = $this->get_resource_id();
266 799
267 800 // Return the full Post Content, unedited, if the Resource ID is false, as this means
268 801 // no restrict content setting has been defined for this Post.
269 - if ( ! $resource_id ) {
802 + if ( ! $this->resource_id ) {
270 803 return $content;
271 804 }
272 805
806 + // Return the full Post Content, unedited, if the request is from a crawler.
807 + if ( $this->restrict_content_settings->permit_crawlers() && $this->is_crawler() ) {
808 + return $content;
809 + }
810 +
273 811 // Return if this request is after the user entered their email address,
274 812 // which means we're going through the authentication flow.
275 813 if ( $this->in_authentication_flow() ) {
276 - return $this->restrict_content( $content, $resource_type, $resource_id );
814 + return $this->restrict_content( $content );
277 815 }
278 816
279 817 // Get the subscriber ID, either from the request or an existing cookie.
280 818 $subscriber_id = $this->get_subscriber_id_from_request();
@@ -280,21 +818,37 @@
280 818 $subscriber_id = $this->get_subscriber_id_from_request();
281 819
282 820 // If no subscriber ID exists, the visitor cannot view the content.
283 821 if ( ! $subscriber_id ) {
284 - return $this->restrict_content( $content, $resource_type, $resource_id );
822 + return $this->restrict_content( $content );
285 823 }
286 824
287 825 // If the subscriber is not subscribed to the product, restrict the content.
288 - if ( ! $this->subscriber_has_access( $subscriber_id, $resource_type, $resource_id ) ) {
826 + if ( ! $this->subscriber_has_access( $subscriber_id ) ) {
289 827 // Show an error before the call to action, to tell the subscriber why they still cannot
290 828 // view the content.
829 + switch ( $this->resource_type ) {
830 + case 'form':
831 + $message = $this->restrict_content_settings->get_by_key( 'no_access_text_form' );
832 + break;
833 +
834 + case 'tag':
835 + $message = $this->restrict_content_settings->get_by_key( 'no_access_text_tag' );
836 + break;
837 +
838 + case 'product':
839 + default:
840 + $message = $this->restrict_content_settings->get_by_key( 'no_access_text' );
841 + break;
842 + }
843 +
844 + // Define error for output.
291 845 $this->error = new WP_Error(
292 846 'convertkit_restrict_content_subscriber_no_access',
293 - esc_html( $this->restrict_content_settings->get_by_key( 'no_access_text' ) )
847 + esc_html( $message )
294 848 );
295 849
296 - return $this->restrict_content( $content, $resource_type, $resource_id );
850 + return $this->restrict_content( $content );
297 851 }
298 852
299 853 // If here, the subscriber has subscribed to the product.
300 854 // Show the full Post Content.
@@ -333,9 +887,9 @@
333 887 // Build replacement where statement.
334 888 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order < ' . $post->menu_order;
335 889
336 890 // Replace existing where statement with new statement.
337 - $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND' ) );
891 + $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
338 892
339 893 // Return.
340 894 return $where;
341 895
@@ -372,9 +926,9 @@
372 926 // Build replacement where statement.
373 927 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order > ' . $post->menu_order;
374 928
375 929 // Replace existing where statement with new statement.
376 - $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND' ) );
930 + $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
377 931
378 932 // Return.
379 933 return $where;
380 934
@@ -412,19 +966,72 @@
412 966
413 967 }
414 968
415 969 /**
970 + * Stores the given subscriber ID in the ck_subscriber_id cookie.
971 + *
972 + * @since 2.3.7
973 + *
974 + * @param string|int $subscriber_id Subscriber ID (int if restrict by tag, signed subscriber id string if restrict by product).
975 + */
976 + private function store_subscriber_id_in_cookie( $subscriber_id ) {
977 +
978 + // Store subscriber ID in cookie.
979 + // We don't need to use validate_and_store_subscriber_id() as we just validated the subscriber via authentication above.
980 + $subscriber = new ConvertKit_Subscriber();
981 + $subscriber->set( $subscriber_id );
982 +
983 + }
984 +
985 + /**
986 + * Redirects to the current URL, removing any query parameters (such as tokens), and appending
987 + * a ck-cache-bust query parameter to beat caching plugins.
988 + *
989 + * @since 2.3.7
990 + *
991 + * @param int $post_id Post ID.
992 + */
993 + private function redirect( $post_id ) {
994 +
995 + // Redirect to the Post, appending a query parameter to the URL to prevent caching plugins and
996 + // aggressive cache hosting configurations from serving a cached page, which would
997 + // result in maybe_restrict_content() not showing an error message or permitting
998 + // access to the content.
999 + wp_safe_redirect( $this->get_url( $post_id, true ) );
1000 + exit;
1001 +
1002 + }
1003 +
1004 + /**
416 1005 * Returns the URL for the current request, excluding any query parameters.
417 1006 *
418 1007 * @since 2.1.0
419 1008 *
420 - * @return string URL.
1009 + * @param int $post_id Post ID.
1010 + * @param bool $cache_bust Include `ck-cache-bust` parameter in URL.
1011 + * @return string URL.
421 1012 */
422 - private function get_url() {
1013 + public function get_url( $post_id, $cache_bust = false ) {
423 1014
424 - $url = wp_parse_url( get_site_url() . $_SERVER['REQUEST_URI'] );
425 - return $url['scheme'] . '://' . $url['host'] . $url['path'];
1015 + // Get URL of Post.
1016 + $url = get_permalink( $post_id );
426 1017
1018 + // If no cache busting required, return the URL now.
1019 + if ( ! $cache_bust ) {
1020 + return $url;
1021 + }
1022 +
1023 + // Append a query parameter to the URL to prevent caching plugins and
1024 + // aggressive cache hosting configurations from serving a cached page, which would
1025 + // result in maybe_restrict_content() not showing an error message or permitting
1026 + // access to the content.
1027 + return add_query_arg(
1028 + array(
1029 + 'ck-cache-bust' => microtime(),
1030 + ),
1031 + $url
1032 + );
1033 +
427 1034 }
428 1035
429 1036 /**
430 1037 * Determines if the request is for a WordPress Page that has the Restrict Content
@@ -440,12 +1047,12 @@
440 1047 if ( ! is_singular() ) {
441 1048 return false;
442 1049 }
443 1050
444 - // If a Post ID is already defined in this class, this check has already been performed,
445 - // and the Post's settings class has been initialized.
446 - if ( $this->post_id ) {
447 - return true;
1051 + // If the Plugin Access Token has not been configured, we can't determine the validity of this subscriber ID
1052 + // or which resource(s) they have access to.
1053 + if ( ! $this->settings->has_access_and_refresh_token() ) {
1054 + return false;
448 1055 }
449 1056
450 1057 // Get Post ID.
451 1058 $this->post_id = get_the_ID();
@@ -452,14 +1059,8 @@
452 1059
453 1060 // Initialize Settings and Post Setting classes.
454 1061 $this->post_settings = new ConvertKit_Post( $this->post_id );
455 1062
456 - // If the Plugin API keys have not been configured, we can't determine the validity of this subscriber ID
457 - // or which resource(s) they have access to.
458 - if ( ! $this->settings->has_api_key_and_secret() ) {
459 - return false;
460 - }
461 -
462 1063 // Return whether the Post's settings are set to restrict content.
463 1064 return $this->post_settings->restrict_content_enabled();
464 1065
465 1066 }
@@ -509,13 +1110,15 @@
509 1110 * Get the Post's Restricted Content resource type.
510 1111 *
511 1112 * @since 2.1.0
512 1113 *
513 - * @param int $post_id Post ID.
514 1114 * @return bool|string Resource Type (product).
515 1115 */
516 - private function get_resource_type( $post_id ) {
1116 + private function get_resource_type() {
517 1117
1118 + // Initialize Post Setting classes.
1119 + $this->post_settings = new ConvertKit_Post( $this->post_id );
1120 +
518 1121 // Get resource type.
519 1122 $resource_type = $this->post_settings->get_restrict_content_type();
520 1123
521 1124 /**
@@ -528,9 +1131,9 @@
528 1131 *
529 1132 * @param string $resource_type Resource Type (product)
530 1133 * @param int $post_id Post ID
531 1134 */
532 - $resource_type = apply_filters( 'convertkit_output_restrict_content_get_resource_type', $resource_type, $post_id );
1135 + $resource_type = apply_filters( 'convertkit_output_restrict_content_get_resource_type', $resource_type, $this->post_id );
533 1136
534 1137 // If resource type is blank, set it to false.
535 1138 if ( empty( $resource_type ) ) {
536 1139 $resource_type = false;
@@ -545,13 +1148,15 @@
545 1148 * Get the Post's Restricted Content resource ID.
546 1149 *
547 1150 * @since 2.1.0
548 1151 *
549 - * @param int $post_id Post ID.
550 1152 * @return int Resource ID (product ID).
551 1153 */
552 - private function get_resource_id( $post_id ) {
1154 + private function get_resource_id() {
553 1155
1156 + // Initialize Post Setting classes.
1157 + $this->post_settings = new ConvertKit_Post( $this->post_id );
1158 +
554 1159 // Get resource ID.
555 1160 $resource_id = $this->post_settings->get_restrict_content_id();
556 1161
557 1162 /**
@@ -564,9 +1169,9 @@
564 1169 *
565 1170 * @param int $resource_id Resource ID
566 1171 * @param int $post_id Post ID
567 1172 */
568 - $resource_id = apply_filters( 'convertkit_output_restrict_content_get_resource_id', $resource_id, $post_id );
1173 + $resource_id = apply_filters( 'convertkit_output_restrict_content_get_resource_id', $resource_id, $this->post_id );
569 1174
570 1175 // Return.
571 1176 return $resource_id;
572 1177
@@ -572,50 +1177,87 @@
572 1177
573 1178 }
574 1179
575 1180 /**
576 - * Determines if the given subscriber has an active subscription to
577 - * the given resource and its ID.
1181 + * Queries the API to confirm whether the resource exists.
578 1182 *
579 - * @since 2.1.0
1183 + * @since 2.3.3
580 1184 *
581 - * @param string|int $subscriber_id Signed Subscriber ID or Subscriber ID.
582 - * @param string $resource_type Resource Type (product).
583 - * @param int $resource_id Resource ID (Product ID).
584 - * @return bool Can view restricted content
1185 + * @return bool
585 1186 */
586 - private function subscriber_has_access( $subscriber_id, $resource_type, $resource_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1187 + private function resource_exists() {
587 1188
588 - // Initialize the API.
589 - $this->api = new ConvertKit_API( $this->settings->get_api_key(), $this->settings->get_api_secret(), $this->settings->debug_enabled() );
1189 + switch ( $this->resource_type ) {
590 1190
591 - // Depending on the resource type, determine if the subscriber has access to it.
592 - // This is deliberately a switch statement, because we will likely add in support
593 - // for restrict by tag and form later.
594 - switch ( $resource_type ) {
595 1191 case 'product':
596 - // Get products that the subscriber has access to.
597 - $result = $this->api->profile( $subscriber_id );
1192 + // Get Product.
1193 + $products = new ConvertKit_Resource_Products( 'restrict_content' );
1194 + $product = $products->get_by_id( $this->resource_id );
598 1195
599 - // If an error occured, the subscriber ID is invalid.
600 - if ( is_wp_error( $result ) ) {
1196 + // If the Product does not exist, return false.
1197 + if ( ! $product ) {
601 1198 return false;
602 1199 }
603 1200
604 - // If no products exist, there's no access.
605 - if ( ! $result['products'] || ! count( $result['products'] ) ) {
1201 + // Product exists in ConvertKit.
1202 + return true;
1203 +
1204 + case 'form':
1205 + // Get Form.
1206 + $forms = new ConvertKit_Resource_Forms( 'restrict_content' );
1207 + $form = $forms->get_by_id( $this->resource_id );
1208 +
1209 + // If the Form does not exist, return false.
1210 + if ( ! $form ) {
606 1211 return false;
607 1212 }
608 1213
609 - // Return if the subscriber is not subscribed to the product.
610 - if ( ! in_array( absint( $resource_id ), $result['products'], true ) ) {
1214 + // Form exists in ConvertKit.
1215 + return true;
1216 +
1217 + case 'tag':
1218 + // Get Tag.
1219 + $tags = new ConvertKit_Resource_Tags( 'restrict_content' );
1220 + $tag = $tags->get_by_id( $this->resource_id );
1221 +
1222 + // If the Tag does not exist, return false.
1223 + if ( ! $tag ) {
611 1224 return false;
612 1225 }
613 1226
614 - // If here, the subscriber is subscribed to the product.
1227 + // Tag exists in ConvertKit.
615 1228 return true;
1229 +
1230 + default:
1231 + return false;
1232 +
616 1233 }
617 1234
1235 + }
1236 +
1237 + /**
1238 + * Determines if the given subscriber has an active subscription to
1239 + * the given resource and its ID.
1240 + *
1241 + * @since 2.1.0
1242 + *
1243 + * @param string|int $subscriber_id Signed Subscriber ID or Subscriber ID.
1244 + * @return bool Can view restricted content
1245 + */
1246 + private function subscriber_has_access( $subscriber_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1247 +
1248 + switch ( $this->resource_type ) {
1249 + case 'product':
1250 + return $this->subscriber_has_access_to_product_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1251 +
1252 + case 'form':
1253 + return $this->subscriber_has_access_to_form_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1254 +
1255 + case 'tag':
1256 + return $this->subscriber_has_access_to_tag_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1257 +
1258 + }
1259 +
618 1260 // If here, the subscriber does not have access.
619 1261 return false;
620 1262
621 1263 }
@@ -620,8 +1262,98 @@
620 1262
621 1263 }
622 1264
623 1265 /**
1266 + * Determines if the given signed subscriber ID has an active subscription to
1267 + * the given product.
1268 + *
1269 + * @since 2.7.1
1270 + *
1271 + * @param string $signed_subscriber_id Signed Subscriber ID.
1272 + * @param int $product_id Product ID.
1273 + * @return bool Has access to product
1274 + */
1275 + private function subscriber_has_access_to_product_by_signed_subscriber_id( $signed_subscriber_id, $product_id ) {
1276 +
1277 + // Get products that the subscriber has access to.
1278 + $result = $this->api->profile( $signed_subscriber_id );
1279 +
1280 + // If an error occurred, the subscriber ID is invalid.
1281 + if ( is_wp_error( $result ) ) {
1282 + return false;
1283 + }
1284 +
1285 + // If no products exist, there's no access.
1286 + if ( ! $result['products'] || ! count( $result['products'] ) ) {
1287 + return false;
1288 + }
1289 +
1290 + // Return if the subscriber is subscribed to the product or not.
1291 + return in_array( $product_id, $result['products'], true );
1292 +
1293 + }
1294 +
1295 + /**
1296 + * Determines if the given signed subscriber ID has an active subscription to
1297 + * the given form.
1298 + *
1299 + * @since 2.7.3
1300 + *
1301 + * @param string $signed_subscriber_id Signed Subscriber ID.
1302 + * @param int $form_id Form ID.
1303 + * @return bool Has access to form
1304 + */
1305 + private function subscriber_has_access_to_form_by_signed_subscriber_id( $signed_subscriber_id, $form_id ) {
1306 +
1307 + // Get products that the subscriber has access to.
1308 + $result = $this->api->profile( $signed_subscriber_id );
1309 +
1310 + // If an error occurred, the subscriber ID is invalid.
1311 + if ( is_wp_error( $result ) ) {
1312 + return false;
1313 + }
1314 +
1315 + // If no forms exist, there's no access.
1316 + if ( ! $result['forms'] || ! count( $result['forms'] ) ) {
1317 + return false;
1318 + }
1319 +
1320 + // Return if the subscriber is subscribed to the form or not.
1321 + return in_array( $form_id, $result['forms'], true );
1322 +
1323 + }
1324 +
1325 + /**
1326 + * Determines if the given signed subscriber ID has an active subscription to
1327 + * the given tag.
1328 + *
1329 + * @since 2.7.1
1330 + *
1331 + * @param string $signed_subscriber_id Signed Subscriber ID.
1332 + * @param int $tag_id Tag ID.
1333 + * @return bool Has access to tag
1334 + */
1335 + private function subscriber_has_access_to_tag_by_signed_subscriber_id( $signed_subscriber_id, $tag_id ) {
1336 +
1337 + // Get products that the subscriber has access to.
1338 + $result = $this->api->profile( $signed_subscriber_id );
1339 +
1340 + // If an error occurred, the subscriber ID is invalid.
1341 + if ( is_wp_error( $result ) ) {
1342 + return false;
1343 + }
1344 +
1345 + // If no tags exist, there's no access.
1346 + if ( ! $result['tags'] || ! count( $result['tags'] ) ) {
1347 + return false;
1348 + }
1349 +
1350 + // Return if the subscriber is subscribed to the tag or not.
1351 + return in_array( $tag_id, $result['tags'], true );
1352 +
1353 + }
1354 +
1355 + /**
624 1356 * Gets the subscriber ID from the request (either the cookie or the URL).
625 1357 *
626 1358 * @since 2.1.0
627 1359 *
@@ -632,9 +1364,9 @@
632 1364 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
633 1365 $subscriber = new ConvertKit_Subscriber();
634 1366 $subscriber_id = $subscriber->get_subscriber_id();
635 1367
636 - // If an error occured, the subscriber ID in the request/cookie is not a valid subscriber.
1368 + // If an error occurred, the subscriber ID in the request/cookie is not a valid subscriber.
637 1369 if ( is_wp_error( $subscriber_id ) ) {
638 1370 return 0;
639 1371 }
640 1372
@@ -648,16 +1380,73 @@
648 1380 *
649 1381 * @since 2.1.0
650 1382 *
651 1383 * @param string $content Post Content.
652 - * @param string $resource_type Resource Type (product).
653 - * @param int $resource_id Resource ID (Product ID).
654 - * @return string Post Content preview with call to action
1384 + * @return string Post Content preview with call to action
655 1385 */
656 - private function restrict_content( $content, $resource_type, $resource_id ) {
1386 + private function restrict_content( $content ) {
657 1387
658 - return $this->get_content_preview( $content ) . $this->get_call_to_action( $this->post_id, $resource_type, $resource_id );
1388 + // Check that the resource exists before restricting the content.
1389 + // This handles cases where e.g. a Tag or Product has been deleted in ConvertKit,
1390 + // but the Page / Post still references the (now deleted) resource to restrict content with
1391 + // under the 'Member Content' setting.
1392 + if ( ! $this->resource_exists() ) {
1393 + // Return the full Post Content, as we can't restrict it to a Product or Tag that no longer exists.
1394 + return $content;
1395 + }
659 1396
1397 + // Fetch the content preview.
1398 + $content_preview = $this->get_content_preview( $content );
1399 +
1400 + /**
1401 + * Define the output for the content preview when the visitor is not
1402 + * an authenticated subscriber.
1403 + *
1404 + * @since 2.4.1
1405 + *
1406 + * @param string $content_preview Content preview.
1407 + * @param int $post_id Post ID.
1408 + */
1409 + $content_preview = apply_filters( 'convertkit_output_restrict_content_content_preview', $content_preview, $this->post_id );
1410 +
1411 + // Fetch the call to action.
1412 + $call_to_action = $this->get_call_to_action( $this->post_id );
1413 +
1414 + /**
1415 + * Define the output for the call to action, displayed below the content preview,
1416 + * when the visitor is not an authenticated subscriber.
1417 + *
1418 + * @since 2.4.1
1419 + *
1420 + * @param string $call_to_action Call to Action.
1421 + * @param int $post_id Post ID.
1422 + */
1423 + $call_to_action = apply_filters( 'convertkit_output_restrict_content_call_to_action', $call_to_action, $this->post_id );
1424 +
1425 + // Fetch container CSS classes.
1426 + $container_css_classes = explode( ' ', $this->restrict_content_settings->get_by_key( 'container_css_classes' ) );
1427 +
1428 + /**
1429 + * Define the container CSS classes to wrap the content preview and call to action within.
1430 + *
1431 + * @since 3.1.4
1432 + *
1433 + * @param array $container_css_classes Container CSS classes.
1434 + * @param int $post_id Post ID.
1435 + */
1436 + $container_css_classes = apply_filters( 'convertkit_output_restrict_content_container_css_classes', $container_css_classes, $this->post_id );
1437 +
1438 + // Remove empty CSS classes.
1439 + $container_css_classes = array_filter( $container_css_classes );
1440 +
1441 + // If container CSS classes are set, return the content preview and call to action wrapped in the container.
1442 + if ( count( $container_css_classes ) ) {
1443 + return '<div class="' . trim( implode( ' ', map_deep( $container_css_classes, 'sanitize_html_class' ) ) ) . '">' . $content_preview . $call_to_action . '</div>';
1444 + }
1445 +
1446 + // Return the content preview and its call to action.
1447 + return $content_preview . $call_to_action;
1448 +
660 1449 }
661 1450
662 1451 /**
663 1452 * Returns a preview of the given content for visitors that don't have access to restricted content.
@@ -665,12 +1454,12 @@
665 1454 * The preview is determined by:
666 1455 * - A single <!--more--> tag being placed between WordPress paragraphs when using the Classic Editor.
667 1456 * Content before the tag will be returned as the preview, unless 'noteaser' is enabled.
668 1457 * - A single 'Read More' block being placed between WordPress blocks when using the Gutenberg Editor.
669 - * Content before the Read More block will be returned as the preview, unless 'Hide th excerpt
1458 + * Content before the Read More block will be returned as the preview, unless 'Hide the excerpt
670 1459 * on the full content page' is enabled.
671 1460 *
672 - * No preview content is returned if the above conditions are not met.
1461 + * If no more tag or Read More block is present, returns the Post's excerpt.
673 1462 *
674 1463 * @since 2.1.0
675 1464 *
676 1465 * @param string $content Post Content.
@@ -701,44 +1490,74 @@
701 1490 // If noteaser is enabled, this will correctly be blank.
702 1491 return $content_breakdown[0];
703 1492 }
704 1493
705 - // If here, there is no preview content available. Don't return any content.
706 - return '';
1494 + // If here, there is no preview content available. Use the Post's excerpt.
1495 + return $this->get_excerpt( $post->ID );
707 1496
708 1497 }
709 1498
710 1499 /**
1500 + * Returns the excerpt for the given Post.
1501 + *
1502 + * If no excerpt is defined, generates one from the Post's content.
1503 + *
1504 + * @since 2.3.7
1505 + *
1506 + * @param int $post_id Post ID.
1507 + * @return string Post excerpt.
1508 + */
1509 + private function get_excerpt( $post_id ) {
1510 +
1511 + // Remove 'the_content' filter, as if the Post contains no defined excerpt, WordPress
1512 + // will invoke the Post's content to build an excerpt, resulting in an infinite loop.
1513 + remove_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
1514 +
1515 + // Generate the Post's excerpt.
1516 + $excerpt = get_the_excerpt( $post_id );
1517 +
1518 + // Restore filters so other functions and Plugins aren't affected.
1519 + add_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
1520 +
1521 + // Return the excerpt.
1522 + return wpautop( $excerpt );
1523 +
1524 + }
1525 +
1526 + /**
711 1527 * Returns the HTML output for the call to action for visitors not subscribed to the required
712 1528 * resource type and ID.
713 1529 *
714 1530 * @since 2.1.0
715 1531 *
716 - * @param int $post_id Post ID.
717 - * @param string $resource_type Resource Type (product).
718 - * @param int $resource_id Resource ID (Product ID).
1532 + * @param int $post_id Post ID.
719 1533 * @return string HTML
720 1534 */
721 - private function get_call_to_action( $post_id, $resource_type, $resource_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1535 + private function get_call_to_action( $post_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
722 1536
1537 + // Enqueue CSS and JS.
1538 + $this->enqueue_scripts_and_styles();
1539 +
1540 + // Output code form if this request is after the user entered their email address,
1541 + // which means we're going through the authentication flow.
1542 + if ( $this->in_authentication_flow() ) {
1543 + ob_start();
1544 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/code.php';
1545 + return trim( ob_get_clean() );
1546 + }
1547 +
1548 + // Get resource type and id.
1549 + $resource_type = $this->resource_type;
1550 + $resource_id = $this->resource_id;
1551 +
723 1552 // This is deliberately a switch statement, because we will likely add in support
724 1553 // for restrict by tag and form later.
725 1554 switch ( $resource_type ) {
726 1555 case 'product':
727 - // Only load styles if the Disable CSS option is off.
728 - if ( ! $this->settings->css_disabled() ) {
729 - // Enqueue styles.
730 - wp_enqueue_style( 'convertkit-restrict-content', CONVERTKIT_PLUGIN_URL . 'resources/frontend/css/restrict-content.css', array(), CONVERTKIT_PLUGIN_VERSION );
731 - }
1556 + // Get header and text from settings for Products.
1557 + $heading = $this->restrict_content_settings->get_by_key( 'subscribe_heading' );
1558 + $text = $this->restrict_content_settings->get_by_key( 'subscribe_text' );
732 1559
733 - // Output product code form if this request is after the user entered their email address,
734 - // which means we're going through the authentication flow.
735 - if ( $this->in_authentication_flow() ) { // phpcs:ignore WordPress.Security.NonceVerification
736 - ob_start();
737 - include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/product-code.php';
738 - return trim( ob_get_clean() );
739 - }
740 -
741 1560 // Output product restricted message and email form.
742 1561 // Get Product.
743 1562 $products = new ConvertKit_Resource_Products( 'restrict_content' );
744 1563 $product = $products->get_by_id( $resource_id );
@@ -748,18 +1567,632 @@
748 1567 if ( $url ) {
749 1568 wp_enqueue_script( 'convertkit-commerce', $url, array(), CONVERTKIT_PLUGIN_VERSION, true );
750 1569 }
751 1570
1571 + // If scripts are enabled, output the email login form in a modal, which will be displayed
1572 + // when the 'log in' link is clicked.
1573 + if ( ! $this->settings->scripts_disabled() ) {
1574 + $this->output_login_modal( $post_id, $resource_id, $resource_type );
1575 + }
1576 +
752 1577 // Output.
753 1578 ob_start();
754 - $button = $products->get_html( $resource_id, $this->restrict_content_settings->get_by_key( 'subscribe_button_label' ) );
1579 + $button = $products->get_html(
1580 + $resource_id,
1581 + $this->restrict_content_settings->get_by_key( 'subscribe_button_label' ),
1582 + array(
1583 + 'css_classes' => array( 'wp-block-button__link', 'wp-element-button' ),
1584 + )
1585 + );
755 1586 include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/product.php';
756 1587 return trim( ob_get_clean() );
757 1588
1589 + case 'form':
1590 + // Display the Form.
1591 + $forms = new ConvertKit_Resource_Forms( 'restrict_content' );
1592 + $form = $forms->get_html( $resource_id, $post_id );
1593 +
1594 + // If scripts are enabled, output the email login form in a modal, which will be displayed
1595 + // when the 'log in' link is clicked.
1596 + if ( ! $this->settings->scripts_disabled() ) {
1597 + $this->output_login_modal( $post_id, $resource_id, $resource_type );
1598 + }
1599 +
1600 + // Output.
1601 + ob_start();
1602 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/form.php';
1603 + return trim( ob_get_clean() );
1604 +
1605 + case 'tag':
1606 + // Get header and text from settings for Tags.
1607 + $heading = $this->restrict_content_settings->get_by_key( 'subscribe_heading_tag' );
1608 + $text = $this->restrict_content_settings->get_by_key( 'subscribe_text_tag' );
1609 +
1610 + // If scripts are enabled, output the email login form in a modal, which will be displayed
1611 + // when the 'log in' link is clicked.
1612 + if ( ! $this->settings->scripts_disabled() ) {
1613 + $this->output_login_modal( $post_id, $resource_id, $resource_type );
1614 + }
1615 +
1616 + // Output.
1617 + ob_start();
1618 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/tag.php';
1619 + return trim( ob_get_clean() );
1620 +
758 1621 default:
759 1622 return '';
760 1623
761 1624 }
1625 +
1626 + }
1627 +
1628 + /**
1629 + * Whether this request is from a search engine crawler.
1630 + *
1631 + * @since 2.4.2
1632 + *
1633 + * @return bool
1634 + */
1635 + private function is_crawler() {
1636 +
1637 + // Define permitted user agent crawlers and their IP addresses.
1638 + $permitted_user_agent_ip_ranges = array(
1639 + // Google.
1640 + // https://developers.google.com/static/search/apis/ipranges/googlebot.json.
1641 + 'Googlebot' => array(
1642 + '192.178.5.0/27',
1643 + '34.100.182.96/28',
1644 + '34.101.50.144/28',
1645 + '34.118.254.0/28',
1646 + '34.118.66.0/28',
1647 + '34.126.178.96/28',
1648 + '34.146.150.144/28',
1649 + '34.147.110.144/28',
1650 + '34.151.74.144/28',
1651 + '34.152.50.64/28',
1652 + '34.154.114.144/28',
1653 + '34.155.98.32/28',
1654 + '34.165.18.176/28',
1655 + '34.175.160.64/28',
1656 + '34.176.130.16/28',
1657 + '34.22.85.0/27',
1658 + '34.64.82.64/28',
1659 + '34.65.242.112/28',
1660 + '34.80.50.80/28',
1661 + '34.88.194.0/28',
1662 + '34.89.10.80/28',
1663 + '34.89.198.80/28',
1664 + '34.96.162.48/28',
1665 + '35.247.243.240/28',
1666 + '66.249.64.0/27',
1667 + '66.249.64.128/27',
1668 + '66.249.64.160/27',
1669 + '66.249.64.192/27',
1670 + '66.249.64.224/27',
1671 + '66.249.64.32/27',
1672 + '66.249.64.64/27',
1673 + '66.249.64.96/27',
1674 + '66.249.65.0/27',
1675 + '66.249.65.160/27',
1676 + '66.249.65.192/27',
1677 + '66.249.65.224/27',
1678 + '66.249.65.32/27',
1679 + '66.249.65.64/27',
1680 + '66.249.65.96/27',
1681 + '66.249.66.0/27',
1682 + '66.249.66.128/27',
1683 + '66.249.66.160/27',
1684 + '66.249.66.192/27',
1685 + '66.249.66.32/27',
1686 + '66.249.66.64/27',
1687 + '66.249.66.96/27',
1688 + '66.249.68.0/27',
1689 + '66.249.68.32/27',
1690 + '66.249.68.64/27',
1691 + '66.249.69.0/27',
1692 + '66.249.69.128/27',
1693 + '66.249.69.160/27',
1694 + '66.249.69.192/27',
1695 + '66.249.69.224/27',
1696 + '66.249.69.32/27',
1697 + '66.249.69.64/27',
1698 + '66.249.69.96/27',
1699 + '66.249.70.0/27',
1700 + '66.249.70.128/27',
1701 + '66.249.70.160/27',
1702 + '66.249.70.192/27',
1703 + '66.249.70.224/27',
1704 + '66.249.70.32/27',
1705 + '66.249.70.64/27',
1706 + '66.249.70.96/27',
1707 + '66.249.71.0/27',
1708 + '66.249.71.128/27',
1709 + '66.249.71.160/27',
1710 + '66.249.71.192/27',
1711 + '66.249.71.224/27',
1712 + '66.249.71.32/27',
1713 + '66.249.71.64/27',
1714 + '66.249.71.96/27',
1715 + '66.249.72.0/27',
1716 + '66.249.72.128/27',
1717 + '66.249.72.160/27',
1718 + '66.249.72.192/27',
1719 + '66.249.72.224/27',
1720 + '66.249.72.32/27',
1721 + '66.249.72.64/27',
1722 + '66.249.72.96/27',
1723 + '66.249.73.0/27',
1724 + '66.249.73.128/27',
1725 + '66.249.73.160/27',
1726 + '66.249.73.192/27',
1727 + '66.249.73.224/27',
1728 + '66.249.73.32/27',
1729 + '66.249.73.64/27',
1730 + '66.249.73.96/27',
1731 + '66.249.74.0/27',
1732 + '66.249.74.128/27',
1733 + '66.249.74.32/27',
1734 + '66.249.74.64/27',
1735 + '66.249.74.96/27',
1736 + '66.249.75.0/27',
1737 + '66.249.75.128/27',
1738 + '66.249.75.160/27',
1739 + '66.249.75.192/27',
1740 + '66.249.75.224/27',
1741 + '66.249.75.32/27',
1742 + '66.249.75.64/27',
1743 + '66.249.75.96/27',
1744 + '66.249.76.0/27',
1745 + '66.249.76.128/27',
1746 + '66.249.76.160/27',
1747 + '66.249.76.192/27',
1748 + '66.249.76.224/27',
1749 + '66.249.76.32/27',
1750 + '66.249.76.64/27',
1751 + '66.249.76.96/27',
1752 + '66.249.77.0/27',
1753 + '66.249.77.128/27',
1754 + '66.249.77.160/27',
1755 + '66.249.77.192/27',
1756 + '66.249.77.224/27',
1757 + '66.249.77.32/27',
1758 + '66.249.77.64/27',
1759 + '66.249.77.96/27',
1760 + '66.249.78.0/27',
1761 + '66.249.78.32/27',
1762 + '66.249.79.0/27',
1763 + '66.249.79.128/27',
1764 + '66.249.79.160/27',
1765 + '66.249.79.192/27',
1766 + '66.249.79.224/27',
1767 + '66.249.79.32/27',
1768 + '66.249.79.64/27',
1769 + '66.249.79.96/27',
1770 + ),
1771 +
1772 + // Applebot.
1773 + // http://search.developer.apple.com/applebot.json.
1774 + 'Applebot' => array(
1775 + '17.241.208.160/27',
1776 + '17.241.193.160/27',
1777 + '17.241.200.160/27',
1778 + '17.22.237.0/24',
1779 + '17.22.245.0/24',
1780 + '17.22.253.0/24',
1781 + '17.241.75.0/24',
1782 + '17.241.219.0/24',
1783 + '17.241.227.0/24',
1784 + '17.246.15.0/24',
1785 + '17.246.19.0/24',
1786 + '17.246.23.0/24',
1787 + ),
1788 +
1789 + // Bing.
1790 + // https://www.bing.com/toolbox/bingbot.json.
1791 + 'Bingbot' => array(
1792 + '157.55.39.0/24',
1793 + '207.46.13.0/24',
1794 + '40.77.167.0/24',
1795 + '13.66.139.0/24',
1796 + '13.66.144.0/24',
1797 + '52.167.144.0/24',
1798 + '13.67.10.16/28',
1799 + '13.69.66.240/28',
1800 + '13.71.172.224/28',
1801 + '139.217.52.0/28',
1802 + '191.233.204.224/28',
1803 + '20.36.108.32/28',
1804 + '20.43.120.16/28',
1805 + '40.79.131.208/28',
1806 + '40.79.186.176/28',
1807 + '52.231.148.0/28',
1808 + '20.79.107.240/28',
1809 + '51.105.67.0/28',
1810 + '20.125.163.80/28',
1811 + '40.77.188.0/22',
1812 + '65.55.210.0/24',
1813 + '199.30.24.0/23',
1814 + '40.77.202.0/24',
1815 + '40.77.139.0/25',
1816 + '20.74.197.0/28',
1817 + '20.15.133.160/27',
1818 + '40.77.177.0/24',
1819 + '40.77.178.0/23',
1820 + ),
1821 +
1822 + // DuckDuckGo.
1823 + // https://duckduckgo.com/duckduckgo-help-pages/results/duckduckbot.
1824 + 'DuckDuckBot' => array(
1825 + '57.152.72.128/32',
1826 + '51.8.253.152/32',
1827 + '40.80.242.63/32',
1828 + '20.12.141.99/32',
1829 + '20.49.136.28/32',
1830 + '51.116.131.221/32',
1831 + '51.107.40.209/32',
1832 + '20.40.133.240/32',
1833 + '20.50.168.91/32',
1834 + '51.120.48.122/32',
1835 + '20.193.45.113/32',
1836 + '40.76.173.151/32',
1837 + '40.76.163.7/32',
1838 + '20.185.79.47/32',
1839 + '52.142.26.175/32',
1840 + '20.185.79.15/32',
1841 + '52.142.24.149/32',
1842 + '40.76.162.208/32',
1843 + '40.76.163.23/32',
1844 + '40.76.162.191/32',
1845 + '40.76.162.247/32',
1846 + '40.88.21.235/32',
1847 + '20.191.45.212/32',
1848 + '52.146.59.12/32',
1849 + '52.146.59.156/32',
1850 + '52.146.59.154/32',
1851 + '52.146.58.236/32',
1852 + '20.62.224.44/32',
1853 + '51.104.180.53/32',
1854 + '51.104.180.47/32',
1855 + '51.104.180.26/32',
1856 + '51.104.146.225/32',
1857 + '51.104.146.235/32',
1858 + '20.73.202.147/32',
1859 + '20.73.132.240/32',
1860 + '20.71.12.143/32',
1861 + '20.56.197.58/32',
1862 + '20.56.197.63/32',
1863 + '20.43.150.93/32',
1864 + '20.43.150.85/32',
1865 + '20.44.222.1/32',
1866 + '40.89.243.175/32',
1867 + '13.89.106.77/32',
1868 + '52.143.242.6/32',
1869 + '52.143.241.111/32',
1870 + '52.154.60.82/32',
1871 + '20.197.209.11/32',
1872 + '20.197.209.27/32',
1873 + '20.226.133.105/32',
1874 + '191.234.216.4/32',
1875 + '191.234.216.178/32',
1876 + '20.53.92.211/32',
1877 + '20.53.91.2/32',
1878 + '20.207.99.197/32',
1879 + '20.207.97.190/32',
1880 + '40.81.250.205/32',
1881 + '40.64.106.11/32',
1882 + '40.64.105.247/32',
1883 + '20.72.242.93/32',
1884 + '20.99.255.235/32',
1885 + '20.113.3.121/32',
1886 + '52.224.16.221/32',
1887 + '52.224.21.53/32',
1888 + '52.224.20.204/32',
1889 + '52.224.21.19/32',
1890 + '52.224.20.249/32',
1891 + '52.224.20.203/32',
1892 + '52.224.20.190/32',
1893 + '52.224.16.229/32',
1894 + '52.224.21.20/32',
1895 + '52.146.63.80/32',
1896 + '52.224.20.227/32',
1897 + '52.224.20.193/32',
1898 + '52.190.37.160/32',
1899 + '52.224.21.23/32',
1900 + '52.224.20.223/32',
1901 + '52.224.20.181/32',
1902 + '52.224.21.49/32',
1903 + '52.224.21.55/32',
1904 + '52.224.21.61/32',
1905 + '52.224.19.152/32',
1906 + '52.224.20.186/32',
1907 + '52.224.21.27/32',
1908 + '52.224.21.51/32',
1909 + '52.224.20.174/32',
1910 + '52.224.21.4/32',
1911 + '51.104.164.109/32',
1912 + '51.104.167.71/32',
1913 + '51.104.160.177/32',
1914 + '51.104.162.149/32',
1915 + '51.104.167.95/32',
1916 + '51.104.167.54/32',
1917 + '51.104.166.111/32',
1918 + '51.104.167.88/32',
1919 + '51.104.161.32/32',
1920 + '51.104.163.250/32',
1921 + '51.104.164.189/32',
1922 + '51.104.167.19/32',
1923 + '51.104.160.167/32',
1924 + '51.104.167.110/32',
1925 + '20.191.44.119/32',
1926 + '51.104.167.104/32',
1927 + '20.191.44.234/32',
1928 + '51.104.164.215/32',
1929 + '51.104.167.52/32',
1930 + '20.191.44.22/32',
1931 + '51.104.167.87/32',
1932 + '51.104.167.96/32',
1933 + '20.191.44.16/32',
1934 + '51.104.167.61/32',
1935 + '51.104.164.147/32',
1936 + '20.50.48.159/32',
1937 + '40.114.182.172/32',
1938 + '20.50.50.130/32',
1939 + '20.50.50.163/32',
1940 + '20.50.50.46/32',
1941 + '40.114.182.153/32',
1942 + '20.50.50.118/32',
1943 + '20.50.49.55/32',
1944 + '20.50.49.25/32',
1945 + '40.114.183.251/32',
1946 + '20.50.50.123/32',
1947 + '20.50.49.237/32',
1948 + '20.50.48.192/32',
1949 + '20.50.50.134/32',
1950 + '51.138.90.233/32',
1951 + '40.114.183.196/32',
1952 + '20.50.50.146/32',
1953 + '40.114.183.88/32',
1954 + '20.50.50.145/32',
1955 + '20.50.50.121/32',
1956 + '20.50.49.40/32',
1957 + '51.138.90.206/32',
1958 + '40.114.182.45/32',
1959 + '51.138.90.161/32',
1960 + '20.50.49.0/32',
1961 + '40.119.232.215/32',
1962 + '104.43.55.167/32',
1963 + '40.119.232.251/32',
1964 + '40.119.232.50/32',
1965 + '40.119.232.146/32',
1966 + '40.119.232.218/32',
1967 + '104.43.54.127/32',
1968 + '104.43.55.117/32',
1969 + '104.43.55.116/32',
1970 + '104.43.55.166/32',
1971 + '52.154.169.50/32',
1972 + '52.154.171.70/32',
1973 + '52.154.170.229/32',
1974 + '52.154.170.113/32',
1975 + '52.154.171.44/32',
1976 + '52.154.172.2/32',
1977 + '52.143.244.81/32',
1978 + '52.154.171.87/32',
1979 + '52.154.171.250/32',
1980 + '52.154.170.28/32',
1981 + '52.154.170.122/32',
1982 + '52.143.243.117/32',
1983 + '52.143.247.235/32',
1984 + '52.154.171.235/32',
1985 + '52.154.171.196/32',
1986 + '52.154.171.0/32',
1987 + '52.154.170.243/32',
1988 + '52.154.170.26/32',
1989 + '52.154.169.200/32',
1990 + '52.154.170.96/32',
1991 + '52.154.170.88/32',
1992 + '52.154.171.150/32',
1993 + '52.154.171.205/32',
1994 + '52.154.170.117/32',
1995 + '52.154.170.209/32',
1996 + '191.235.202.48/32',
1997 + '191.233.3.202/32',
1998 + '191.235.201.214/32',
1999 + '191.233.3.197/32',
2000 + '191.235.202.38/32',
2001 + '20.53.78.144/32',
2002 + '20.193.24.10/32',
2003 + '20.53.78.236/32',
2004 + '20.53.78.138/32',
2005 + '20.53.78.123/32',
2006 + '20.53.78.106/32',
2007 + '20.193.27.215/32',
2008 + '20.193.25.197/32',
2009 + '20.193.12.126/32',
2010 + '20.193.24.251/32',
2011 + '20.204.242.101/32',
2012 + '20.207.72.113/32',
2013 + '20.204.242.19/32',
2014 + '20.219.45.67/32',
2015 + '20.207.72.11/32',
2016 + '20.219.45.190/32',
2017 + '20.204.243.55/32',
2018 + '20.204.241.148/32',
2019 + '20.207.72.110/32',
2020 + '20.204.240.172/32',
2021 + '20.207.72.21/32',
2022 + '20.204.246.81/32',
2023 + '20.207.107.181/32',
2024 + '20.204.246.254/32',
2025 + '20.219.43.246/32',
2026 + '52.149.25.43/32',
2027 + '52.149.61.51/32',
2028 + '52.149.58.139/32',
2029 + '52.149.60.38/32',
2030 + '52.148.165.38/32',
2031 + '52.143.95.162/32',
2032 + '52.149.56.151/32',
2033 + '52.149.30.45/32',
2034 + '52.149.58.173/32',
2035 + '52.143.95.204/32',
2036 + '52.149.28.83/32',
2037 + '52.149.58.69/32',
2038 + '52.148.161.87/32',
2039 + '52.149.58.27/32',
2040 + '52.149.28.18/32',
2041 + '20.79.226.26/32',
2042 + '20.79.239.66/32',
2043 + '20.79.238.198/32',
2044 + '20.113.14.159/32',
2045 + '20.75.144.152/32',
2046 + '20.43.172.120/32',
2047 + '20.53.134.160/32',
2048 + '20.201.15.208/32',
2049 + '20.93.28.24/32',
2050 + '20.61.34.40/32',
2051 + '52.242.224.168/32',
2052 + '20.80.129.80/32',
2053 + '20.195.108.47/32',
2054 + '4.195.133.120/32',
2055 + '4.228.76.163/32',
2056 + '4.182.131.108/32',
2057 + '4.209.224.56/32',
2058 + '108.141.83.74/32',
2059 + '4.213.46.14/32',
2060 + '172.169.17.165/32',
2061 + '51.8.71.117/32',
2062 + '20.3.1.178/32',
2063 + ),
2064 +
2065 + // OpenAI Search Bot.
2066 + // https://platform.openai.com/docs/bots/overview-of-openai-crawlers.
2067 + // https://openai.com/searchbot.json.
2068 + 'OAI-SearchBot' => array(
2069 + '20.42.10.176/28',
2070 + '172.203.190.128/28',
2071 + '104.210.140.128/28',
2072 + '51.8.102.0/24',
2073 + '135.234.64.0/24',
2074 + ),
2075 +
2076 + // Perplexity Search Bot.
2077 + // https://www.perplexity.com/perplexitybot.json.
2078 + 'PerplexityBot' => array(
2079 + '107.20.236.150/32',
2080 + '3.224.62.45/32',
2081 + '18.210.92.235/32',
2082 + '3.222.232.239/32',
2083 + '3.211.124.183/32',
2084 + '3.231.139.107/32',
2085 + '18.97.1.228/30',
2086 + '18.97.9.96/29',
2087 + ),
2088 +
2089 + // YandexBot.
2090 + // https://yandex.com/support/webmaster/en/robot-workings/check-yandex-robots.html.
2091 + 'YandexBot' => array(
2092 + '5.45.192.0/18',
2093 + '5.255.192.0/18',
2094 + '37.9.64.0/18',
2095 + '37.140.128.0/18',
2096 + '77.88.0.0/18',
2097 + '84.252.160.0/19',
2098 + '87.250.224.0/19',
2099 + '90.156.176.0/22',
2100 + '93.158.128.0/18',
2101 + '95.108.128.0/17',
2102 + '141.8.128.0/18',
2103 + '178.154.128.0/18',
2104 + '213.180.192.0/19',
2105 + '185.32.187.0/24',
2106 + ),
2107 +
2108 + );
2109 +
2110 + /**
2111 + * Define the permitted user agents and their IP address ranges that can bypass
2112 + * Restrict Content to index content for search engines.
2113 + *
2114 + * @since 2.4.2
2115 + *
2116 + * @param array $permitted Permitted user agent and IP address ranges.
2117 + */
2118 + $permitted_user_agent_ip_ranges = apply_filters( 'convertkit_output_restrict_content_is_crawler_permitted_user_agent_ip_ranges', $permitted_user_agent_ip_ranges );
2119 +
2120 + // Not a crawler if no user agent defined or client IP address defined.
2121 + if ( ! array_key_exists( 'HTTP_USER_AGENT', $_SERVER ) || ! array_key_exists( 'REMOTE_ADDR', $_SERVER ) ) {
2122 + return false;
2123 + }
2124 +
2125 + // Iterate through permitted crawler IP addresses.
2126 + foreach ( $permitted_user_agent_ip_ranges as $permitted_user_agent => $permitted_ip_addresses ) {
2127 + // Skip this user agent's IP addresses if the client user agent doesn't contain this user agent.
2128 + if ( stripos( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), $permitted_user_agent ) === false ) {
2129 + continue;
2130 + }
2131 +
2132 + // Check IP address.
2133 + foreach ( $permitted_ip_addresses as $permitted_ip_range ) {
2134 + if ( ! $this->ip_in_range( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ), $permitted_ip_range ) ) {
2135 + continue;
2136 + }
2137 +
2138 + // The client user agent and IP address match a known crawler and its IP address.
2139 + // This is a crawler.
2140 + return true;
2141 + }
2142 + }
2143 +
2144 + // If here, the client IP address isn't from a crawler.
2145 + return false;
2146 +
2147 + }
2148 +
2149 + /**
2150 + * Determines if the given IP address falls within the given CIDR range.
2151 + *
2152 + * @since 2.4.2
2153 + *
2154 + * @param string $ip Client IP Address (e.g. 127.0.0.1).
2155 + * @param string $range IP Address and bits (e.g. 127.0.0.1/27).
2156 + * @return bool Client IP Address matches range.
2157 + */
2158 + public function ip_in_range( $ip, $range ) {
2159 +
2160 + // Return false if the IP address isn't valid.
2161 + if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
2162 + return false;
2163 + }
2164 +
2165 + // Return false if the range doesn't include the CIDR.
2166 + if ( strpos( $range, '/' ) === false ) {
2167 + return false;
2168 + }
2169 +
2170 + // Get subnet and bits from range.
2171 + list( $subnet, $bits ) = explode( '/', $range );
2172 +
2173 + // Return false if the CIDR isn't numerical.
2174 + if ( ! is_numeric( $bits ) ) {
2175 + return false;
2176 + }
2177 +
2178 + // Cast CIDR to integer.
2179 + $bits = (int) $bits;
2180 +
2181 + // Return false if the CIDR is not wihtin the permitted range.
2182 + if ( $bits < 0 || $bits > 32 ) {
2183 + return false;
2184 + }
2185 +
2186 + // Convert to long representation.
2187 + $ip = ip2long( $ip );
2188 + $subnet = ip2long( $subnet );
2189 + $mask = -1 << ( 32 - $bits );
2190 +
2191 + // If the supplied subnet wasn't correctly aligned.
2192 + $subnet &= $mask;
2193 +
2194 + return ( $ip & $mask ) === $subnet;
762 2195
763 2196 }
764 2197
765 2198 }