PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.3.9
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.3.9
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 2.3.2 2.3.3 All 194 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.3.9, at includes/class-convertkit-output-restrict-content.php

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