PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.3
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.3
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 2.2.9 2.3.0 2.3.1 All 196 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.3, at includes/class-convertkit-output-restrict-content.php

2,199 lines 63.5 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 }
642
643 /**
644 * Outputs the login modal in the footer, ensuring it is only output once
645 * when a Post contains multiple Member Content Login blocks.
646 *
647 * @since 3.4.2
648 *
649 * @param int $post_id Post ID.
650 * @param bool|int $resource_id Resource ID.
651 * @param bool|string $resource_type Resource Type.
652 */
653 public function output_login_modal( $post_id, $resource_id = 0, $resource_type = '' ) {
654
655 if ( $this->login_modal_output ) {
656 return;
657 }
658
659 $this->login_modal_output = true;
660
661 add_action(
662 'wp_footer',
663 function () use ( $post_id, $resource_id, $resource_type ) {
664
665 include_once CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal.php';
666
667 }
668 );
669
670 }
671
672 /**
673 * Sends an email to the subscriber with a code and link to authenticate they have access to the email address submitted.
674 *
675 * @since 3.1.0
676 *
677 * @param string $email Email address.
678 * @param int $post_id Post ID.
679 *
680 * @return WP_Error|string Error or Token.
681 */
682 public function subscriber_authentication_send_code( $email, $post_id ) {
683
684 // Send email to subscriber with a link to authenticate they have access to the email address submitted.
685 $token = $this->api->subscriber_authentication_send_code(
686 $email,
687 $this->get_url( $post_id )
688 );
689
690 // Bail if an error occurred.
691 if ( is_wp_error( $token ) ) {
692 return $token;
693 }
694
695 // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email.
696 $subscriber = new ConvertKit_Subscriber();
697 $subscriber->forget();
698
699 // Return the token.
700 return $token;
701
702 }
703
704 /**
705 * Verifies the token and subscriber code, which tells us that the email
706 * address supplied truly belongs to the user, and that we can safely
707 * trust their subscriber ID to be valid.
708 *
709 * @since 3.1.0
710 *
711 * @param int $post_id Post ID.
712 * @param string $token Token.
713 * @param string $subscriber_code Subscriber code.
714 *
715 * @return WP_Error|string Error or Signed Subscriber ID.
716 */
717 public function subscriber_authentication_verify( $post_id, $token, $subscriber_code ) {
718
719 // Verify the token and subscriber code.
720 $subscriber_id = $this->api->subscriber_authentication_verify( $token, $subscriber_code );
721
722 // Bail if an error occurred.
723 if ( is_wp_error( $subscriber_id ) ) {
724 return $subscriber_id;
725 }
726
727 // Store subscriber ID in cookie.
728 $this->store_subscriber_id_in_cookie( $subscriber_id );
729
730 // Return signed subscriber ID.
731 return $subscriber_id;
732
733 }
734
735 /**
736 * Registers the applicable content filter for maybe restricting content, depending
737 * on the Theme or Page Builder used.
738 *
739 * @since 2.7.7
740 */
741 public function register_content_filter() {
742
743 // Use the standard `the_content` filter, which works for most Themes
744 // and Page Builders.
745 add_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
746
747 /**
748 * Allow specific Themes and Page Builders to use a different filter
749 * for Restrict Content functionality.
750 *
751 * @since 2.7.7
752 */
753 do_action( 'convertkit_restrict_content_register_content_filter' );
754
755 }
756
757 /**
758 * Displays (or hides) content on a singular Page, Post or Custom Post Type's Content,
759 * depending on whether the visitor is an authenticated ConvertKit subscriber and has
760 * subscribed to the ConvertKit Product or Tag.
761 *
762 * @since 2.1.0
763 *
764 * @param string $content Post Content.
765 * @return string Post Content with content restricted/not restricted
766 */
767 public function maybe_restrict_content( $content ) {
768
769 // Bail if the Restrict Content setting is not enabled on this Page.
770 if ( ! $this->is_restricted_content() ) {
771 return $content;
772 }
773
774 // Bail if the Page is being edited in a frontend Page Builder / Editor by a logged
775 // in WordPress user who has the capability to edit the Page.
776 // This ensures the User can view all content to edit it, instead of seeing the Restrict Content
777 // view.
778 if ( current_user_can( 'edit_post', get_the_ID() ) && WP_ConvertKit()->is_admin_or_frontend_editor() ) {
779 return $content;
780 }
781
782 // Get resource type (Product or Tag) that the visitor must be subscribed against to access this content.
783 $this->resource_type = $this->get_resource_type();
784
785 // Return the Post Content, unedited, if the Resource Type is false.
786 if ( ! $this->resource_type ) {
787 return $content;
788 }
789
790 // Get resource ID (Product ID or Tag ID) that the visitor must be subscribed against to access this content.
791 $this->resource_id = $this->get_resource_id();
792
793 // Return the full Post Content, unedited, if the Resource ID is false, as this means
794 // no restrict content setting has been defined for this Post.
795 if ( ! $this->resource_id ) {
796 return $content;
797 }
798
799 // Return the full Post Content, unedited, if the request is from a crawler.
800 if ( $this->restrict_content_settings->permit_crawlers() && $this->is_crawler() ) {
801 return $content;
802 }
803
804 // Return if this request is after the user entered their email address,
805 // which means we're going through the authentication flow.
806 if ( $this->in_authentication_flow() ) {
807 return $this->restrict_content( $content );
808 }
809
810 // Get the subscriber ID, either from the request or an existing cookie.
811 $subscriber_id = $this->get_subscriber_id_from_request();
812
813 // If no subscriber ID exists, the visitor cannot view the content.
814 if ( ! $subscriber_id ) {
815 return $this->restrict_content( $content );
816 }
817
818 // If the subscriber is not subscribed to the product, restrict the content.
819 if ( ! $this->subscriber_has_access( $subscriber_id ) ) {
820 // Show an error before the call to action, to tell the subscriber why they still cannot
821 // view the content.
822 switch ( $this->resource_type ) {
823 case 'form':
824 $message = $this->restrict_content_settings->get_by_key( 'no_access_text_form' );
825 break;
826
827 case 'tag':
828 $message = $this->restrict_content_settings->get_by_key( 'no_access_text_tag' );
829 break;
830
831 case 'product':
832 default:
833 $message = $this->restrict_content_settings->get_by_key( 'no_access_text' );
834 break;
835 }
836
837 // Define error for output.
838 $this->error = new WP_Error(
839 'convertkit_restrict_content_subscriber_no_access',
840 esc_html( $message )
841 );
842
843 return $this->restrict_content( $content );
844 }
845
846 // If here, the subscriber has subscribed to the product.
847 // Show the full Post Content.
848 return $content;
849
850 }
851
852 /**
853 * Changes how WordPress' get_adjacent_post() function queries Pages, to determine what
854 * the previous Page link is when using the Previous navigation block on a Page that
855 * has the Restrict Content setting defined.
856 *
857 * By default, get_adjacent_post() will query by post_date, which we change to menu_order.
858 *
859 * @since 2.1.0
860 *
861 * @param string $where The `WHERE` clause in the SQL.
862 * @param bool $in_same_term Whether post should be in a same taxonomy term.
863 * @param array $excluded_terms Array of excluded term IDs.
864 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
865 * @param WP_Post $post WP_Post object.
866 * @return string Modified `WHERE` clause
867 */
868 public function maybe_change_previous_post_where_clause( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
869
870 // Bail if the Restrict Content setting is not enabled on this Page.
871 if ( ! $this->is_restricted_content() ) {
872 return $where;
873 }
874
875 // Bail if the Page doesn't match the current Page being viewed, or has no parent Page.
876 if ( ! $this->has_parent_page( $post ) ) {
877 return $where;
878 }
879
880 // Build replacement where statement.
881 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order < ' . $post->menu_order;
882
883 // Replace existing where statement with new statement.
884 $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
885
886 // Return.
887 return $where;
888
889 }
890
891 /**
892 * Changes how WordPress' get_adjacent_post() function queries Pages, to determine what
893 * the next Page link is when using the Previous navigation block on a Page that
894 * has the Restrict Content setting defined.
895 *
896 * By default, get_adjacent_post() will query by post_date, which we change to menu_order.
897 *
898 * @since 2.1.0
899 *
900 * @param string $where The `WHERE` clause in the SQL.
901 * @param bool $in_same_term Whether post should be in a same taxonomy term.
902 * @param array $excluded_terms Array of excluded term IDs.
903 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
904 * @param WP_Post $post WP_Post object.
905 * @return string Modified `WHERE` clause
906 */
907 public function maybe_change_next_post_where_clause( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
908
909 // Bail if the Restrict Content setting is not enabled on this Page.
910 if ( ! $this->is_restricted_content() ) {
911 return $where;
912 }
913
914 // Bail if the Page doesn't match the current Page being viewed, or has no parent Page.
915 if ( ! $this->has_parent_page( $post ) ) {
916 return $where;
917 }
918
919 // Build replacement where statement.
920 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order > ' . $post->menu_order;
921
922 // Replace existing where statement with new statement.
923 $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
924
925 // Return.
926 return $where;
927
928 }
929
930 /**
931 * Changes how WordPress' get_adjacent_post() function orders Pages, to determine what
932 * the next and previous Page links are when using Previous / Next navigation blocks
933 * on a Page that has the Restrict Content setting defined.
934 *
935 * By default, get_adjacent_post() will sort by Post Date, which we change to Page Order
936 * (called menu_order in WordPress).
937 *
938 * @since 2.1.0
939 *
940 * @param string $order_by SQL ORDER BY statement.
941 * @param WP_Post $post WordPress Post.
942 * @param string $order Order.
943 * @return string Modified SQL ORDER BY statement.
944 */
945 public function maybe_change_previous_next_post_order_by_clause( $order_by, $post, $order ) {
946
947 // Bail if the Restrict Content setting is not enabled on this Page.
948 if ( ! $this->is_restricted_content() ) {
949 return $order_by;
950 }
951
952 // Bail if the Page doesn't match the current Page being viewed, or has no parent Page.
953 if ( ! $this->has_parent_page( $post ) ) {
954 return $order_by;
955 }
956
957 // Order by Page order (menu_order), highest to lowest, instead of post_date.
958 return 'ORDER BY p.menu_order ' . $order . ' LIMIT 1';
959
960 }
961
962 /**
963 * Stores the given subscriber ID in the ck_subscriber_id cookie.
964 *
965 * @since 2.3.7
966 *
967 * @param string|int $subscriber_id Subscriber ID (int if restrict by tag, signed subscriber id string if restrict by product).
968 */
969 private function store_subscriber_id_in_cookie( $subscriber_id ) {
970
971 // Store subscriber ID in cookie.
972 // We don't need to use validate_and_store_subscriber_id() as we just validated the subscriber via authentication above.
973 $subscriber = new ConvertKit_Subscriber();
974 $subscriber->set( $subscriber_id );
975
976 }
977
978 /**
979 * Redirects to the current URL, removing any query parameters (such as tokens), and appending
980 * a ck-cache-bust query parameter to beat caching plugins.
981 *
982 * @since 2.3.7
983 *
984 * @param int $post_id Post ID.
985 */
986 private function redirect( $post_id ) {
987
988 // Redirect to the Post, appending a query parameter to the URL to prevent caching plugins and
989 // aggressive cache hosting configurations from serving a cached page, which would
990 // result in maybe_restrict_content() not showing an error message or permitting
991 // access to the content.
992 wp_safe_redirect( $this->get_url( $post_id, true ) );
993 exit;
994
995 }
996
997 /**
998 * Returns the URL for the current request, excluding any query parameters.
999 *
1000 * @since 2.1.0
1001 *
1002 * @param int $post_id Post ID.
1003 * @param bool $cache_bust Include `ck-cache-bust` parameter in URL.
1004 * @return string URL.
1005 */
1006 public function get_url( $post_id, $cache_bust = false ) {
1007
1008 // Get URL of Post.
1009 $url = get_permalink( $post_id );
1010
1011 // If no cache busting required, return the URL now.
1012 if ( ! $cache_bust ) {
1013 return $url;
1014 }
1015
1016 // Append a query parameter to the URL to prevent caching plugins and
1017 // aggressive cache hosting configurations from serving a cached page, which would
1018 // result in maybe_restrict_content() not showing an error message or permitting
1019 // access to the content.
1020 return add_query_arg(
1021 array(
1022 'ck-cache-bust' => microtime(),
1023 ),
1024 $url
1025 );
1026
1027 }
1028
1029 /**
1030 * Determines if the request is for a WordPress Page that has the Restrict Content
1031 * setting defined.
1032 *
1033 * @since 2.1.0
1034 *
1035 * @return bool
1036 */
1037 private function is_restricted_content() {
1038
1039 // Bail if not a singular Post Type.
1040 if ( ! is_singular() ) {
1041 return false;
1042 }
1043
1044 // If the Plugin Access Token has not been configured, we can't determine the validity of this subscriber ID
1045 // or which resource(s) they have access to.
1046 if ( ! $this->settings->has_access_and_refresh_token() ) {
1047 return false;
1048 }
1049
1050 // Get Post ID.
1051 $this->post_id = get_the_ID();
1052
1053 // Initialize Settings and Post Setting classes.
1054 $this->post_settings = new ConvertKit_Post( $this->post_id );
1055
1056 // Return whether the Post's settings are set to restrict content.
1057 return $this->post_settings->restrict_content_enabled();
1058
1059 }
1060
1061 /**
1062 * Determines if the user entered a valid email address, and need to be prompted
1063 * to enter a code sent to their email address.
1064 *
1065 * @since 2.1.0
1066 *
1067 * @return bool
1068 */
1069 private function in_authentication_flow() {
1070
1071 return ( $this->token !== false );
1072
1073 }
1074
1075 /**
1076 * Checks if the given WordPress Page matches the Page ID viewed, and has a parent.
1077 *
1078 * @since 2.1.0
1079 *
1080 * @param WP_Post $post WordPress Post.
1081 * @return bool Has parent page
1082 */
1083 private function has_parent_page( $post ) {
1084
1085 // Bail if the Page doesn't match the current Page being viewed.
1086 // This prevents us accidentally interfering with other previous / next link queries, which shouldn't happen
1087 // as we check if we're viewing a restricted content page above.
1088 if ( $post->ID !== $this->post_id ) {
1089 return false;
1090 }
1091
1092 // Bail if the Page doesn't have a parent Page.
1093 // We don't want to modify the default sort behaviour in this instance.
1094 if ( $post->post_parent === 0 ) {
1095 return false;
1096 }
1097
1098 return true;
1099
1100 }
1101
1102 /**
1103 * Get the Post's Restricted Content resource type.
1104 *
1105 * @since 2.1.0
1106 *
1107 * @return bool|string Resource Type (product).
1108 */
1109 private function get_resource_type() {
1110
1111 // Initialize Post Setting classes.
1112 $this->post_settings = new ConvertKit_Post( $this->post_id );
1113
1114 // Get resource type.
1115 $resource_type = $this->post_settings->get_restrict_content_type();
1116
1117 /**
1118 * Define the ConvertKit Resource Type that the visitor must be subscribed against
1119 * to access this content, overriding the Post setting.
1120 *
1121 * Return false or an empty string to not restrict content.
1122 *
1123 * @since 2.1.0
1124 *
1125 * @param string $resource_type Resource Type (product)
1126 * @param int $post_id Post ID
1127 */
1128 $resource_type = apply_filters( 'convertkit_output_restrict_content_get_resource_type', $resource_type, $this->post_id );
1129
1130 // If resource type is blank, set it to false.
1131 if ( empty( $resource_type ) ) {
1132 $resource_type = false;
1133 }
1134
1135 // Return.
1136 return $resource_type;
1137
1138 }
1139
1140 /**
1141 * Get the Post's Restricted Content resource ID.
1142 *
1143 * @since 2.1.0
1144 *
1145 * @return int Resource ID (product ID).
1146 */
1147 private function get_resource_id() {
1148
1149 // Initialize Post Setting classes.
1150 $this->post_settings = new ConvertKit_Post( $this->post_id );
1151
1152 // Get resource ID.
1153 $resource_id = $this->post_settings->get_restrict_content_id();
1154
1155 /**
1156 * Define the ConvertKit Resource ID that the visitor must be subscribed against
1157 * to access this content, overriding the Post setting.
1158 *
1159 * Return 0 to not restrict content.
1160 *
1161 * @since 2.1.0
1162 *
1163 * @param int $resource_id Resource ID
1164 * @param int $post_id Post ID
1165 */
1166 $resource_id = apply_filters( 'convertkit_output_restrict_content_get_resource_id', $resource_id, $this->post_id );
1167
1168 // Return.
1169 return $resource_id;
1170
1171 }
1172
1173 /**
1174 * Queries the API to confirm whether the resource exists.
1175 *
1176 * @since 2.3.3
1177 *
1178 * @return bool
1179 */
1180 private function resource_exists() {
1181
1182 switch ( $this->resource_type ) {
1183
1184 case 'product':
1185 // Get Product.
1186 $products = new ConvertKit_Resource_Products( 'restrict_content' );
1187 $product = $products->get_by_id( $this->resource_id );
1188
1189 // If the Product does not exist, return false.
1190 if ( ! $product ) {
1191 return false;
1192 }
1193
1194 // Product exists in ConvertKit.
1195 return true;
1196
1197 case 'form':
1198 // Get Form.
1199 $forms = new ConvertKit_Resource_Forms( 'restrict_content' );
1200 $form = $forms->get_by_id( $this->resource_id );
1201
1202 // If the Form does not exist, return false.
1203 if ( ! $form ) {
1204 return false;
1205 }
1206
1207 // Form exists in ConvertKit.
1208 return true;
1209
1210 case 'tag':
1211 // Get Tag.
1212 $tags = new ConvertKit_Resource_Tags( 'restrict_content' );
1213 $tag = $tags->get_by_id( $this->resource_id );
1214
1215 // If the Tag does not exist, return false.
1216 if ( ! $tag ) {
1217 return false;
1218 }
1219
1220 // Tag exists in ConvertKit.
1221 return true;
1222
1223 default:
1224 return false;
1225
1226 }
1227
1228 }
1229
1230 /**
1231 * Determines if the given subscriber has an active subscription to
1232 * the given resource and its ID.
1233 *
1234 * @since 2.1.0
1235 *
1236 * @param string|int $subscriber_id Signed Subscriber ID or Subscriber ID.
1237 * @return bool Can view restricted content
1238 */
1239 private function subscriber_has_access( $subscriber_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1240
1241 switch ( $this->resource_type ) {
1242 case 'product':
1243 return $this->subscriber_has_access_to_product_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1244
1245 case 'form':
1246 return $this->subscriber_has_access_to_form_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1247
1248 case 'tag':
1249 return $this->subscriber_has_access_to_tag_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1250
1251 }
1252
1253 // If here, the subscriber does not have access.
1254 return false;
1255
1256 }
1257
1258 /**
1259 * Determines if the given signed subscriber ID has an active subscription to
1260 * the given product.
1261 *
1262 * @since 2.7.1
1263 *
1264 * @param string $signed_subscriber_id Signed Subscriber ID.
1265 * @param int $product_id Product ID.
1266 * @return bool Has access to product
1267 */
1268 private function subscriber_has_access_to_product_by_signed_subscriber_id( $signed_subscriber_id, $product_id ) {
1269
1270 // Get products that the subscriber has access to.
1271 $result = $this->api->profile( $signed_subscriber_id );
1272
1273 // If an error occurred, the subscriber ID is invalid.
1274 if ( is_wp_error( $result ) ) {
1275 return false;
1276 }
1277
1278 // If no products exist, there's no access.
1279 if ( ! $result['products'] || ! count( $result['products'] ) ) {
1280 return false;
1281 }
1282
1283 // Return if the subscriber is subscribed to the product or not.
1284 return in_array( $product_id, $result['products'], true );
1285
1286 }
1287
1288 /**
1289 * Determines if the given signed subscriber ID has an active subscription to
1290 * the given form.
1291 *
1292 * @since 2.7.3
1293 *
1294 * @param string $signed_subscriber_id Signed Subscriber ID.
1295 * @param int $form_id Form ID.
1296 * @return bool Has access to form
1297 */
1298 private function subscriber_has_access_to_form_by_signed_subscriber_id( $signed_subscriber_id, $form_id ) {
1299
1300 // Get products that the subscriber has access to.
1301 $result = $this->api->profile( $signed_subscriber_id );
1302
1303 // If an error occurred, the subscriber ID is invalid.
1304 if ( is_wp_error( $result ) ) {
1305 return false;
1306 }
1307
1308 // If no forms exist, there's no access.
1309 if ( ! $result['forms'] || ! count( $result['forms'] ) ) {
1310 return false;
1311 }
1312
1313 // Return if the subscriber is subscribed to the form or not.
1314 return in_array( $form_id, $result['forms'], true );
1315
1316 }
1317
1318 /**
1319 * Determines if the given signed subscriber ID has an active subscription to
1320 * the given tag.
1321 *
1322 * @since 2.7.1
1323 *
1324 * @param string $signed_subscriber_id Signed Subscriber ID.
1325 * @param int $tag_id Tag ID.
1326 * @return bool Has access to tag
1327 */
1328 private function subscriber_has_access_to_tag_by_signed_subscriber_id( $signed_subscriber_id, $tag_id ) {
1329
1330 // Get products that the subscriber has access to.
1331 $result = $this->api->profile( $signed_subscriber_id );
1332
1333 // If an error occurred, the subscriber ID is invalid.
1334 if ( is_wp_error( $result ) ) {
1335 return false;
1336 }
1337
1338 // If no tags exist, there's no access.
1339 if ( ! $result['tags'] || ! count( $result['tags'] ) ) {
1340 return false;
1341 }
1342
1343 // Return if the subscriber is subscribed to the tag or not.
1344 return in_array( $tag_id, $result['tags'], true );
1345
1346 }
1347
1348 /**
1349 * Gets the subscriber ID from the request (either the cookie or the URL).
1350 *
1351 * @since 2.1.0
1352 *
1353 * @return int|string Subscriber ID or Signed ID
1354 */
1355 public function get_subscriber_id_from_request() {
1356
1357 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
1358 $subscriber = new ConvertKit_Subscriber();
1359 $subscriber_id = $subscriber->get_subscriber_id();
1360
1361 // If an error occurred, the subscriber ID in the request/cookie is not a valid subscriber.
1362 if ( is_wp_error( $subscriber_id ) ) {
1363 return 0;
1364 }
1365
1366 return $subscriber_id;
1367
1368 }
1369
1370 /**
1371 * Restrict the given Post Content by showing a preview of the content, and appending
1372 * the call to action to subscribe or authenticate.
1373 *
1374 * @since 2.1.0
1375 *
1376 * @param string $content Post Content.
1377 * @return string Post Content preview with call to action
1378 */
1379 private function restrict_content( $content ) {
1380
1381 // Check that the resource exists before restricting the content.
1382 // This handles cases where e.g. a Tag or Product has been deleted in ConvertKit,
1383 // but the Page / Post still references the (now deleted) resource to restrict content with
1384 // under the 'Member Content' setting.
1385 if ( ! $this->resource_exists() ) {
1386 // Return the full Post Content, as we can't restrict it to a Product or Tag that no longer exists.
1387 return $content;
1388 }
1389
1390 // Fetch the content preview.
1391 $content_preview = $this->get_content_preview( $content );
1392
1393 /**
1394 * Define the output for the content preview when the visitor is not
1395 * an authenticated subscriber.
1396 *
1397 * @since 2.4.1
1398 *
1399 * @param string $content_preview Content preview.
1400 * @param int $post_id Post ID.
1401 */
1402 $content_preview = apply_filters( 'convertkit_output_restrict_content_content_preview', $content_preview, $this->post_id );
1403
1404 // Fetch the call to action.
1405 $call_to_action = $this->get_call_to_action( $this->post_id );
1406
1407 /**
1408 * Define the output for the call to action, displayed below the content preview,
1409 * when the visitor is not an authenticated subscriber.
1410 *
1411 * @since 2.4.1
1412 *
1413 * @param string $call_to_action Call to Action.
1414 * @param int $post_id Post ID.
1415 */
1416 $call_to_action = apply_filters( 'convertkit_output_restrict_content_call_to_action', $call_to_action, $this->post_id );
1417
1418 // Fetch container CSS classes.
1419 $container_css_classes = explode( ' ', $this->restrict_content_settings->get_by_key( 'container_css_classes' ) );
1420
1421 /**
1422 * Define the container CSS classes to wrap the content preview and call to action within.
1423 *
1424 * @since 3.1.4
1425 *
1426 * @param array $container_css_classes Container CSS classes.
1427 * @param int $post_id Post ID.
1428 */
1429 $container_css_classes = apply_filters( 'convertkit_output_restrict_content_container_css_classes', $container_css_classes, $this->post_id );
1430
1431 // Remove empty CSS classes.
1432 $container_css_classes = array_filter( $container_css_classes );
1433
1434 // If container CSS classes are set, return the content preview and call to action wrapped in the container.
1435 if ( count( $container_css_classes ) ) {
1436 return '<div class="' . trim( implode( ' ', map_deep( $container_css_classes, 'sanitize_html_class' ) ) ) . '">' . $content_preview . $call_to_action . '</div>';
1437 }
1438
1439 // Return the content preview and its call to action.
1440 return $content_preview . $call_to_action;
1441
1442 }
1443
1444 /**
1445 * Returns a preview of the given content for visitors that don't have access to restricted content.
1446 *
1447 * The preview is determined by:
1448 * - A single <!--more--> tag being placed between WordPress paragraphs when using the Classic Editor.
1449 * Content before the tag will be returned as the preview, unless 'noteaser' is enabled.
1450 * - A single 'Read More' block being placed between WordPress blocks when using the Gutenberg Editor.
1451 * Content before the Read More block will be returned as the preview, unless 'Hide the excerpt
1452 * on the full content page' is enabled.
1453 *
1454 * If no more tag or Read More block is present, returns the Post's excerpt.
1455 *
1456 * @since 2.1.0
1457 *
1458 * @param string $content Post Content.
1459 * @return string Post Content Preview.
1460 */
1461 private function get_content_preview( $content ) {
1462
1463 global $post;
1464
1465 // Check if the content contains a <!--more--> tag, which the editor might have placed
1466 // in the content through WordPress' Classic Editor.
1467 $content_breakdown = get_extended( $content );
1468
1469 // If the <!-- more --> tag exists, the 'extended' key will contain the restricted content.
1470 if ( ! empty( $content_breakdown['extended'] ) ) {
1471 // Return the preview content.
1472 return $content_breakdown['main'];
1473 }
1474
1475 // Check if the content contains a 'Read More' block, which the editor might have placed
1476 // in the content through the Gutenberg Editor.
1477 $block_editor_tag = '<span id="more-' . $post->ID . '"></span>';
1478 if ( strpos( $content, $block_editor_tag ) !== false ) {
1479 // Split content into an array by the tag.
1480 $content_breakdown = explode( $block_editor_tag, $content );
1481
1482 // Return the content before the tag.
1483 // If noteaser is enabled, this will correctly be blank.
1484 return $content_breakdown[0];
1485 }
1486
1487 // If here, there is no preview content available. Use the Post's excerpt.
1488 return $this->get_excerpt( $post->ID );
1489
1490 }
1491
1492 /**
1493 * Returns the excerpt for the given Post.
1494 *
1495 * If no excerpt is defined, generates one from the Post's content.
1496 *
1497 * @since 2.3.7
1498 *
1499 * @param int $post_id Post ID.
1500 * @return string Post excerpt.
1501 */
1502 private function get_excerpt( $post_id ) {
1503
1504 // Remove 'the_content' filter, as if the Post contains no defined excerpt, WordPress
1505 // will invoke the Post's content to build an excerpt, resulting in an infinite loop.
1506 remove_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
1507
1508 // Generate the Post's excerpt.
1509 $excerpt = get_the_excerpt( $post_id );
1510
1511 // Restore filters so other functions and Plugins aren't affected.
1512 add_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
1513
1514 // Return the excerpt.
1515 return wpautop( $excerpt );
1516
1517 }
1518
1519 /**
1520 * Returns the HTML output for the call to action for visitors not subscribed to the required
1521 * resource type and ID.
1522 *
1523 * @since 2.1.0
1524 *
1525 * @param int $post_id Post ID.
1526 * @return string HTML
1527 */
1528 private function get_call_to_action( $post_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1529
1530 // Enqueue CSS and JS.
1531 $this->enqueue_scripts_and_styles();
1532
1533 // Output code form if this request is after the user entered their email address,
1534 // which means we're going through the authentication flow.
1535 if ( $this->in_authentication_flow() ) {
1536 ob_start();
1537 include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/code.php';
1538 return trim( ob_get_clean() );
1539 }
1540
1541 // Get resource type and id.
1542 $resource_type = $this->resource_type;
1543 $resource_id = $this->resource_id;
1544
1545 // This is deliberately a switch statement, because we will likely add in support
1546 // for restrict by tag and form later.
1547 switch ( $resource_type ) {
1548 case 'product':
1549 // Get header and text from settings for Products.
1550 $heading = $this->restrict_content_settings->get_by_key( 'subscribe_heading' );
1551 $text = $this->restrict_content_settings->get_by_key( 'subscribe_text' );
1552
1553 // Output product restricted message and email form.
1554 // Get Product.
1555 $products = new ConvertKit_Resource_Products( 'restrict_content' );
1556 $product = $products->get_by_id( $resource_id );
1557
1558 // Get commerce.js URL and enqueue.
1559 $url = $products->get_commerce_js_url();
1560 if ( $url ) {
1561 wp_enqueue_script( 'convertkit-commerce', $url, array(), CONVERTKIT_PLUGIN_VERSION, true );
1562 }
1563
1564 // If scripts are enabled, output the email login form in a modal, which will be displayed
1565 // when the 'log in' link is clicked.
1566 if ( ! $this->settings->scripts_disabled() ) {
1567 $this->output_login_modal( $post_id, $resource_id, $resource_type );
1568 }
1569
1570 // Output.
1571 ob_start();
1572 $button = $products->get_html(
1573 $resource_id,
1574 $this->restrict_content_settings->get_by_key( 'subscribe_button_label' ),
1575 array(
1576 'css_classes' => array( 'wp-block-button__link', 'wp-element-button' ),
1577 )
1578 );
1579 include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/product.php';
1580 return trim( ob_get_clean() );
1581
1582 case 'form':
1583 // Display the Form.
1584 $forms = new ConvertKit_Resource_Forms( 'restrict_content' );
1585 $form = $forms->get_html( $resource_id, $post_id );
1586
1587 // If scripts are enabled, output the email login form in a modal, which will be displayed
1588 // when the 'log in' link is clicked.
1589 if ( ! $this->settings->scripts_disabled() ) {
1590 $this->output_login_modal( $post_id, $resource_id, $resource_type );
1591 }
1592
1593 // Output.
1594 ob_start();
1595 include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/form.php';
1596 return trim( ob_get_clean() );
1597
1598 case 'tag':
1599 // Get header and text from settings for Tags.
1600 $heading = $this->restrict_content_settings->get_by_key( 'subscribe_heading_tag' );
1601 $text = $this->restrict_content_settings->get_by_key( 'subscribe_text_tag' );
1602
1603 // If scripts are enabled, output the email login form in a modal, which will be displayed
1604 // when the 'log in' link is clicked.
1605 if ( ! $this->settings->scripts_disabled() ) {
1606 $this->output_login_modal( $post_id, $resource_id, $resource_type );
1607 }
1608
1609 // Enqueue the active spam protection provider's client-side script.
1610 $spam = new ConvertKit_Spam_Protection();
1611 $spam_provider = $spam->get_active_provider();
1612 if ( $spam_provider !== false ) {
1613 $spam_provider->enqueue_scripts();
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',
1826 '51.8.253.152',
1827 '40.80.242.63',
1828 '20.12.141.99',
1829 '20.49.136.28',
1830 '51.116.131.221',
1831 '51.107.40.209',
1832 '20.40.133.240',
1833 '20.50.168.91',
1834 '51.120.48.122',
1835 '20.193.45.113',
1836 '40.76.173.151',
1837 '40.76.163.7',
1838 '20.185.79.47',
1839 '52.142.26.175',
1840 '20.185.79.15',
1841 '52.142.24.149',
1842 '40.76.162.208',
1843 '40.76.163.23',
1844 '40.76.162.191',
1845 '40.76.162.247',
1846 '40.88.21.235',
1847 '20.191.45.212',
1848 '52.146.59.12',
1849 '52.146.59.156',
1850 '52.146.59.154',
1851 '52.146.58.236',
1852 '20.62.224.44',
1853 '51.104.180.53',
1854 '51.104.180.47',
1855 '51.104.180.26',
1856 '51.104.146.225',
1857 '51.104.146.235',
1858 '20.73.202.147',
1859 '20.73.132.240',
1860 '20.71.12.143',
1861 '20.56.197.58',
1862 '20.56.197.63',
1863 '20.43.150.93',
1864 '20.43.150.85',
1865 '20.44.222.1',
1866 '40.89.243.175',
1867 '13.89.106.77',
1868 '52.143.242.6',
1869 '52.143.241.111',
1870 '52.154.60.82',
1871 '20.197.209.11',
1872 '20.197.209.27',
1873 '20.226.133.105',
1874 '191.234.216.4',
1875 '191.234.216.178',
1876 '20.53.92.211',
1877 '20.53.91.2',
1878 '20.207.99.197',
1879 '20.207.97.190',
1880 '40.81.250.205',
1881 '40.64.106.11',
1882 '40.64.105.247',
1883 '20.72.242.93',
1884 '20.99.255.235',
1885 '20.113.3.121',
1886 '52.224.16.221',
1887 '52.224.21.53',
1888 '52.224.20.204',
1889 '52.224.21.19',
1890 '52.224.20.249',
1891 '52.224.20.203',
1892 '52.224.20.190',
1893 '52.224.16.229',
1894 '52.224.21.20',
1895 '52.146.63.80',
1896 '52.224.20.227',
1897 '52.224.20.193',
1898 '52.190.37.160',
1899 '52.224.21.23',
1900 '52.224.20.223',
1901 '52.224.20.181',
1902 '52.224.21.49',
1903 '52.224.21.55',
1904 '52.224.21.61',
1905 '52.224.19.152',
1906 '52.224.20.186',
1907 '52.224.21.27',
1908 '52.224.21.51',
1909 '52.224.20.174',
1910 '52.224.21.4',
1911 '51.104.164.109',
1912 '51.104.167.71',
1913 '51.104.160.177',
1914 '51.104.162.149',
1915 '51.104.167.95',
1916 '51.104.167.54',
1917 '51.104.166.111',
1918 '51.104.167.88',
1919 '51.104.161.32',
1920 '51.104.163.250',
1921 '51.104.164.189',
1922 '51.104.167.19',
1923 '51.104.160.167',
1924 '51.104.167.110',
1925 '20.191.44.119',
1926 '51.104.167.104',
1927 '20.191.44.234',
1928 '51.104.164.215',
1929 '51.104.167.52',
1930 '20.191.44.22',
1931 '51.104.167.87',
1932 '51.104.167.96',
1933 '20.191.44.16',
1934 '51.104.167.61',
1935 '51.104.164.147',
1936 '20.50.48.159',
1937 '40.114.182.172',
1938 '20.50.50.130',
1939 '20.50.50.163',
1940 '20.50.50.46',
1941 '40.114.182.153',
1942 '20.50.50.118',
1943 '20.50.49.55',
1944 '20.50.49.25',
1945 '40.114.183.251',
1946 '20.50.50.123',
1947 '20.50.49.237',
1948 '20.50.48.192',
1949 '20.50.50.134',
1950 '51.138.90.233',
1951 '40.114.183.196',
1952 '20.50.50.146',
1953 '40.114.183.88',
1954 '20.50.50.145',
1955 '20.50.50.121',
1956 '20.50.49.40',
1957 '51.138.90.206',
1958 '40.114.182.45',
1959 '51.138.90.161',
1960 '20.50.49.0',
1961 '40.119.232.215',
1962 '104.43.55.167',
1963 '40.119.232.251',
1964 '40.119.232.50',
1965 '40.119.232.146',
1966 '40.119.232.218',
1967 '104.43.54.127',
1968 '104.43.55.117',
1969 '104.43.55.116',
1970 '104.43.55.166',
1971 '52.154.169.50',
1972 '52.154.171.70',
1973 '52.154.170.229',
1974 '52.154.170.113',
1975 '52.154.171.44',
1976 '52.154.172.2',
1977 '52.143.244.81',
1978 '52.154.171.87',
1979 '52.154.171.250',
1980 '52.154.170.28',
1981 '52.154.170.122',
1982 '52.143.243.117',
1983 '52.143.247.235',
1984 '52.154.171.235',
1985 '52.154.171.196',
1986 '52.154.171.0',
1987 '52.154.170.243',
1988 '52.154.170.26',
1989 '52.154.169.200',
1990 '52.154.170.96',
1991 '52.154.170.88',
1992 '52.154.171.150',
1993 '52.154.171.205',
1994 '52.154.170.117',
1995 '52.154.170.209',
1996 '191.235.202.48',
1997 '191.233.3.202',
1998 '191.235.201.214',
1999 '191.233.3.197',
2000 '191.235.202.38',
2001 '20.53.78.144',
2002 '20.193.24.10',
2003 '20.53.78.236',
2004 '20.53.78.138',
2005 '20.53.78.123',
2006 '20.53.78.106',
2007 '20.193.27.215',
2008 '20.193.25.197',
2009 '20.193.12.126',
2010 '20.193.24.251',
2011 '20.204.242.101',
2012 '20.207.72.113',
2013 '20.204.242.19',
2014 '20.219.45.67',
2015 '20.207.72.11',
2016 '20.219.45.190',
2017 '20.204.243.55',
2018 '20.204.241.148',
2019 '20.207.72.110',
2020 '20.204.240.172',
2021 '20.207.72.21',
2022 '20.204.246.81',
2023 '20.207.107.181',
2024 '20.204.246.254',
2025 '20.219.43.246',
2026 '52.149.25.43',
2027 '52.149.61.51',
2028 '52.149.58.139',
2029 '52.149.60.38',
2030 '52.148.165.38',
2031 '52.143.95.162',
2032 '52.149.56.151',
2033 '52.149.30.45',
2034 '52.149.58.173',
2035 '52.143.95.204',
2036 '52.149.28.83',
2037 '52.149.58.69',
2038 '52.148.161.87',
2039 '52.149.58.27',
2040 '52.149.28.18',
2041 '20.79.226.26',
2042 '20.79.239.66',
2043 '20.79.238.198',
2044 '20.113.14.159',
2045 '20.75.144.152',
2046 '20.43.172.120',
2047 '20.53.134.160',
2048 '20.201.15.208',
2049 '20.93.28.24',
2050 '20.61.34.40',
2051 '52.242.224.168',
2052 '20.80.129.80',
2053 '20.195.108.47',
2054 '4.195.133.120',
2055 '4.228.76.163',
2056 '4.182.131.108',
2057 '4.209.224.56',
2058 '108.141.83.74',
2059 '4.213.46.14',
2060 '172.169.17.165',
2061 '51.8.71.117',
2062 '20.3.1.178',
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