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
convertkit / includes / class-convertkit-output-restrict-content.php

class-convertkit-output-restrict-content.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 3.4.6, at includes/class-convertkit-output-restrict-content.php

2,199 lines 64.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ConvertKit Output Restrict Content class.
4 *
5 * @package ConvertKit
6 * @author ConvertKit
7 */
8
9 /**
10 * Restricts (or displays) a single Page, Post or Custom Post Type's content
11 * based on the Post's "Restrict Content" configuration.
12 *
13 * @since 2.1.0
14 */
15 class ConvertKit_Output_Restrict_Content {
16
17 /**
18 * Holds the WP_Error object if an API call / authentication failed,
19 * to display on screen as a notification.
20 *
21 * @since 2.1.0
22 *
23 * @var bool|WP_Error
24 */
25 public $error = false;
26
27 /**
28 * Holds the ConvertKit Plugin Settings class
29 *
30 * @since 2.1.0
31 *
32 * @var bool|ConvertKit_Settings
33 */
34 public $settings = false;
35
36 /**
37 * Holds the ConvertKit Restrict Content Settings class
38 *
39 * @since 2.1.0
40 *
41 * @var bool|ConvertKit_Settings_Restrict_Content
42 */
43 public $restrict_content_settings = false;
44
45 /**
46 * Holds the ConvertKit Post Settings class
47 *
48 * @since 2.1.0
49 *
50 * @var bool|ConvertKit_Post
51 */
52 public $post_settings = false;
53
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 /**
75 * Holds the Post ID
76 *
77 * @since 2.1.0
78 *
79 * @var bool|int
80 */
81 public $post_id = false;
82
83 /**
84 * Holds the ConvertKit API class
85 *
86 * @since 2.1.0
87 *
88 * @var bool|ConvertKit_API_V4
89 */
90 public $api = false;
91
92 /**
93 * Holds the token returned from calling the subscriber_authentication_send_code API endpoint.
94 *
95 * @since 2.1.0
96 *
97 * @var bool|string
98 */
99 public $token = false;
100
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 /**
111 * Constructor. Registers actions and filters to possibly limit output of a Page/Post/CPT's
112 * content on the frontend site.
113 *
114 * @since 2.1.0
115 */
116 public function __construct() {
117
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 );
124 add_filter( 'get_previous_post_where', array( $this, 'maybe_change_previous_post_where_clause' ), 10, 5 );
125 add_filter( 'get_next_post_where', array( $this, 'maybe_change_next_post_where_clause' ), 10, 5 );
126 add_filter( 'get_previous_post_sort', array( $this, 'maybe_change_previous_next_post_order_by_clause' ), 10, 3 );
127 add_filter( 'get_next_post_sort', array( $this, 'maybe_change_previous_next_post_order_by_clause' ), 10, 3 );
128
129 }
130
131 /**
132 * Register REST API routes.
133 *
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.
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 *
408 * @since 2.1.0
409 */
410 public function maybe_run_subscriber_authentication() {
411
412 // Bail if no nonce was specified via form submission.
413 if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) {
414 return;
415 }
416
417 // Bail if the request is a form submission and the nonce failed validation.
418 if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) {
419 return;
420 }
421
422 // Bail if the expected email or Post ID are missing from the request.
423 if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) {
424 return;
425 }
426 if ( ! array_key_exists( 'convertkit_post_id', $_REQUEST ) ) {
427 return;
428 }
429
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 }
434
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'] );
440
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.
476 if ( is_wp_error( $result ) ) {
477 $this->error = $result;
478 return;
479 }
480
481 // Store the token so it's included in the subscriber code form.
482 $this->token = $result;
483
484 }
485
486 /**
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
491 *
492 * This calls the API to verify the token and subscriber code, which tells us that the email
493 * address supplied truly belongs to the user, and that we can safely trust their subscriber ID
494 * to be valid.
495 *
496 * @since 2.1.0
497 */
498 public function maybe_run_subscriber_verification() {
499
500 // Bail if the expected token and subscriber code is missing.
501 if ( ! array_key_exists( 'token', $_REQUEST ) ) {
502 return;
503 }
504 if ( ! array_key_exists( 'subscriber_code', $_REQUEST ) ) {
505 return;
506 }
507
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() ) {
518 return;
519 }
520
521 // Store the token so it's included in the subscriber code form if verification fails.
522 $this->token = sanitize_text_field( wp_unslash( $_REQUEST['token'] ) );
523
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 }
532
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'] ) ) );
535
536 // Bail if an error occurred.
537 if ( is_wp_error( $subscriber_id ) ) {
538 $this->error = $subscriber_id;
539 return;
540 }
541
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.
571 $subscriber = new ConvertKit_Subscriber();
572 $subscriber->forget();
573
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',
633 array(
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 )
639 );
640
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 }
647
648 }
649
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 /**
765 * Displays (or hides) content on a singular Page, Post or Custom Post Type's Content,
766 * depending on whether the visitor is an authenticated ConvertKit subscriber and has
767 * subscribed to the ConvertKit Product or Tag.
768 *
769 * @since 2.1.0
770 *
771 * @param string $content Post Content.
772 * @return string Post Content with content restricted/not restricted
773 */
774 public function maybe_restrict_content( $content ) {
775
776 // Bail if the Restrict Content setting is not enabled on this Page.
777 if ( ! $this->is_restricted_content() ) {
778 return $content;
779 }
780
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 }
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
792 // Return the Post Content, unedited, if the Resource Type is false.
793 if ( ! $this->resource_type ) {
794 return $content;
795 }
796
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();
799
800 // Return the full Post Content, unedited, if the Resource ID is false, as this means
801 // no restrict content setting has been defined for this Post.
802 if ( ! $this->resource_id ) {
803 return $content;
804 }
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
811 // Return if this request is after the user entered their email address,
812 // which means we're going through the authentication flow.
813 if ( $this->in_authentication_flow() ) {
814 return $this->restrict_content( $content );
815 }
816
817 // Get the subscriber ID, either from the request or an existing cookie.
818 $subscriber_id = $this->get_subscriber_id_from_request();
819
820 // If no subscriber ID exists, the visitor cannot view the content.
821 if ( ! $subscriber_id ) {
822 return $this->restrict_content( $content );
823 }
824
825 // If the subscriber is not subscribed to the product, restrict the content.
826 if ( ! $this->subscriber_has_access( $subscriber_id ) ) {
827 // Show an error before the call to action, to tell the subscriber why they still cannot
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.
845 $this->error = new WP_Error(
846 'convertkit_restrict_content_subscriber_no_access',
847 esc_html( $message )
848 );
849
850 return $this->restrict_content( $content );
851 }
852
853 // If here, the subscriber has subscribed to the product.
854 // Show the full Post Content.
855 return $content;
856
857 }
858
859 /**
860 * Changes how WordPress' get_adjacent_post() function queries Pages, to determine what
861 * the previous Page link is when using the Previous navigation block on a Page that
862 * has the Restrict Content setting defined.
863 *
864 * By default, get_adjacent_post() will query by post_date, which we change to menu_order.
865 *
866 * @since 2.1.0
867 *
868 * @param string $where The `WHERE` clause in the SQL.
869 * @param bool $in_same_term Whether post should be in a same taxonomy term.
870 * @param array $excluded_terms Array of excluded term IDs.
871 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
872 * @param WP_Post $post WP_Post object.
873 * @return string Modified `WHERE` clause
874 */
875 public function maybe_change_previous_post_where_clause( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
876
877 // Bail if the Restrict Content setting is not enabled on this Page.
878 if ( ! $this->is_restricted_content() ) {
879 return $where;
880 }
881
882 // Bail if the Page doesn't match the current Page being viewed, or has no parent Page.
883 if ( ! $this->has_parent_page( $post ) ) {
884 return $where;
885 }
886
887 // Build replacement where statement.
888 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order < ' . $post->menu_order;
889
890 // Replace existing where statement with new statement.
891 $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
892
893 // Return.
894 return $where;
895
896 }
897
898 /**
899 * Changes how WordPress' get_adjacent_post() function queries Pages, to determine what
900 * the next Page link is when using the Previous navigation block on a Page that
901 * has the Restrict Content setting defined.
902 *
903 * By default, get_adjacent_post() will query by post_date, which we change to menu_order.
904 *
905 * @since 2.1.0
906 *
907 * @param string $where The `WHERE` clause in the SQL.
908 * @param bool $in_same_term Whether post should be in a same taxonomy term.
909 * @param array $excluded_terms Array of excluded term IDs.
910 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
911 * @param WP_Post $post WP_Post object.
912 * @return string Modified `WHERE` clause
913 */
914 public function maybe_change_next_post_where_clause( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
915
916 // Bail if the Restrict Content setting is not enabled on this Page.
917 if ( ! $this->is_restricted_content() ) {
918 return $where;
919 }
920
921 // Bail if the Page doesn't match the current Page being viewed, or has no parent Page.
922 if ( ! $this->has_parent_page( $post ) ) {
923 return $where;
924 }
925
926 // Build replacement where statement.
927 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order > ' . $post->menu_order;
928
929 // Replace existing where statement with new statement.
930 $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
931
932 // Return.
933 return $where;
934
935 }
936
937 /**
938 * Changes how WordPress' get_adjacent_post() function orders Pages, to determine what
939 * the next and previous Page links are when using Previous / Next navigation blocks
940 * on a Page that has the Restrict Content setting defined.
941 *
942 * By default, get_adjacent_post() will sort by Post Date, which we change to Page Order
943 * (called menu_order in WordPress).
944 *
945 * @since 2.1.0
946 *
947 * @param string $order_by SQL ORDER BY statement.
948 * @param WP_Post $post WordPress Post.
949 * @param string $order Order.
950 * @return string Modified SQL ORDER BY statement.
951 */
952 public function maybe_change_previous_next_post_order_by_clause( $order_by, $post, $order ) {
953
954 // Bail if the Restrict Content setting is not enabled on this Page.
955 if ( ! $this->is_restricted_content() ) {
956 return $order_by;
957 }
958
959 // Bail if the Page doesn't match the current Page being viewed, or has no parent Page.
960 if ( ! $this->has_parent_page( $post ) ) {
961 return $order_by;
962 }
963
964 // Order by Page order (menu_order), highest to lowest, instead of post_date.
965 return 'ORDER BY p.menu_order ' . $order . ' LIMIT 1';
966
967 }
968
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 /**
1005 * Returns the URL for the current request, excluding any query parameters.
1006 *
1007 * @since 2.1.0
1008 *
1009 * @param int $post_id Post ID.
1010 * @param bool $cache_bust Include `ck-cache-bust` parameter in URL.
1011 * @return string URL.
1012 */
1013 public function get_url( $post_id, $cache_bust = false ) {
1014
1015 // Get URL of Post.
1016 $url = get_permalink( $post_id );
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
1034 }
1035
1036 /**
1037 * Determines if the request is for a WordPress Page that has the Restrict Content
1038 * setting defined.
1039 *
1040 * @since 2.1.0
1041 *
1042 * @return bool
1043 */
1044 private function is_restricted_content() {
1045
1046 // Bail if not a singular Post Type.
1047 if ( ! is_singular() ) {
1048 return false;
1049 }
1050
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;
1055 }
1056
1057 // Get Post ID.
1058 $this->post_id = get_the_ID();
1059
1060 // Initialize Settings and Post Setting classes.
1061 $this->post_settings = new ConvertKit_Post( $this->post_id );
1062
1063 // Return whether the Post's settings are set to restrict content.
1064 return $this->post_settings->restrict_content_enabled();
1065
1066 }
1067
1068 /**
1069 * Determines if the user entered a valid email address, and need to be prompted
1070 * to enter a code sent to their email address.
1071 *
1072 * @since 2.1.0
1073 *
1074 * @return bool
1075 */
1076 private function in_authentication_flow() {
1077
1078 return ( $this->token !== false );
1079
1080 }
1081
1082 /**
1083 * Checks if the given WordPress Page matches the Page ID viewed, and has a parent.
1084 *
1085 * @since 2.1.0
1086 *
1087 * @param WP_Post $post WordPress Post.
1088 * @return bool Has parent page
1089 */
1090 private function has_parent_page( $post ) {
1091
1092 // Bail if the Page doesn't match the current Page being viewed.
1093 // This prevents us accidentally interfering with other previous / next link queries, which shouldn't happen
1094 // as we check if we're viewing a restricted content page above.
1095 if ( $post->ID !== $this->post_id ) {
1096 return false;
1097 }
1098
1099 // Bail if the Page doesn't have a parent Page.
1100 // We don't want to modify the default sort behaviour in this instance.
1101 if ( $post->post_parent === 0 ) {
1102 return false;
1103 }
1104
1105 return true;
1106
1107 }
1108
1109 /**
1110 * Get the Post's Restricted Content resource type.
1111 *
1112 * @since 2.1.0
1113 *
1114 * @return bool|string Resource Type (product).
1115 */
1116 private function get_resource_type() {
1117
1118 // Initialize Post Setting classes.
1119 $this->post_settings = new ConvertKit_Post( $this->post_id );
1120
1121 // Get resource type.
1122 $resource_type = $this->post_settings->get_restrict_content_type();
1123
1124 /**
1125 * Define the ConvertKit Resource Type that the visitor must be subscribed against
1126 * to access this content, overriding the Post setting.
1127 *
1128 * Return false or an empty string to not restrict content.
1129 *
1130 * @since 2.1.0
1131 *
1132 * @param string $resource_type Resource Type (product)
1133 * @param int $post_id Post ID
1134 */
1135 $resource_type = apply_filters( 'convertkit_output_restrict_content_get_resource_type', $resource_type, $this->post_id );
1136
1137 // If resource type is blank, set it to false.
1138 if ( empty( $resource_type ) ) {
1139 $resource_type = false;
1140 }
1141
1142 // Return.
1143 return $resource_type;
1144
1145 }
1146
1147 /**
1148 * Get the Post's Restricted Content resource ID.
1149 *
1150 * @since 2.1.0
1151 *
1152 * @return int Resource ID (product ID).
1153 */
1154 private function get_resource_id() {
1155
1156 // Initialize Post Setting classes.
1157 $this->post_settings = new ConvertKit_Post( $this->post_id );
1158
1159 // Get resource ID.
1160 $resource_id = $this->post_settings->get_restrict_content_id();
1161
1162 /**
1163 * Define the ConvertKit Resource ID that the visitor must be subscribed against
1164 * to access this content, overriding the Post setting.
1165 *
1166 * Return 0 to not restrict content.
1167 *
1168 * @since 2.1.0
1169 *
1170 * @param int $resource_id Resource ID
1171 * @param int $post_id Post ID
1172 */
1173 $resource_id = apply_filters( 'convertkit_output_restrict_content_get_resource_id', $resource_id, $this->post_id );
1174
1175 // Return.
1176 return $resource_id;
1177
1178 }
1179
1180 /**
1181 * Queries the API to confirm whether the resource exists.
1182 *
1183 * @since 2.3.3
1184 *
1185 * @return bool
1186 */
1187 private function resource_exists() {
1188
1189 switch ( $this->resource_type ) {
1190
1191 case 'product':
1192 // Get Product.
1193 $products = new ConvertKit_Resource_Products( 'restrict_content' );
1194 $product = $products->get_by_id( $this->resource_id );
1195
1196 // If the Product does not exist, return false.
1197 if ( ! $product ) {
1198 return false;
1199 }
1200
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 ) {
1211 return false;
1212 }
1213
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 ) {
1224 return false;
1225 }
1226
1227 // Tag exists in ConvertKit.
1228 return true;
1229
1230 default:
1231 return false;
1232
1233 }
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
1260 // If here, the subscriber does not have access.
1261 return false;
1262
1263 }
1264
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 /**
1356 * Gets the subscriber ID from the request (either the cookie or the URL).
1357 *
1358 * @since 2.1.0
1359 *
1360 * @return int|string Subscriber ID or Signed ID
1361 */
1362 public function get_subscriber_id_from_request() {
1363
1364 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
1365 $subscriber = new ConvertKit_Subscriber();
1366 $subscriber_id = $subscriber->get_subscriber_id();
1367
1368 // If an error occurred, the subscriber ID in the request/cookie is not a valid subscriber.
1369 if ( is_wp_error( $subscriber_id ) ) {
1370 return 0;
1371 }
1372
1373 return $subscriber_id;
1374
1375 }
1376
1377 /**
1378 * Restrict the given Post Content by showing a preview of the content, and appending
1379 * the call to action to subscribe or authenticate.
1380 *
1381 * @since 2.1.0
1382 *
1383 * @param string $content Post Content.
1384 * @return string Post Content preview with call to action
1385 */
1386 private function restrict_content( $content ) {
1387
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 }
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
1449 }
1450
1451 /**
1452 * Returns a preview of the given content for visitors that don't have access to restricted content.
1453 *
1454 * The preview is determined by:
1455 * - A single <!--more--> tag being placed between WordPress paragraphs when using the Classic Editor.
1456 * Content before the tag will be returned as the preview, unless 'noteaser' is enabled.
1457 * - A single 'Read More' block being placed between WordPress blocks when using the Gutenberg Editor.
1458 * Content before the Read More block will be returned as the preview, unless 'Hide the excerpt
1459 * on the full content page' is enabled.
1460 *
1461 * If no more tag or Read More block is present, returns the Post's excerpt.
1462 *
1463 * @since 2.1.0
1464 *
1465 * @param string $content Post Content.
1466 * @return string Post Content Preview.
1467 */
1468 private function get_content_preview( $content ) {
1469
1470 global $post;
1471
1472 // Check if the content contains a <!--more--> tag, which the editor might have placed
1473 // in the content through WordPress' Classic Editor.
1474 $content_breakdown = get_extended( $content );
1475
1476 // If the <!-- more --> tag exists, the 'extended' key will contain the restricted content.
1477 if ( ! empty( $content_breakdown['extended'] ) ) {
1478 // Return the preview content.
1479 return $content_breakdown['main'];
1480 }
1481
1482 // Check if the content contains a 'Read More' block, which the editor might have placed
1483 // in the content through the Gutenberg Editor.
1484 $block_editor_tag = '<span id="more-' . $post->ID . '"></span>';
1485 if ( strpos( $content, $block_editor_tag ) !== false ) {
1486 // Split content into an array by the tag.
1487 $content_breakdown = explode( $block_editor_tag, $content );
1488
1489 // Return the content before the tag.
1490 // If noteaser is enabled, this will correctly be blank.
1491 return $content_breakdown[0];
1492 }
1493
1494 // If here, there is no preview content available. Use the Post's excerpt.
1495 return $this->get_excerpt( $post->ID );
1496
1497 }
1498
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 /**
1527 * Returns the HTML output for the call to action for visitors not subscribed to the required
1528 * resource type and ID.
1529 *
1530 * @since 2.1.0
1531 *
1532 * @param int $post_id Post ID.
1533 * @return string HTML
1534 */
1535 private function get_call_to_action( $post_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
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
1552 // This is deliberately a switch statement, because we will likely add in support
1553 // for restrict by tag and form later.
1554 switch ( $resource_type ) {
1555 case 'product':
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' );
1559
1560 // Output product restricted message and email form.
1561 // Get Product.
1562 $products = new ConvertKit_Resource_Products( 'restrict_content' );
1563 $product = $products->get_by_id( $resource_id );
1564
1565 // Get commerce.js URL and enqueue.
1566 $url = $products->get_commerce_js_url();
1567 if ( $url ) {
1568 wp_enqueue_script( 'convertkit-commerce', $url, array(), CONVERTKIT_PLUGIN_VERSION, true );
1569 }
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
1577 // Output.
1578 ob_start();
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 );
1586 include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/product.php';
1587 return trim( ob_get_clean() );
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
1621 default:
1622 return '';
1623
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;
2195
2196 }
2197
2198 }
2199