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 +216 -150 3.3.43.4.3 View file →
@@ -98,8 +98,17 @@
98 98 */
99 99 public $token = false;
100 100
101 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 + /**
102 111 * Constructor. Registers actions and filters to possibly limit output of a Page/Post/CPT's
103 112 * content on the frontend site.
104 113 *
105 114 * @since 2.1.0
@@ -108,8 +117,9 @@
108 117
109 118 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
110 119 add_action( 'init', array( $this, 'initialize_classes' ), 2 );
111 120 add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 );
121 + add_action( 'wp', array( $this, 'maybe_run_subscriber_logout' ), 3 );
112 122 add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 );
113 123 add_action( 'wp', array( $this, 'register_content_filter' ), 5 );
114 124 add_filter( 'get_previous_post_where', array( $this, 'maybe_change_previous_post_where_clause' ), 10, 5 );
115 125 add_filter( 'get_next_post_where', array( $this, 'maybe_change_next_post_where_clause' ), 10, 5 );
@@ -154,11 +164,12 @@
154 164 },
155 165 'sanitize_callback' => 'absint',
156 166 ),
157 167
158 - // Resource Type: Validate resource type is included in the request and is a string.
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.
159 170 'convertkit_resource_type' => array(
160 - 'required' => true,
171 + 'required' => false,
161 172 'validate_callback' => function ( $param ) {
162 173
163 174 return is_string( $param );
164 175
@@ -165,11 +176,12 @@
165 176 },
166 177 'sanitize_callback' => 'sanitize_text_field',
167 178 ),
168 179
169 - // Resource ID: Validate resource ID is included in the request and is an integer.
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.
170 182 'convertkit_resource_id' => array(
171 - 'required' => true,
183 + 'required' => false,
172 184 'validate_callback' => function ( $param ) {
173 185
174 186 return is_numeric( $param );
175 187
@@ -175,8 +187,32 @@
175 187
176 188 },
177 189 'sanitize_callback' => 'absint',
178 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 + ),
179 215 ),
180 216 'callback' => function ( $request ) {
181 217
182 218 // Initialize classes that will be used.
@@ -188,8 +224,26 @@
188 224 $post_id = $request->get_param( 'convertkit_post_id' );
189 225 $resource_type = $request->get_param( 'convertkit_resource_type' );
190 226 $resource_id = $request->get_param( 'convertkit_resource_id' );
191 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 +
192 246 // Run subscriber authentication.
193 247 $result = $output_restrict_content->subscriber_authentication_send_code(
194 248 $email,
195 249 $post_id
@@ -201,9 +255,9 @@
201 255 $output_restrict_content->error = $result;
202 256
203 257 // Build email form view to return for output with error message.
204 258 ob_start();
205 - include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-email.php';
259 + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/' . ( $request->get_param( 'display_heading' ) ? 'login-modal-content-email.php' : 'login-email.php' );
206 260 $output = trim( ob_get_clean() );
207 261 return rest_ensure_response(
208 262 array(
209 263 'success' => false,
@@ -344,14 +398,14 @@
344 398 }
345 399
346 400 /**
347 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.
348 - * Also runs if restrict content by tag and require login is disabled, as we immediately tag and redirect if this is the case.
349 402 * If so, calls the API depending on the Restrict Content resource that's required:
350 - * - tag: subscribes the email address to the tag, storing the subscriber ID in a cookie and redirecting
351 - * - product: calls the API to send the subscriber a magic link by email containing a code. See maybe_run_subscriber_verification()
352 - * for logic once they click the link in the email or enter the code on screen.
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.
353 405 *
406 + * See maybe_run_subscriber_verification() for logic once they click the link in the email or enter the code on screen.
407 + *
354 408 * @since 2.1.0
355 409 */
356 410 public function maybe_run_subscriber_authentication() {
357 411
@@ -364,18 +418,12 @@
364 418 if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) {
365 419 return;
366 420 }
367 421
368 - // Bail if the expected email, resource type, resource ID or Post ID are missing from the request.
422 + // Bail if the expected email or Post ID are missing from the request.
369 423 if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) {
370 424 return;
371 425 }
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 426 if ( ! array_key_exists( 'convertkit_post_id', $_REQUEST ) ) {
379 427 return;
380 428 }
381 429
@@ -385,24 +433,21 @@
385 433 }
386 434
387 435 // Sanitize inputs.
388 436 $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'] );
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 );
391 439 $this->post_id = absint( $_REQUEST['convertkit_post_id'] );
392 440
393 441 // If Restrict Content is by tag, tag the subscriber.
394 442 if ( $this->resource_type === 'tag' ) {
395 - // Check reCAPTCHA.
396 - $recaptcha = new ConvertKit_Recaptcha();
397 - $recaptcha_response = $recaptcha->verify_recaptcha(
398 - ( isset( $_POST['g-recaptcha-response'] ) ? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) ) : '' ),
399 - 'convertkit_restrict_content_tag'
400 - );
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' );
401 446
402 - // Bail if reCAPTCHA failed.
403 - if ( is_wp_error( $recaptcha_response ) ) {
404 - $this->error = $recaptcha_response;
447 + // Bail if spam protection failed.
448 + if ( is_wp_error( $spam_check ) ) {
449 + $this->error = $spam_check;
405 450 return;
406 451 }
407 452
408 453 // Tag subscriber.
@@ -412,28 +457,19 @@
412 457 if ( is_wp_error( $result ) ) {
413 458 $this->error = $result;
414 459 return;
415 460 }
461 + } else {
462 + // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings).
463 + $spam_check = $this->verify_spam_protection();
416 464
417 - // If require login is disabled, return now.
418 - if ( ! $this->restrict_content_settings->require_tag_login() ) {
419 - // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email.
420 - $subscriber = new ConvertKit_Subscriber();
421 - $subscriber->forget();
422 -
423 - // Fetch the subscriber ID from the result.
424 - $subscriber_id = $result['subscriber']['id'];
425 -
426 - // Store subscriber ID in cookie.
427 - $this->store_subscriber_id_in_cookie( $subscriber_id );
428 -
429 - // Redirect.
430 - $this->redirect( $this->post_id );
465 + // Bail if spam protection failed.
466 + if ( is_wp_error( $spam_check ) ) {
467 + $this->error = $spam_check;
431 468 return;
432 469 }
433 470 }
434 471
435 - // If here, require login is enabled for tags or this is a product/form.
436 472 // Run subscriber authentication.
437 473 $result = $this->subscriber_authentication_send_code( $email, $this->post_id );
438 474
439 475 // Bail if an error occurred.
@@ -508,8 +544,133 @@
508 544
509 545 }
510 546
511 547 /**
548 + * Logs the subscriber out by deleting their subscriber ID cookie, when the
549 + * log out button is clicked in the Member Content Login block.
550 + *
551 + * @since 3.4.2
552 + */
553 + public function maybe_run_subscriber_logout() {
554 +
555 + // Bail if no logout request was made.
556 + if ( ! array_key_exists( 'convertkit_logout', $_REQUEST ) ) {
557 + return;
558 + }
559 +
560 + // Bail if no nonce was specified.
561 + if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) {
562 + return;
563 + }
564 +
565 + // Bail if the nonce failed validation.
566 + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_member_content_logout' ) ) {
567 + return;
568 + }
569 +
570 + // Delete the subscriber ID cookie.
571 + $subscriber = new ConvertKit_Subscriber();
572 + $subscriber->forget();
573 +
574 + // Reload the Post, so the login form displays.
575 + wp_safe_redirect( $this->get_url( get_the_ID(), true ) );
576 + exit();
577 +
578 + }
579 +
580 + /**
581 + * Verifies the spam protection response for the login form, using the spam
582 + * protection provider enabled in the Plugin's settings.
583 + *
584 + * @since 3.4.2
585 + *
586 + * @param bool|string $response Spam protection response, if supplied by a REST API request.
587 + * @return bool|WP_Error
588 + */
589 + public function verify_spam_protection( $response = false ) {
590 +
591 + $spam_protection = new ConvertKit_Spam_Protection();
592 + $provider = $spam_protection->get_active_provider();
593 +
594 + // Return true if no spam protection provider is enabled.
595 + if ( $provider === false ) {
596 + return true;
597 + }
598 +
599 + // Verify the response included in the REST API request.
600 + if ( ! empty( $response ) ) {
601 + return $provider->verify( $response, 'convertkit_member_content_login' );
602 + }
603 +
604 + // Verify the response included in the form submission.
605 + return $spam_protection->verify( 'convertkit_member_content_login' );
606 +
607 + }
608 +
609 + /**
610 + * Enqueues the CSS and JS required by the login form and modal.
611 + *
612 + * @since 3.4.2
613 + */
614 + public function enqueue_scripts_and_styles() {
615 +
616 + // Only load styles if the Disable CSS option is off.
617 + if ( ! $this->settings->css_disabled() ) {
618 + convertkit_enqueue_frontend_css();
619 + }
620 +
621 + // Bail if scripts are disabled.
622 + if ( $this->settings->scripts_disabled() ) {
623 + return;
624 + }
625 +
626 + // Enqueue scripts.
627 + convertkit_enqueue_frontend_js();
628 +
629 + // Define variables.
630 + wp_localize_script(
631 + 'convertkit-js',
632 + 'convertkit_restrict_content',
633 + array(
634 + 'nonce' => wp_create_nonce( 'wp_rest' ),
635 + 'subscriber_authentication_url' => rest_url( 'kit/v1/restrict-content/subscriber-authentication' ),
636 + 'subscriber_verification_url' => rest_url( 'kit/v1/restrict-content/subscriber-verification' ),
637 + 'debug' => $this->settings->debug_enabled(),
638 + )
639 + );
640 +
641 + }
642 +
643 + /**
644 + * Outputs the login modal in the footer, ensuring it is only output once
645 + * when a Post contains multiple Member Content Login blocks.
646 + *
647 + * @since 3.4.2
648 + *
649 + * @param int $post_id Post ID.
650 + * @param bool|int $resource_id Resource ID.
651 + * @param bool|string $resource_type Resource Type.
652 + */
653 + public function output_login_modal( $post_id, $resource_id = 0, $resource_type = '' ) {
654 +
655 + if ( $this->login_modal_output ) {
656 + return;
657 + }
658 +
659 + $this->login_modal_output = true;
660 +
661 + add_action(
662 + 'wp_footer',
663 + function () use ( $post_id, $resource_id, $resource_type ) {
664 +
665 + include_once CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal.php';
666 +
667 + }
668 + );
669 +
670 + }
671 +
672 + /**
512 673 * Sends an email to the subscriber with a code and link to authenticate they have access to the email address submitted.
513 674 *
514 675 * @since 3.1.0
515 676 *
@@ -1076,34 +1237,16 @@
1076 1237 * @return bool Can view restricted content
1077 1238 */
1078 1239 private function subscriber_has_access( $subscriber_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1079 1240
1080 - // Depending on the resource type, determine if the subscriber has access to it.
1081 - // This is deliberately a switch statement, because we will likely add in support
1082 - // for restrict by tag and form later.
1083 1241 switch ( $this->resource_type ) {
1084 1242 case 'product':
1085 - // For products, the subscriber ID has to be a signed subscriber ID string.
1086 1243 return $this->subscriber_has_access_to_product_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1087 1244
1088 1245 case 'form':
1089 - // For forms, the subscriber ID has to be a signed subscriber ID string.
1090 1246 return $this->subscriber_has_access_to_form_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1091 1247
1092 1248 case 'tag':
1093 - // If the subscriber ID is numeric, check using get_subscriber_tags().
1094 - if ( is_numeric( $subscriber_id ) ) {
1095 - // If require login is enabled, only a signed subscriber ID is accepted, as this is generated
1096 - // via the subscriber verify email flow.
1097 - if ( $this->restrict_content_settings->require_tag_login() ) {
1098 - return false;
1099 - }
1100 -
1101 - return $this->subscriber_has_access_to_tag_by_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1102 - }
1103 -
1104 - // The subscriber ID is a signed subscriber ID string.
1105 - // Check using profile().
1106 1249 return $this->subscriber_has_access_to_tag_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
1107 1250
1108 1251 }
1109 1252
@@ -1202,46 +1345,8 @@
1202 1345
1203 1346 }
1204 1347
1205 1348 /**
1206 - * Determines if the given signed subscriber ID has an active subscription to
1207 - * the given tag.
1208 - *
1209 - * @since 2.7.1
1210 - *
1211 - * @param int $subscriber_id Subscriber ID.
1212 - * @param int $tag_id Tag ID.
1213 - * @return bool Has access to tag
1214 - */
1215 - private function subscriber_has_access_to_tag_by_subscriber_id( $subscriber_id, $tag_id ) {
1216 -
1217 - // Get tags that the subscriber has been assigned.
1218 - $tags = $this->api->get_subscriber_tags( $subscriber_id );
1219 -
1220 - // If an error occurred, the subscriber ID is invalid.
1221 - if ( is_wp_error( $tags ) ) {
1222 - return false;
1223 - }
1224 -
1225 - // If no tags exist, there's no access.
1226 - if ( ! count( $tags['tags'] ) ) {
1227 - return false;
1228 - }
1229 -
1230 - // Iterate through the subscriber's tags to see if they have the required tag.
1231 - foreach ( $tags['tags'] as $tag ) {
1232 - if ( $tag['id'] === $tag_id ) {
1233 - // Subscriber has the required tag assigned to them - grant access.
1234 - return true;
1235 - }
1236 - }
1237 -
1238 - // If here, the subscriber does not have the tag.
1239 - return false;
1240 -
1241 - }
1242 -
1243 - /**
1244 1349 * Gets the subscriber ID from the request (either the cookie or the URL).
1245 1350 *
1246 1351 * @since 2.1.0
1247 1352 *
@@ -1421,32 +1526,11 @@
1421 1526 * @return string HTML
1422 1527 */
1423 1528 private function get_call_to_action( $post_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1424 1529
1425 - // Only load styles if the Disable CSS option is off.
1426 - if ( ! $this->settings->css_disabled() ) {
1427 - // Enqueue styles.
1428 - convertkit_enqueue_frontend_css();
1429 - }
1530 + // Enqueue CSS and JS.
1531 + $this->enqueue_scripts_and_styles();
1430 1532
1431 - // Only load scripts if the Disable Scripts option is off.
1432 - if ( ! $this->settings->scripts_disabled() ) {
1433 - // Enqueue scripts.
1434 - convertkit_enqueue_frontend_js();
1435 -
1436 - // Define variables.
1437 - wp_localize_script(
1438 - 'convertkit-js',
1439 - 'convertkit_restrict_content',
1440 - array(
1441 - 'nonce' => wp_create_nonce( 'wp_rest' ),
1442 - 'subscriber_authentication_url' => rest_url( 'kit/v1/restrict-content/subscriber-authentication' ),
1443 - 'subscriber_verification_url' => rest_url( 'kit/v1/restrict-content/subscriber-verification' ),
1444 - 'debug' => $this->settings->debug_enabled(),
1445 - )
1446 - );
1447 - }
1448 -
1449 1533 // Output code form if this request is after the user entered their email address,
1450 1534 // which means we're going through the authentication flow.
1451 1535 if ( $this->in_authentication_flow() ) {
1452 1536 ob_start();
@@ -1479,16 +1563,9 @@
1479 1563
1480 1564 // If scripts are enabled, output the email login form in a modal, which will be displayed
1481 1565 // when the 'log in' link is clicked.
1482 1566 if ( ! $this->settings->scripts_disabled() ) {
1483 - add_action(
1484 - 'wp_footer',
1485 - function () use ( $post_id, $resource_id, $resource_type ) {
1486 -
1487 - include_once CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal.php';
1488 -
1489 - }
1490 - );
1567 + $this->output_login_modal( $post_id, $resource_id, $resource_type );
1491 1568 }
1492 1569
1493 1570 // Output.
1494 1571 ob_start();
@@ -1509,16 +1586,9 @@
1509 1586
1510 1587 // If scripts are enabled, output the email login form in a modal, which will be displayed
1511 1588 // when the 'log in' link is clicked.
1512 1589 if ( ! $this->settings->scripts_disabled() ) {
1513 - add_action(
1514 - 'wp_footer',
1515 - function () use ( $post_id, $resource_id, $resource_type ) {
1516 -
1517 - include_once CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal.php';
1518 -
1519 - }
1520 - );
1590 + $this->output_login_modal( $post_id, $resource_id, $resource_type );
1521 1591 }
1522 1592
1523 1593 // Output.
1524 1594 ob_start();
@@ -1529,24 +1599,20 @@
1529 1599 // Get header and text from settings for Tags.
1530 1600 $heading = $this->restrict_content_settings->get_by_key( 'subscribe_heading_tag' );
1531 1601 $text = $this->restrict_content_settings->get_by_key( 'subscribe_text_tag' );
1532 1602
1533 - // If require login is enabled and scripts are enabled, output the email login form in a modal, which will be displayed
1603 + // If scripts are enabled, output the email login form in a modal, which will be displayed
1534 1604 // when the 'log in' link is clicked.
1535 - if ( $this->restrict_content_settings->require_tag_login() && ! $this->settings->scripts_disabled() ) {
1536 - add_action(
1537 - 'wp_footer',
1538 - function () use ( $post_id, $resource_id, $resource_type ) {
1605 + if ( ! $this->settings->scripts_disabled() ) {
1606 + $this->output_login_modal( $post_id, $resource_id, $resource_type );
1607 + }
1539 1608
1540 - include_once CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal.php';
1541 -
1542 - }
1543 - );
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();
1544 1614 }
1545 -
1546 - // Enqueue Google reCAPTCHA JS.
1547 - $recaptcha = new ConvertKit_Recaptcha();
1548 - $recaptcha->enqueue_scripts();
1549 1615
1550 1616 // Output.
1551 1617 ob_start();
1552 1618 include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/tag.php';