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
← All changes | includes/class-convertkit-output-restrict-content.php +1600 -171 2.2.53.4.3 View file →
@@ -14,17 +14,8 @@
14 14 */
15 15 class ConvertKit_Output_Restrict_Content {
16 16
17 17 /**
18 - * Holds the success message to display on screen as a notification.
19 - *
20 - * @since 2.1.0
21 - *
22 - * @var bool|string
23 - */
24 - private $success = false;
25 -
26 - /**
27 18 * Holds the WP_Error object if an API call / authentication failed,
28 19 * to display on screen as a notification.
29 20 *
30 21 * @since 2.1.0
@@ -30,9 +21,9 @@
30 21 * @since 2.1.0
31 22 *
32 23 * @var bool|WP_Error
33 24 */
34 - private $error = false;
25 + public $error = false;
35 26
36 27 /**
37 28 * Holds the ConvertKit Plugin Settings class
38 29 *
@@ -39,9 +30,9 @@
39 30 * @since 2.1.0
40 31 *
41 32 * @var bool|ConvertKit_Settings
42 33 */
43 - private $settings = false;
34 + public $settings = false;
44 35
45 36 /**
46 37 * Holds the ConvertKit Restrict Content Settings class
47 38 *
@@ -48,9 +39,9 @@
48 39 * @since 2.1.0
49 40 *
50 41 * @var bool|ConvertKit_Settings_Restrict_Content
51 42 */
52 - private $restrict_content_settings = false;
43 + public $restrict_content_settings = false;
53 44
54 45 /**
55 46 * Holds the ConvertKit Post Settings class
56 47 *
@@ -57,11 +48,31 @@
57 48 * @since 2.1.0
58 49 *
59 50 * @var bool|ConvertKit_Post
60 51 */
61 - private $post_settings = false;
52 + public $post_settings = false;
62 53
63 54 /**
55 + * Holds the Resource Type (product|tag) that must be subscribed to in order
56 + * to grant access to the Post.
57 + *
58 + * @since 2.3.8
59 + *
60 + * @var bool|string
61 + */
62 + public $resource_type = false;
63 +
64 + /**
65 + * Holds the Resource ID that must be subscribed to in order
66 + * to grant access to the Post.
67 + *
68 + * @since 2.3.8
69 + *
70 + * @var bool|int
71 + */
72 + public $resource_id = false;
73 +
74 + /**
64 75 * Holds the Post ID
65 76 *
66 77 * @since 2.1.0
67 78 *
@@ -66,9 +77,9 @@
66 77 * @since 2.1.0
67 78 *
68 79 * @var bool|int
69 80 */
70 - private $post_id = false;
81 + public $post_id = false;
71 82
72 83 /**
73 84 * Holds the ConvertKit API class
74 85 *
@@ -73,11 +84,11 @@
73 84 * Holds the ConvertKit API class
74 85 *
75 86 * @since 2.1.0
76 87 *
77 - * @var bool|ConvertKit_API
88 + * @var bool|ConvertKit_API_V4
78 89 */
79 - private $api = false;
90 + public $api = false;
80 91
81 92 /**
82 93 * Holds the token returned from calling the subscriber_authentication_send_code API endpoint.
83 94 *
@@ -84,11 +95,20 @@
84 95 * @since 2.1.0
85 96 *
86 97 * @var bool|string
87 98 */
88 - private $token = false;
99 + public $token = false;
89 100
90 101 /**
102 + * Whether the login modal has been output.
103 + *
104 + * @since 3.4.2
105 + *
106 + * @var bool
107 + */
108 + public $login_modal_output = false;
109 +
110 + /**
91 111 * Constructor. Registers actions and filters to possibly limit output of a Page/Post/CPT's
92 112 * content on the frontend site.
93 113 *
94 114 * @since 2.1.0
@@ -94,20 +114,14 @@
94 114 * @since 2.1.0
95 115 */
96 116 public function __construct() {
97 117
98 - // Initialize classes that will be used.
99 - $this->settings = new ConvertKit_Settings();
100 - $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content();
101 -
102 - // Bail if Restrict Content isn't enabled.
103 - if ( ! $this->restrict_content_settings->enabled() ) {
104 - return;
105 - }
106 -
107 - add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 1 );
108 - add_action( 'init', array( $this, 'maybe_run_subscriber_verification' ), 2 );
109 - add_filter( 'the_content', array( $this, 'maybe_restrict_content' ) );
118 + add_action( 'rest_api_init', array( $this, 'register_routes' ) );
119 + add_action( 'init', array( $this, 'initialize_classes' ), 2 );
120 + add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 );
121 + add_action( 'wp', array( $this, 'maybe_run_subscriber_logout' ), 3 );
122 + add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 );
123 + add_action( 'wp', array( $this, 'register_content_filter' ), 5 );
110 124 add_filter( 'get_previous_post_where', array( $this, 'maybe_change_previous_post_where_clause' ), 10, 5 );
111 125 add_filter( 'get_next_post_where', array( $this, 'maybe_change_next_post_where_clause' ), 10, 5 );
112 126 add_filter( 'get_previous_post_sort', array( $this, 'maybe_change_previous_next_post_order_by_clause' ), 10, 3 );
113 127 add_filter( 'get_next_post_sort', array( $this, 'maybe_change_previous_next_post_order_by_clause' ), 10, 3 );
@@ -114,63 +128,367 @@
114 128
115 129 }
116 130
117 131 /**
118 - * Checks if the request is a Restrict Content login request with an email address,
119 - * calling the API to send the subscriber a magic link by email.
132 + * Register REST API routes.
120 133 *
121 - * Once they click the link in the email, maybe_run_subscriber_verification() will run.
134 + * @since 3.1.0
135 + */
136 + public function register_routes() {
137 +
138 + // Register route to run subscriber authentication.
139 + register_rest_route(
140 + 'kit/v1',
141 + '/restrict-content/subscriber-authentication',
142 + array(
143 + 'methods' => WP_REST_Server::CREATABLE,
144 + 'args' => array(
145 + // Email: Validate email is included in the request, is a valid email address
146 + // and sanitize the email address.
147 + 'convertkit_email' => array(
148 + 'required' => true,
149 + 'validate_callback' => function ( $param ) {
150 +
151 + return is_string( $param ) && is_email( $param );
152 +
153 + },
154 + 'sanitize_callback' => 'sanitize_email',
155 + ),
156 +
157 + // Post ID: Validate post ID is included in the request and is an integer.
158 + 'convertkit_post_id' => array(
159 + 'required' => true,
160 + 'validate_callback' => function ( $param ) {
161 +
162 + return is_numeric( $param );
163 +
164 + },
165 + 'sanitize_callback' => 'absint',
166 + ),
167 +
168 + // Resource Type: Validate resource type is a string, if included in the request.
169 + // It's not included when logging in using the Member Content Login block.
170 + 'convertkit_resource_type' => array(
171 + 'required' => false,
172 + 'validate_callback' => function ( $param ) {
173 +
174 + return is_string( $param );
175 +
176 + },
177 + 'sanitize_callback' => 'sanitize_text_field',
178 + ),
179 +
180 + // Resource ID: Validate resource ID is an integer, if included in the request.
181 + // It's not included when logging in using the Member Content Login block.
182 + 'convertkit_resource_id' => array(
183 + 'required' => false,
184 + 'validate_callback' => function ( $param ) {
185 +
186 + return is_numeric( $param );
187 +
188 + },
189 + 'sanitize_callback' => 'absint',
190 + ),
191 +
192 + // Spam protection response, if a spam protection provider is enabled.
193 + 'spam_protection_response' => array(
194 + 'required' => false,
195 + 'validate_callback' => function ( $param ) {
196 +
197 + return is_string( $param );
198 +
199 + },
200 + 'sanitize_callback' => 'sanitize_text_field',
201 + ),
202 +
203 + // Whether to display the heading above the login form.
204 + // It's not displayed by the Member Content Login block, as it refers to
205 + // reading the Member Content the subscriber is logging in to view.
206 + 'display_heading' => array(
207 + 'required' => false,
208 + 'default' => true,
209 + 'validate_callback' => function ( $param ) {
210 +
211 + return is_bool( $param );
212 +
213 + },
214 + ),
215 + ),
216 + 'callback' => function ( $request ) {
217 +
218 + // Initialize classes that will be used.
219 + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' );
220 + $output_restrict_content->initialize_classes();
221 +
222 + // Fetch Post ID, Resource Type and Resource ID for the view.
223 + $email = $request->get_param( 'convertkit_email' );
224 + $post_id = $request->get_param( 'convertkit_post_id' );
225 + $resource_type = $request->get_param( 'convertkit_resource_type' );
226 + $resource_id = $request->get_param( 'convertkit_resource_id' );
227 +
228 + // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings).
229 + $result = $output_restrict_content->verify_spam_protection( $request->get_param( 'spam_protection_response' ) );
230 +
231 + // If spam protection failed, build the email form view with the error message.
232 + if ( is_wp_error( $result ) ) {
233 + $output_restrict_content->error = $result;
234 +
235 + ob_start();
236 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/' . ( $request->get_param( 'display_heading' ) ? 'login-modal-content-email.php' : 'login-email.php' );
237 + $output = trim( ob_get_clean() );
238 + return rest_ensure_response(
239 + array(
240 + 'success' => false,
241 + 'data' => $output,
242 + )
243 + );
244 + }
245 +
246 + // Run subscriber authentication.
247 + $result = $output_restrict_content->subscriber_authentication_send_code(
248 + $email,
249 + $post_id
250 + );
251 +
252 + // If an error occurred, build the email form view with the error message.
253 + if ( is_wp_error( $result ) ) {
254 + // Set error to display on screen.
255 + $output_restrict_content->error = $result;
256 +
257 + // Build email form view to return for output with error message.
258 + ob_start();
259 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/' . ( $request->get_param( 'display_heading' ) ? 'login-modal-content-email.php' : 'login-email.php' );
260 + $output = trim( ob_get_clean() );
261 + return rest_ensure_response(
262 + array(
263 + 'success' => false,
264 + 'data' => $output,
265 + )
266 + );
267 + }
268 +
269 + // Set token and Post ID for authentication code view.
270 + $output_restrict_content->token = $result;
271 + $output_restrict_content->post_id = $post_id;
272 +
273 + // Build authentication code view to return for output.
274 + ob_start();
275 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php';
276 + $output = trim( ob_get_clean() );
277 + return rest_ensure_response(
278 + array(
279 + 'success' => true,
280 + 'data' => $output,
281 + )
282 + );
283 + },
284 +
285 + // No authentication required, as this is on the frontend site.
286 + 'permission_callback' => '__return_true',
287 + )
288 + );
289 +
290 + // Register route to run subscriber verification.
291 + register_rest_route(
292 + 'kit/v1',
293 + '/restrict-content/subscriber-verification',
294 + array(
295 + 'methods' => WP_REST_Server::CREATABLE,
296 + 'args' => array(
297 + // Post ID: Validate post ID is an integer if included in the request.
298 + 'convertkit_post_id' => array(
299 + 'required' => false,
300 + 'validate_callback' => function ( $param ) {
301 +
302 + return is_numeric( $param );
303 +
304 + },
305 + 'sanitize_callback' => 'absint',
306 + ),
307 +
308 + // Token: Validate token is included in the request and is a string.
309 + 'token' => array(
310 + 'required' => true,
311 + 'validate_callback' => function ( $param ) {
312 +
313 + return is_string( $param );
314 +
315 + },
316 + 'sanitize_callback' => 'sanitize_text_field',
317 + ),
318 +
319 + // Subscriber Code: Validate subscriber code is included in the request and is a string.
320 + 'subscriber_code' => array(
321 + 'required' => true,
322 + 'validate_callback' => function ( $param ) {
323 +
324 + return is_string( $param );
325 +
326 + },
327 + 'sanitize_callback' => 'sanitize_text_field',
328 + ),
329 + ),
330 + 'callback' => function ( $request ) {
331 +
332 + // Initialize classes that will be used.
333 + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' );
334 + $output_restrict_content->initialize_classes();
335 +
336 + // Fetch Post ID, Resource Type and Resource ID for the view.
337 + $post_id = $request->get_param( 'convertkit_post_id' );
338 + $token = $request->get_param( 'token' );
339 + $subscriber_code = $request->get_param( 'subscriber_code' );
340 +
341 + // Run subscriber authentication.
342 + $result = $output_restrict_content->subscriber_authentication_verify( $post_id, $token, $subscriber_code );
343 +
344 + // If an error occurred, build the code form view with the error message.
345 + if ( is_wp_error( $result ) ) {
346 + // Set error to display on screen.
347 + $output_restrict_content->error = $result;
348 +
349 + // Set token and post ID for authentication code view.
350 + $output_restrict_content->token = $token;
351 + $output_restrict_content->post_id = $post_id;
352 +
353 + // Build code form view to return for output with error message.
354 + ob_start();
355 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php';
356 + $output = trim( ob_get_clean() );
357 + return rest_ensure_response(
358 + array(
359 + 'success' => false,
360 + 'data' => $output,
361 + )
362 + );
363 + }
364 +
365 + // Return success with the URL to the Post, including the `ck-cache-bust` parameter.
366 + return rest_ensure_response(
367 + array(
368 + 'success' => true,
369 + 'url' => $output_restrict_content->get_url( $post_id, true ),
370 + )
371 + );
372 + },
373 +
374 + // No authentication required, as this is on the frontend site.
375 + 'permission_callback' => '__return_true',
376 + )
377 + );
378 + }
379 +
380 + /**
381 + * Initialize classes that will be used.
122 382 *
383 + * @since 3.1.0
384 + */
385 + public function initialize_classes() {
386 +
387 + $this->settings = new ConvertKit_Settings();
388 + $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content();
389 + $this->api = new ConvertKit_API_V4(
390 + CONVERTKIT_OAUTH_CLIENT_ID,
391 + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
392 + $this->settings->get_access_token(),
393 + $this->settings->get_refresh_token(),
394 + $this->settings->debug_enabled(),
395 + 'restrict_content'
396 + );
397 +
398 + }
399 +
400 + /**
401 + * If the user isn't using JavaScript, or the Plugin's Disable JS is enabled, checks if the request is a Restrict Content request with an email address.
402 + * If so, calls the API depending on the Restrict Content resource that's required:
403 + * - tag: subscribes the email address to the tag, and calls the API to send the subscriber a magic link by email containing a code.
404 + * - form + product: calls the API to send the subscriber a magic link by email containing a code.
405 + *
406 + * See maybe_run_subscriber_verification() for logic once they click the link in the email or enter the code on screen.
407 + *
123 408 * @since 2.1.0
124 409 */
125 410 public function maybe_run_subscriber_authentication() {
126 411
127 - // Bail if no nonce was specified.
412 + // Bail if no nonce was specified via form submission.
128 413 if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) {
129 414 return;
130 415 }
131 416
132 - // Bail if the nonce failed validation.
417 + // Bail if the request is a form submission and the nonce failed validation.
133 418 if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) {
134 - $this->error = new WP_Error( 'convertkit_output_restrict_content_error', __( 'Invalid nonce specified. Please try again.', 'convertkit' ) );
135 419 return;
136 420 }
137 421
138 - // If the Plugin API keys have not been configured, we can't get this subscriber's ID by email.
139 - if ( ! $this->settings->has_api_key_and_secret() ) {
422 + // Bail if the expected email or Post ID are missing from the request.
423 + if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) {
140 424 return;
141 425 }
426 + if ( ! array_key_exists( 'convertkit_post_id', $_REQUEST ) ) {
427 + return;
428 + }
142 429
143 - // Initialize the API.
144 - $this->api = new ConvertKit_API( $this->settings->get_api_key(), $this->settings->get_api_secret(), $this->settings->debug_enabled() );
430 + // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email.
431 + if ( ! $this->settings->has_access_and_refresh_token() ) {
432 + return;
433 + }
145 434
146 - // Send email to subscriber with a link to authenticate they have access to the email address submitted.
147 - $result = $this->api->subscriber_authentication_send_code(
148 - sanitize_text_field( $_REQUEST['convertkit_email'] ),
149 - $this->get_url()
150 - );
435 + // Sanitize inputs.
436 + $email = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_email'] ) );
437 + $this->resource_type = ( array_key_exists( 'convertkit_resource_type', $_REQUEST ) ? sanitize_text_field( wp_unslash( $_REQUEST['convertkit_resource_type'] ) ) : '' );
438 + $this->resource_id = ( array_key_exists( 'convertkit_resource_id', $_REQUEST ) ? absint( $_REQUEST['convertkit_resource_id'] ) : 0 );
439 + $this->post_id = absint( $_REQUEST['convertkit_post_id'] );
151 440
152 - // Bail if an error occured.
441 + // If Restrict Content is by tag, tag the subscriber.
442 + if ( $this->resource_type === 'tag' ) {
443 + // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings).
444 + $spam_protection = new ConvertKit_Spam_Protection();
445 + $spam_check = $spam_protection->verify( 'convertkit_restrict_content_tag' );
446 +
447 + // Bail if spam protection failed.
448 + if ( is_wp_error( $spam_check ) ) {
449 + $this->error = $spam_check;
450 + return;
451 + }
452 +
453 + // Tag subscriber.
454 + $result = $this->api->tag_subscribe( $this->resource_id, $email );
455 +
456 + // Bail if an error occurred.
457 + if ( is_wp_error( $result ) ) {
458 + $this->error = $result;
459 + return;
460 + }
461 + } else {
462 + // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings).
463 + $spam_check = $this->verify_spam_protection();
464 +
465 + // Bail if spam protection failed.
466 + if ( is_wp_error( $spam_check ) ) {
467 + $this->error = $spam_check;
468 + return;
469 + }
470 + }
471 +
472 + // Run subscriber authentication.
473 + $result = $this->subscriber_authentication_send_code( $email, $this->post_id );
474 +
475 + // Bail if an error occurred.
153 476 if ( is_wp_error( $result ) ) {
154 477 $this->error = $result;
155 478 return;
156 479 }
157 480
158 - // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email.
159 - $subscriber = new ConvertKit_Subscriber();
160 - $subscriber->forget();
161 -
162 481 // Store the token so it's included in the subscriber code form.
163 482 $this->token = $result;
164 483
165 - // Show a message telling the subscriber to check their email and click the link in the email.
166 - $this->success = $this->restrict_content_settings->get_by_key( 'email_check_text' );
167 -
168 484 }
169 485
170 486 /**
171 - * Checks if the request contains a token and subscriber_code i.e. the subscriber clicked
172 - * the link in the email sent by the maybe_run_subscriber_authentication() function above.
487 + * If the user isn't using JavaScript, or the Plugin's Disable JS is enabled, checks if the request contains a token and subscriber_code,
488 + * which happens when the subscriber either:
489 + * - clicked the link in the email sent by run_subscriber_authentication(), or
490 + * - entered the code from the email on the screen
173 491 *
174 492 * This calls the API to verify the token and subscriber code, which tells us that the email
175 493 * address supplied truly belongs to the user, and that we can safely trust their subscriber ID
176 494 * to be valid.
@@ -178,9 +496,8 @@
178 496 * @since 2.1.0
179 497 */
180 498 public function maybe_run_subscriber_verification() {
181 499
182 - // phpcs:disable WordPress.Security.NonceVerification.Recommended
183 500 // Bail if the expected token and subscriber code is missing.
184 501 if ( ! array_key_exists( 'token', $_REQUEST ) ) {
185 502 return;
186 503 }
@@ -187,60 +504,261 @@
187 504 if ( ! array_key_exists( 'subscriber_code', $_REQUEST ) ) {
188 505 return;
189 506 }
190 507
191 - // If the Plugin API keys have not been configured, we can't get this subscriber's ID by email.
192 - if ( ! $this->settings->has_api_key_and_secret() ) {
508 + // If a nonce was specified, validate it now.
509 + // It won't be provided if clicking the link in the magic link email.
510 + if ( array_key_exists( '_wpnonce', $_REQUEST ) && ! is_null( $_REQUEST['_wpnonce'] ) ) {
511 + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_subscriber_code' ) ) {
512 + return;
513 + }
514 + }
515 +
516 + // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email.
517 + if ( ! $this->settings->has_access_and_refresh_token() ) {
193 518 return;
194 519 }
195 520
196 521 // Store the token so it's included in the subscriber code form if verification fails.
197 - $this->token = sanitize_text_field( $_REQUEST['token'] );
522 + $this->token = sanitize_text_field( wp_unslash( $_REQUEST['token'] ) );
198 523
199 - // Initialize the API.
200 - $this->api = new ConvertKit_API( $this->settings->get_api_key(), $this->settings->get_api_secret(), $this->settings->debug_enabled() );
524 + // Store the post ID if this is an AJAX request.
525 + // This won't be included if clicking the link in the magic link email, so fall back to using
526 + // get_the_ID() to get the post ID.
527 + if ( array_key_exists( 'convertkit_post_id', $_REQUEST ) ) {
528 + $this->post_id = absint( wp_unslash( $_REQUEST['convertkit_post_id'] ) );
529 + } else {
530 + $this->post_id = get_the_ID();
531 + }
201 532
202 - // Verify the token and subscriber code.
203 - $subscriber_id = $this->api->subscriber_authentication_verify(
204 - sanitize_text_field( $_REQUEST['token'] ),
205 - sanitize_text_field( $_REQUEST['subscriber_code'] )
206 - );
207 - // phpcs:enable
533 + // Run subscriber verification.
534 + $subscriber_id = $this->subscriber_authentication_verify( $this->post_id, sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ), sanitize_text_field( wp_unslash( $_REQUEST['subscriber_code'] ) ) );
208 535
209 - // Bail if an error occured.
536 + // Bail if an error occurred.
210 537 if ( is_wp_error( $subscriber_id ) ) {
211 538 $this->error = $subscriber_id;
212 539 return;
213 540 }
214 541
215 - // Store subscriber ID in cookie.
216 - // We don't need to use validate_and_store_subscriber_id() as we just validated the subscriber via authentication above.
542 + // Redirect now to reload the Post.
543 + $this->redirect( $this->post_id );
544 +
545 + }
546 +
547 + /**
548 + * Logs the subscriber out by deleting their subscriber ID cookie, when the
549 + * log out button is clicked in the Member Content Login block.
550 + *
551 + * @since 3.4.2
552 + */
553 + public function maybe_run_subscriber_logout() {
554 +
555 + // Bail if no logout request was made.
556 + if ( ! array_key_exists( 'convertkit_logout', $_REQUEST ) ) {
557 + return;
558 + }
559 +
560 + // Bail if no nonce was specified.
561 + if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) {
562 + return;
563 + }
564 +
565 + // Bail if the nonce failed validation.
566 + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_member_content_logout' ) ) {
567 + return;
568 + }
569 +
570 + // Delete the subscriber ID cookie.
217 571 $subscriber = new ConvertKit_Subscriber();
218 - $subscriber->set( $subscriber_id );
572 + $subscriber->forget();
219 573
220 - // We append a query parameter to the URL to prevent caching plugins and
221 - // aggressive cache hosting configurations from serving a cached page, which would
222 - // result in maybe_restrict_content() not showing an error message or permitting
223 - // access to the content.
224 - $url = add_query_arg(
574 + // Reload the Post, so the login form displays.
575 + wp_safe_redirect( $this->get_url( get_the_ID(), true ) );
576 + exit();
577 +
578 + }
579 +
580 + /**
581 + * Verifies the spam protection response for the login form, using the spam
582 + * protection provider enabled in the Plugin's settings.
583 + *
584 + * @since 3.4.2
585 + *
586 + * @param bool|string $response Spam protection response, if supplied by a REST API request.
587 + * @return bool|WP_Error
588 + */
589 + public function verify_spam_protection( $response = false ) {
590 +
591 + $spam_protection = new ConvertKit_Spam_Protection();
592 + $provider = $spam_protection->get_active_provider();
593 +
594 + // Return true if no spam protection provider is enabled.
595 + if ( $provider === false ) {
596 + return true;
597 + }
598 +
599 + // Verify the response included in the REST API request.
600 + if ( ! empty( $response ) ) {
601 + return $provider->verify( $response, 'convertkit_member_content_login' );
602 + }
603 +
604 + // Verify the response included in the form submission.
605 + return $spam_protection->verify( 'convertkit_member_content_login' );
606 +
607 + }
608 +
609 + /**
610 + * Enqueues the CSS and JS required by the login form and modal.
611 + *
612 + * @since 3.4.2
613 + */
614 + public function enqueue_scripts_and_styles() {
615 +
616 + // Only load styles if the Disable CSS option is off.
617 + if ( ! $this->settings->css_disabled() ) {
618 + convertkit_enqueue_frontend_css();
619 + }
620 +
621 + // Bail if scripts are disabled.
622 + if ( $this->settings->scripts_disabled() ) {
623 + return;
624 + }
625 +
626 + // Enqueue scripts.
627 + convertkit_enqueue_frontend_js();
628 +
629 + // Define variables.
630 + wp_localize_script(
631 + 'convertkit-js',
632 + 'convertkit_restrict_content',
225 633 array(
226 - 'ck-cache-bust' => microtime(),
227 - ),
228 - $this->get_url()
634 + 'nonce' => wp_create_nonce( 'wp_rest' ),
635 + 'subscriber_authentication_url' => rest_url( 'kit/v1/restrict-content/subscriber-authentication' ),
636 + 'subscriber_verification_url' => rest_url( 'kit/v1/restrict-content/subscriber-verification' ),
637 + 'debug' => $this->settings->debug_enabled(),
638 + )
229 639 );
230 640
231 - // Redirect to the Post without the token and subscriber parameters.
232 - // This will then run maybe_restrict_content() to get the subscriber's ID from the cookie,
233 - // and determine if the content can be displayed.
234 - wp_safe_redirect( $url );
235 - exit;
641 + }
236 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 +
237 670 }
238 671
239 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 + /**
240 758 * Displays (or hides) content on a singular Page, Post or Custom Post Type's Content,
241 759 * depending on whether the visitor is an authenticated ConvertKit subscriber and has
242 - * subscribed to the ConvertKit Product.
760 + * subscribed to the ConvertKit Product or Tag.
243 761 *
244 762 * @since 2.1.0
245 763 *
246 764 * @param string $content Post Content.
@@ -252,29 +770,42 @@
252 770 if ( ! $this->is_restricted_content() ) {
253 771 return $content;
254 772 }
255 773
256 - // Get resource type (Product) that the visitor must be subscribed against to access this content.
257 - $resource_type = $this->get_resource_type( $this->post_id );
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 + }
258 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 +
259 785 // Return the Post Content, unedited, if the Resource Type is false.
260 - if ( ! $resource_type ) {
786 + if ( ! $this->resource_type ) {
261 787 return $content;
262 788 }
263 789
264 - // Get resource ID (Product ID) that the visitor must be subscribed against to access this content.
265 - $resource_id = $this->get_resource_id( $this->post_id );
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();
266 792
267 793 // Return the full Post Content, unedited, if the Resource ID is false, as this means
268 794 // no restrict content setting has been defined for this Post.
269 - if ( ! $resource_id ) {
795 + if ( ! $this->resource_id ) {
270 796 return $content;
271 797 }
272 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 +
273 804 // Return if this request is after the user entered their email address,
274 805 // which means we're going through the authentication flow.
275 806 if ( $this->in_authentication_flow() ) {
276 - return $this->restrict_content( $content, $resource_type, $resource_id );
807 + return $this->restrict_content( $content );
277 808 }
278 809
279 810 // Get the subscriber ID, either from the request or an existing cookie.
280 811 $subscriber_id = $this->get_subscriber_id_from_request();
@@ -280,21 +811,37 @@
280 811 $subscriber_id = $this->get_subscriber_id_from_request();
281 812
282 813 // If no subscriber ID exists, the visitor cannot view the content.
283 814 if ( ! $subscriber_id ) {
284 - return $this->restrict_content( $content, $resource_type, $resource_id );
815 + return $this->restrict_content( $content );
285 816 }
286 817
287 818 // If the subscriber is not subscribed to the product, restrict the content.
288 - if ( ! $this->subscriber_has_access( $subscriber_id, $resource_type, $resource_id ) ) {
819 + if ( ! $this->subscriber_has_access( $subscriber_id ) ) {
289 820 // Show an error before the call to action, to tell the subscriber why they still cannot
290 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.
291 838 $this->error = new WP_Error(
292 839 'convertkit_restrict_content_subscriber_no_access',
293 - esc_html( $this->restrict_content_settings->get_by_key( 'no_access_text' ) )
840 + esc_html( $message )
294 841 );
295 842
296 - return $this->restrict_content( $content, $resource_type, $resource_id );
843 + return $this->restrict_content( $content );
297 844 }
298 845
299 846 // If here, the subscriber has subscribed to the product.
300 847 // Show the full Post Content.
@@ -333,9 +880,9 @@
333 880 // Build replacement where statement.
334 881 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order < ' . $post->menu_order;
335 882
336 883 // Replace existing where statement with new statement.
337 - $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND' ) );
884 + $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
338 885
339 886 // Return.
340 887 return $where;
341 888
@@ -372,9 +919,9 @@
372 919 // Build replacement where statement.
373 920 $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order > ' . $post->menu_order;
374 921
375 922 // Replace existing where statement with new statement.
376 - $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND' ) );
923 + $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) );
377 924
378 925 // Return.
379 926 return $where;
380 927
@@ -412,19 +959,72 @@
412 959
413 960 }
414 961
415 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 + /**
416 998 * Returns the URL for the current request, excluding any query parameters.
417 999 *
418 1000 * @since 2.1.0
419 1001 *
420 - * @return string URL.
1002 + * @param int $post_id Post ID.
1003 + * @param bool $cache_bust Include `ck-cache-bust` parameter in URL.
1004 + * @return string URL.
421 1005 */
422 - private function get_url() {
1006 + public function get_url( $post_id, $cache_bust = false ) {
423 1007
424 - $url = wp_parse_url( get_site_url() . $_SERVER['REQUEST_URI'] );
425 - return $url['scheme'] . '://' . $url['host'] . $url['path'];
1008 + // Get URL of Post.
1009 + $url = get_permalink( $post_id );
426 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 +
427 1027 }
428 1028
429 1029 /**
430 1030 * Determines if the request is for a WordPress Page that has the Restrict Content
@@ -440,12 +1040,12 @@
440 1040 if ( ! is_singular() ) {
441 1041 return false;
442 1042 }
443 1043
444 - // If a Post ID is already defined in this class, this check has already been performed,
445 - // and the Post's settings class has been initialized.
446 - if ( $this->post_id ) {
447 - return true;
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;
448 1048 }
449 1049
450 1050 // Get Post ID.
451 1051 $this->post_id = get_the_ID();
@@ -452,14 +1052,8 @@
452 1052
453 1053 // Initialize Settings and Post Setting classes.
454 1054 $this->post_settings = new ConvertKit_Post( $this->post_id );
455 1055
456 - // If the Plugin API keys have not been configured, we can't determine the validity of this subscriber ID
457 - // or which resource(s) they have access to.
458 - if ( ! $this->settings->has_api_key_and_secret() ) {
459 - return false;
460 - }
461 -
462 1056 // Return whether the Post's settings are set to restrict content.
463 1057 return $this->post_settings->restrict_content_enabled();
464 1058
465 1059 }
@@ -509,13 +1103,15 @@
509 1103 * Get the Post's Restricted Content resource type.
510 1104 *
511 1105 * @since 2.1.0
512 1106 *
513 - * @param int $post_id Post ID.
514 1107 * @return bool|string Resource Type (product).
515 1108 */
516 - private function get_resource_type( $post_id ) {
1109 + private function get_resource_type() {
517 1110
1111 + // Initialize Post Setting classes.
1112 + $this->post_settings = new ConvertKit_Post( $this->post_id );
1113 +
518 1114 // Get resource type.
519 1115 $resource_type = $this->post_settings->get_restrict_content_type();
520 1116
521 1117 /**
@@ -528,9 +1124,9 @@
528 1124 *
529 1125 * @param string $resource_type Resource Type (product)
530 1126 * @param int $post_id Post ID
531 1127 */
532 - $resource_type = apply_filters( 'convertkit_output_restrict_content_get_resource_type', $resource_type, $post_id );
1128 + $resource_type = apply_filters( 'convertkit_output_restrict_content_get_resource_type', $resource_type, $this->post_id );
533 1129
534 1130 // If resource type is blank, set it to false.
535 1131 if ( empty( $resource_type ) ) {
536 1132 $resource_type = false;
@@ -545,13 +1141,15 @@
545 1141 * Get the Post's Restricted Content resource ID.
546 1142 *
547 1143 * @since 2.1.0
548 1144 *
549 - * @param int $post_id Post ID.
550 1145 * @return int Resource ID (product ID).
551 1146 */
552 - private function get_resource_id( $post_id ) {
1147 + private function get_resource_id() {
553 1148
1149 + // Initialize Post Setting classes.
1150 + $this->post_settings = new ConvertKit_Post( $this->post_id );
1151 +
554 1152 // Get resource ID.
555 1153 $resource_id = $this->post_settings->get_restrict_content_id();
556 1154
557 1155 /**
@@ -564,9 +1162,9 @@
564 1162 *
565 1163 * @param int $resource_id Resource ID
566 1164 * @param int $post_id Post ID
567 1165 */
568 - $resource_id = apply_filters( 'convertkit_output_restrict_content_get_resource_id', $resource_id, $post_id );
1166 + $resource_id = apply_filters( 'convertkit_output_restrict_content_get_resource_id', $resource_id, $this->post_id );
569 1167
570 1168 // Return.
571 1169 return $resource_id;
572 1170
@@ -572,50 +1170,87 @@
572 1170
573 1171 }
574 1172
575 1173 /**
576 - * Determines if the given subscriber has an active subscription to
577 - * the given resource and its ID.
1174 + * Queries the API to confirm whether the resource exists.
578 1175 *
579 - * @since 2.1.0
1176 + * @since 2.3.3
580 1177 *
581 - * @param string|int $subscriber_id Signed Subscriber ID or Subscriber ID.
582 - * @param string $resource_type Resource Type (product).
583 - * @param int $resource_id Resource ID (Product ID).
584 - * @return bool Can view restricted content
1178 + * @return bool
585 1179 */
586 - private function subscriber_has_access( $subscriber_id, $resource_type, $resource_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1180 + private function resource_exists() {
587 1181
588 - // Initialize the API.
589 - $this->api = new ConvertKit_API( $this->settings->get_api_key(), $this->settings->get_api_secret(), $this->settings->debug_enabled() );
1182 + switch ( $this->resource_type ) {
590 1183
591 - // Depending on the resource type, determine if the subscriber has access to it.
592 - // This is deliberately a switch statement, because we will likely add in support
593 - // for restrict by tag and form later.
594 - switch ( $resource_type ) {
595 1184 case 'product':
596 - // Get products that the subscriber has access to.
597 - $result = $this->api->profile( $subscriber_id );
1185 + // Get Product.
1186 + $products = new ConvertKit_Resource_Products( 'restrict_content' );
1187 + $product = $products->get_by_id( $this->resource_id );
598 1188
599 - // If an error occured, the subscriber ID is invalid.
600 - if ( is_wp_error( $result ) ) {
1189 + // If the Product does not exist, return false.
1190 + if ( ! $product ) {
601 1191 return false;
602 1192 }
603 1193
604 - // If no products exist, there's no access.
605 - if ( ! $result['products'] || ! count( $result['products'] ) ) {
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 ) {
606 1204 return false;
607 1205 }
608 1206
609 - // Return if the subscriber is not subscribed to the product.
610 - if ( ! in_array( absint( $resource_id ), $result['products'], true ) ) {
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 ) {
611 1217 return false;
612 1218 }
613 1219
614 - // If here, the subscriber is subscribed to the product.
1220 + // Tag exists in ConvertKit.
615 1221 return true;
1222 +
1223 + default:
1224 + return false;
1225 +
616 1226 }
617 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 +
618 1253 // If here, the subscriber does not have access.
619 1254 return false;
620 1255
621 1256 }
@@ -620,8 +1255,98 @@
620 1255
621 1256 }
622 1257
623 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 + /**
624 1349 * Gets the subscriber ID from the request (either the cookie or the URL).
625 1350 *
626 1351 * @since 2.1.0
627 1352 *
@@ -632,9 +1357,9 @@
632 1357 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
633 1358 $subscriber = new ConvertKit_Subscriber();
634 1359 $subscriber_id = $subscriber->get_subscriber_id();
635 1360
636 - // If an error occured, the subscriber ID in the request/cookie is not a valid subscriber.
1361 + // If an error occurred, the subscriber ID in the request/cookie is not a valid subscriber.
637 1362 if ( is_wp_error( $subscriber_id ) ) {
638 1363 return 0;
639 1364 }
640 1365
@@ -648,16 +1373,73 @@
648 1373 *
649 1374 * @since 2.1.0
650 1375 *
651 1376 * @param string $content Post Content.
652 - * @param string $resource_type Resource Type (product).
653 - * @param int $resource_id Resource ID (Product ID).
654 - * @return string Post Content preview with call to action
1377 + * @return string Post Content preview with call to action
655 1378 */
656 - private function restrict_content( $content, $resource_type, $resource_id ) {
1379 + private function restrict_content( $content ) {
657 1380
658 - return $this->get_content_preview( $content ) . $this->get_call_to_action( $this->post_id, $resource_type, $resource_id );
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 + }
659 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 +
660 1442 }
661 1443
662 1444 /**
663 1445 * Returns a preview of the given content for visitors that don't have access to restricted content.
@@ -665,12 +1447,12 @@
665 1447 * The preview is determined by:
666 1448 * - A single <!--more--> tag being placed between WordPress paragraphs when using the Classic Editor.
667 1449 * Content before the tag will be returned as the preview, unless 'noteaser' is enabled.
668 1450 * - A single 'Read More' block being placed between WordPress blocks when using the Gutenberg Editor.
669 - * Content before the Read More block will be returned as the preview, unless 'Hide th excerpt
1451 + * Content before the Read More block will be returned as the preview, unless 'Hide the excerpt
670 1452 * on the full content page' is enabled.
671 1453 *
672 - * No preview content is returned if the above conditions are not met.
1454 + * If no more tag or Read More block is present, returns the Post's excerpt.
673 1455 *
674 1456 * @since 2.1.0
675 1457 *
676 1458 * @param string $content Post Content.
@@ -701,48 +1483,74 @@
701 1483 // If noteaser is enabled, this will correctly be blank.
702 1484 return $content_breakdown[0];
703 1485 }
704 1486
705 - // If here, there is no preview content available. Don't return any content.
706 - return '';
1487 + // If here, there is no preview content available. Use the Post's excerpt.
1488 + return $this->get_excerpt( $post->ID );
707 1489
708 1490 }
709 1491
710 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 + /**
711 1520 * Returns the HTML output for the call to action for visitors not subscribed to the required
712 1521 * resource type and ID.
713 1522 *
714 1523 * @since 2.1.0
715 1524 *
716 - * @param int $post_id Post ID.
717 - * @param string $resource_type Resource Type (product).
718 - * @param int $resource_id Resource ID (Product ID).
1525 + * @param int $post_id Post ID.
719 1526 * @return string HTML
720 1527 */
721 - private function get_call_to_action( $post_id, $resource_type, $resource_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1528 + private function get_call_to_action( $post_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
722 1529
723 - // Read success and error notices from this class.
724 - $success = $this->success;
725 - $error = $this->error;
1530 + // Enqueue CSS and JS.
1531 + $this->enqueue_scripts_and_styles();
726 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 +
727 1545 // This is deliberately a switch statement, because we will likely add in support
728 1546 // for restrict by tag and form later.
729 1547 switch ( $resource_type ) {
730 1548 case 'product':
731 - // Only load styles if the Disable CSS option is off.
732 - if ( ! $this->settings->css_disabled() ) {
733 - // Enqueue styles.
734 - wp_enqueue_style( 'convertkit-restrict-content', CONVERTKIT_PLUGIN_URL . 'resources/frontend/css/restrict-content.css', array(), CONVERTKIT_PLUGIN_VERSION );
735 - }
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' );
736 1552
737 - // Output product code form if this request is after the user entered their email address,
738 - // which means we're going through the authentication flow.
739 - if ( $this->in_authentication_flow() ) { // phpcs:ignore WordPress.Security.NonceVerification
740 - ob_start();
741 - include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/product-code.php';
742 - return trim( ob_get_clean() );
743 - }
744 -
745 1553 // Output product restricted message and email form.
746 1554 // Get Product.
747 1555 $products = new ConvertKit_Resource_Products( 'restrict_content' );
748 1556 $product = $products->get_by_id( $resource_id );
@@ -752,18 +1560,639 @@
752 1560 if ( $url ) {
753 1561 wp_enqueue_script( 'convertkit-commerce', $url, array(), CONVERTKIT_PLUGIN_VERSION, true );
754 1562 }
755 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 +
756 1570 // Output.
757 1571 ob_start();
758 - $button = $products->get_html( $resource_id, $this->restrict_content_settings->get_by_key( 'subscribe_button_label' ) );
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 + );
759 1579 include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/product.php';
760 1580 return trim( ob_get_clean() );
761 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 +
762 1621 default:
763 1622 return '';
764 1623
765 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;
766 2195
767 2196 }
768 2197
769 2198 }