From 14644c51a662bf14c3f1e92c5a819ed76455721e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Nov 2025 19:28:58 +0800 Subject: [PATCH 01/22] Member Content: Use REST API --- includes/class-convertkit-ajax.php | 81 ------------------ ...ass-convertkit-output-restrict-content.php | 82 ++++++++++++++++++- resources/frontend/js/restrict-content.js | 12 ++- 3 files changed, 86 insertions(+), 89 deletions(-) diff --git a/includes/class-convertkit-ajax.php b/includes/class-convertkit-ajax.php index a6981f439..a00a03337 100644 --- a/includes/class-convertkit-ajax.php +++ b/includes/class-convertkit-ajax.php @@ -26,12 +26,6 @@ public function __construct() { add_action( 'wp_ajax_nopriv_convertkit_store_subscriber_email_as_id_in_cookie', array( $this, 'store_subscriber_email_as_id_in_cookie' ) ); add_action( 'wp_ajax_convertkit_store_subscriber_email_as_id_in_cookie', array( $this, 'store_subscriber_email_as_id_in_cookie' ) ); - add_action( 'wp_ajax_nopriv_convertkit_subscriber_authentication_send_code', array( $this, 'subscriber_authentication_send_code' ) ); - add_action( 'wp_ajax_convertkit_subscriber_authentication_send_code', array( $this, 'subscriber_authentication_send_code' ) ); - - add_action( 'wp_ajax_nopriv_convertkit_subscriber_verification', array( $this, 'subscriber_verification' ) ); - add_action( 'wp_ajax_convertkit_subscriber_verification', array( $this, 'subscriber_verification' ) ); - } /** @@ -124,79 +118,4 @@ public function store_subscriber_email_as_id_in_cookie() { } - /** - * Calls the API to send the subscriber a magic link by email containing a code when - * the modal version of Restrict Content is used, and the user has submitted their email address. - * - * Returns a view of either: - * - an error message and email input i.e. the user entered an invalid email address, - * - the code input, which is then displayed in the modal for the user to enter the code sent by email. - * - * See maybe_run_subscriber_verification() for logic once they enter the code on screen. - * - * @since 2.3.8 - */ - public function subscriber_authentication_send_code() { - - // Load Restrict Content class. - $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); - - // Run subscriber authentication. - $output_restrict_content->maybe_run_subscriber_authentication(); - - // Fetch Post ID, Resource Type and Resource ID for the view. - $post_id = $output_restrict_content->post_id; - $resource_type = $output_restrict_content->resource_type; - $resource_id = $output_restrict_content->resource_id; - - // If an error occured, build the email form view with the error message. - if ( is_wp_error( $output_restrict_content->error ) ) { - ob_start(); - include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-email.php'; - $output = trim( ob_get_clean() ); - wp_send_json_success( $output ); - } - - // Build authentication code view to return for output. - ob_start(); - include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; - $output = trim( ob_get_clean() ); - wp_send_json_success( $output ); - - } - - /** - * Calls the API to verify the token and entered subscriber code, which tells us that the email - * address supplied truly belongs to the user, and that we can safely trust their subscriber ID - * to be valid. - * - * @since 2.3.8 - */ - public function subscriber_verification() { - - // Load Restrict Content class. - $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); - - // Run subscriber authentication. - $output_restrict_content->maybe_run_subscriber_verification(); - - // Fetch Post ID, Resource Type and Resource ID for the view. - $post_id = $output_restrict_content->post_id; - $resource_type = $output_restrict_content->resource_type; - $resource_id = $output_restrict_content->resource_id; - - // If an error occured, build the code form view with the error message. - if ( is_wp_error( $output_restrict_content->error ) ) { - ob_start(); - include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; - $output = trim( ob_get_clean() ); - wp_send_json_error( $output ); - } - - // Return success with the URL to the Post, including the `ck-cache-bust` parameter. - // JS will load the given URL to show the restricted content. - wp_send_json_success( $output_restrict_content->get_url( true ) ); - - } - } diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index c66817893..cb7b66064 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -117,6 +117,7 @@ public function __construct() { return; } + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 ); add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 ); add_action( 'wp', array( $this, 'register_content_filter' ), 5 ); @@ -127,6 +128,84 @@ public function __construct() { } + /** + * Register REST API routes. + * + * @since 3.1.0 + */ + public function register_routes() { + + // Register route to run subscriber authentication. + register_rest_route( + 'kit/v1', + '/restrict-content/subscriber-authentication', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => function() { + // Load Restrict Content class. + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); + + // Run subscriber authentication. + $output_restrict_content->maybe_run_subscriber_authentication(); + + // Fetch Post ID, Resource Type and Resource ID for the view. + $post_id = $output_restrict_content->post_id; + $resource_type = $output_restrict_content->resource_type; + $resource_id = $output_restrict_content->resource_id; + + // If an error occured, build the email form view with the error message. + if ( is_wp_error( $output_restrict_content->error ) ) { + ob_start(); + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-email.php'; + $output = trim( ob_get_clean() ); + return rest_ensure_response( $output ); + } + + // Build authentication code view to return for output. + ob_start(); + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; + $output = trim( ob_get_clean() ); + return rest_ensure_response( $output ); + }, + 'permission_callback' => '__return_true', + ) + ); + + // Register route to run subscriber verification. + register_rest_route( + 'kit/v1', + '/restrict-content/subscriber-verification', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => function() { + // Load Restrict Content class. + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); + + // Run subscriber authentication. + $output_restrict_content->maybe_run_subscriber_verification(); + + // Fetch Post ID, Resource Type and Resource ID for the view. + $post_id = $output_restrict_content->post_id; + $resource_type = $output_restrict_content->resource_type; + $resource_id = $output_restrict_content->resource_id; + + // If an error occured, build the code form view with the error message. + if ( is_wp_error( $output_restrict_content->error ) ) { + ob_start(); + include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; + $output = trim( ob_get_clean() ); + wp_send_json_error( $output ); + } + + // Return success with the URL to the Post, including the `ck-cache-bust` parameter. + // JS will load the given URL to show the restricted content. + wp_send_json_success( $output_restrict_content->get_url( true ) ); + }, + 'permission_callback' => '__return_true', + ) + ); + } + /** * Checks if the request is a Restrict Content request with an email address. * If so, calls the API depending on the Restrict Content resource that's required: @@ -1214,7 +1293,8 @@ private function get_call_to_action( $post_id ) { // phpcs:ignore Generic.CodeAn 'convertkit-restrict-content', 'convertkit_restrict_content', array( - 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'subscriber_authentication_url' => rest_url( 'kit/v1/restrict-content/subscriber-authentication' ), + 'subscriber_verification_url' => rest_url( 'kit/v1/restrict-content/subscriber-verification' ), 'debug' => $this->settings->debug_enabled(), ) ); diff --git a/resources/frontend/js/restrict-content.js b/resources/frontend/js/restrict-content.js index 5063629f3..c58e542ea 100644 --- a/resources/frontend/js/restrict-content.js +++ b/resources/frontend/js/restrict-content.js @@ -147,13 +147,12 @@ function convertKitRestrictContentSubscriberAuthenticationSendCode( resource_id, post_id ) { - fetch(convertkit_restrict_content.ajaxurl, { + fetch(convertkit_restrict_content.subscriber_authentication_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ - action: 'convertkit_subscriber_authentication_send_code', _wpnonce: nonce, convertkit_email: email, convertkit_resource_type: resource_type, @@ -176,7 +175,7 @@ function convertKitRestrictContentSubscriberAuthenticationSendCode( // Output response, which will be a form with/without an error message. document.querySelector( '#convertkit-restrict-content-modal-content' - ).innerHTML = result.data; + ).innerHTML = result; // Hide loading overlay. document.querySelector( @@ -212,13 +211,12 @@ function convertKitRestrictContentSubscriberVerification( token, post_id ) { - fetch(convertkit_restrict_content.ajaxurl, { + fetch(convertkit_restrict_content.subscriber_verification_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ - action: 'convertkit_subscriber_verification', _wpnonce: nonce, subscriber_code, token, @@ -241,7 +239,7 @@ function convertKitRestrictContentSubscriberVerification( if (!result.success) { document.querySelector( '#convertkit-restrict-content-modal-content' - ).innerHTML = result.data; + ).innerHTML = result; // Hide loading overlay. document.querySelector( @@ -254,7 +252,7 @@ function convertKitRestrictContentSubscriberVerification( } // Code entered is valid; load the URL in the response data. - window.location = result.data; + window.location = result; }) .catch(function (error) { if (convertkit_restrict_content.debug) { From ef4f036662aebd0a2167e7096f4494637ca4161e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 12 Nov 2025 16:38:22 +0800 Subject: [PATCH 02/22] Update logic for REST API replacement --- ...ass-convertkit-output-restrict-content.php | 91 ++++++++++++++----- resources/frontend/js/restrict-content.js | 27 ++++-- 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index cb7b66064..0b187916a 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -110,14 +110,16 @@ public function __construct() { $this->settings = new ConvertKit_Settings(); $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content(); - // Don't register any hooks if this is an AJAX request, otherwise + // Register REST API routes. + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + + // Don't register any hooks if this is a WP REST API request, otherwise // maybe_run_subscriber_authentication() and maybe_run_subscriber_verification() will run - // twice in an AJAX request (once here, and once when called by the ConvertKit_AJAX class). - if ( wp_doing_ajax() ) { + // twice in a WP REST API request (once here, and once when called by WP REST API). + if ( $this->is_rest_request() || array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { return; } - add_action( 'rest_api_init', array( $this, 'register_routes' ) ); add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 ); add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 ); add_action( 'wp', array( $this, 'register_content_filter' ), 5 ); @@ -141,7 +143,7 @@ public function register_routes() { '/restrict-content/subscriber-authentication', array( 'methods' => WP_REST_Server::CREATABLE, - 'callback' => function() { + 'callback' => function () { // Load Restrict Content class. $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); @@ -158,14 +160,24 @@ public function register_routes() { ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-email.php'; $output = trim( ob_get_clean() ); - return rest_ensure_response( $output ); + return rest_ensure_response( + array( + 'success' => false, + 'data' => $output, + ) + ); } // Build authentication code view to return for output. ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; $output = trim( ob_get_clean() ); - return rest_ensure_response( $output ); + return rest_ensure_response( + array( + 'success' => true, + 'data' => $output, + ) + ); }, 'permission_callback' => '__return_true', ) @@ -177,7 +189,7 @@ public function register_routes() { '/restrict-content/subscriber-verification', array( 'methods' => WP_REST_Server::CREATABLE, - 'callback' => function() { + 'callback' => function () { // Load Restrict Content class. $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); @@ -194,12 +206,21 @@ public function register_routes() { ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; $output = trim( ob_get_clean() ); - wp_send_json_error( $output ); + return rest_ensure_response( + array( + 'success' => false, + 'data' => $output, + ) + ); } // Return success with the URL to the Post, including the `ck-cache-bust` parameter. - // JS will load the given URL to show the restricted content. - wp_send_json_success( $output_restrict_content->get_url( true ) ); + return rest_ensure_response( + array( + 'success' => true, + 'url' => $output_restrict_content->get_url( false ), + ) + ); }, 'permission_callback' => '__return_true', ) @@ -217,14 +238,23 @@ public function register_routes() { */ public function maybe_run_subscriber_authentication() { - // Bail if no nonce was specified. - if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) { + // Bail if no nonce was specified via form submission or the WP REST API. + if ( ! array_key_exists( '_wpnonce', $_REQUEST ) && ! array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { return; } - // Bail if the nonce failed validation. - if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) { - return; + // Bail if the request is a form submission and the nonce failed validation. + if ( array_key_exists( '_wpnonce', $_REQUEST ) ) { + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) { + return; + } + } + + // Bail if the request is a WP REST API request and the nonce failed validation. + if ( array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { + if ( ! wp_verify_nonce( sanitize_key( $_SERVER['HTTP_X_WP_NONCE'] ), 'wp_rest' ) ) { + return; + } } // Bail if the expected email, resource ID or Post ID are missing. @@ -290,7 +320,7 @@ public function maybe_run_subscriber_authentication() { // If require login is enabled, show the login screen. if ( $this->restrict_content_settings->require_tag_login() ) { // Tag the subscriber, unless this is an AJAX request. - if ( ! wp_doing_ajax() ) { + if ( ! $this->is_rest_request() ) { $result = $this->api->tag_subscribe( $this->resource_id, $email ); // Bail if an error occured. @@ -356,7 +386,7 @@ public function maybe_run_subscriber_authentication() { $this->store_subscriber_id_in_cookie( $subscriber_id ); // If this isn't an AJAX request, redirect now to reload the Post. - if ( ! wp_doing_ajax() ) { + if ( ! $this->is_rest_request() ) { $this->redirect(); } break; @@ -392,6 +422,11 @@ public function maybe_run_subscriber_verification() { return; } } + if ( array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { + if ( ! wp_verify_nonce( sanitize_key( $_SERVER['HTTP_X_WP_NONCE'] ), 'wp_rest' ) ) { + return; + } + } // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email. if ( ! $this->settings->has_access_and_refresh_token() ) { @@ -436,7 +471,7 @@ public function maybe_run_subscriber_verification() { $this->store_subscriber_id_in_cookie( $subscriber_id ); // If this isn't an AJAX request, redirect now to reload the Post. - if ( ! wp_doing_ajax() ) { + if ( ! $this->is_rest_request() ) { $this->redirect(); } @@ -1293,9 +1328,10 @@ private function get_call_to_action( $post_id ) { // phpcs:ignore Generic.CodeAn 'convertkit-restrict-content', 'convertkit_restrict_content', array( + 'nonce' => wp_create_nonce( 'wp_rest' ), 'subscriber_authentication_url' => rest_url( 'kit/v1/restrict-content/subscriber-authentication' ), - 'subscriber_verification_url' => rest_url( 'kit/v1/restrict-content/subscriber-verification' ), - 'debug' => $this->settings->debug_enabled(), + 'subscriber_verification_url' => rest_url( 'kit/v1/restrict-content/subscriber-verification' ), + 'debug' => $this->settings->debug_enabled(), ) ); @@ -1414,6 +1450,19 @@ function () use ( $post_id, $resource_id, $resource_type ) { } + /** + * Determines if the request is a WordPress REST API request. + * + * @since 3.1.0 + * + * @return bool + */ + private function is_rest_request() { + + return defined( 'REST_REQUEST' ) && REST_REQUEST; + + } + /** * Whether this request is from a search engine crawler. * diff --git a/resources/frontend/js/restrict-content.js b/resources/frontend/js/restrict-content.js index c58e542ea..ae5299df9 100644 --- a/resources/frontend/js/restrict-content.js +++ b/resources/frontend/js/restrict-content.js @@ -77,7 +77,7 @@ function convertKitRestrictContentFormSubmit(e) { if (isCodeSubmission) { // Code submission. convertKitRestrictContentSubscriberVerification( - e.target.querySelector('input[name="_wpnonce"]').value, + convertkit_restrict_content.nonce, e.target.querySelector('input[name="subscriber_code"]').value, e.target.querySelector('input[name="token"]').value, e.target.querySelector('input[name="convertkit_post_id"]').value @@ -88,7 +88,7 @@ function convertKitRestrictContentFormSubmit(e) { // Email submission. convertKitRestrictContentSubscriberAuthenticationSendCode( - e.target.querySelector('input[name="_wpnonce"]').value, + convertkit_restrict_content.nonce, e.target.querySelector('input[name="convertkit_email"]').value, e.target.querySelector('input[name="convertkit_resource_type"]').value, e.target.querySelector('input[name="convertkit_resource_id"]').value, @@ -151,9 +151,9 @@ function convertKitRestrictContentSubscriberAuthenticationSendCode( method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', + 'X-WP-Nonce': nonce, }, body: new URLSearchParams({ - _wpnonce: nonce, convertkit_email: email, convertkit_resource_type: resource_type, convertkit_resource_id: resource_id, @@ -172,10 +172,17 @@ function convertKitRestrictContentSubscriberAuthenticationSendCode( console.log(result); } - // Output response, which will be a form with/without an error message. - document.querySelector( - '#convertkit-restrict-content-modal-content' - ).innerHTML = result; + // Output error message if the response contains a code. + if (typeof result.code !== 'undefined') { + document.querySelector( + '#convertkit-restrict-content-modal-content' + ).innerHTML = result.message; + } else { + // Output response, which will be a form with/without an error message. + document.querySelector( + '#convertkit-restrict-content-modal-content' + ).innerHTML = result.data; + } // Hide loading overlay. document.querySelector( @@ -215,9 +222,9 @@ function convertKitRestrictContentSubscriberVerification( method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', + 'X-WP-Nonce': nonce, }, body: new URLSearchParams({ - _wpnonce: nonce, subscriber_code, token, convertkit_post_id: post_id, @@ -239,7 +246,7 @@ function convertKitRestrictContentSubscriberVerification( if (!result.success) { document.querySelector( '#convertkit-restrict-content-modal-content' - ).innerHTML = result; + ).innerHTML = result.data; // Hide loading overlay. document.querySelector( @@ -252,7 +259,7 @@ function convertKitRestrictContentSubscriberVerification( } // Code entered is valid; load the URL in the response data. - window.location = result; + window.location = result.url; }) .catch(function (error) { if (convertkit_restrict_content.debug) { From ee2dcdd91269462c83b233ab8a3f25009a3d7507 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 16:34:47 +0800 Subject: [PATCH 03/22] Started tests --- tests/Integration/RESTAPITest.php | 106 ++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index 6d30cbc72..1f465eb14 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -301,6 +301,112 @@ public function testRefreshResourcesRestrictContent() $this->assertArrayHasKeys( $data['products'][0], [ 'id', 'name', 'url', 'published' ] ); } + public function testRestrictContentSubscriberAuthenticationForm() + { + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); + $request->set_body_params([ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'form', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'convertkit_post_id' => 1, + ]); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertTrue( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + + public function testRestrictContentSubscriberAuthenticationFormInvalidEmail() + { + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + //$request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); + $request->set_body_params([ + 'convertkit_email' => 'fail@kit.com', + 'convertkit_resource_type' => 'form', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'convertkit_post_id' => 1, + ]); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + //$this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + var_dump( $data ); + die(); + $this->assertIsArray( $data ); + $this->assertTrue( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + + public function testRestrictContentSubscriberAuthenticationTag() + { + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); + $request->set_body_params([ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'tag', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_TAG_ID'], + 'convertkit_post_id' => 1, + ]); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertTrue( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + + public function testRestrictContentSubscriberAuthenticationProduct() + { + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); + $request->set_body_params([ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'product', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_PRODUCT_ID'], + 'convertkit_post_id' => 1, + ]); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertTrue( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + /** * Act as an editor user. * From c7c7896429a5d6e71bcfe7c3748aa8374488cd7d Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 16:52:32 +0800 Subject: [PATCH 04/22] Start refactor of JS and non-JS methods --- ...ass-convertkit-output-restrict-content.php | 146 +++++++++++------- 1 file changed, 86 insertions(+), 60 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 0b187916a..352f431cc 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -143,17 +143,24 @@ public function register_routes() { '/restrict-content/subscriber-authentication', array( 'methods' => WP_REST_Server::CREATABLE, - 'callback' => function () { + 'callback' => function ( $request ) { + + // Fetch Post ID, Resource Type and Resource ID for the view. + $email = $request->get_param('convertkit_email'); + $post_id = $request->get_param('convertkit_post_id'); + $resource_type = $request->get_param('convertkit_resource_type'); + $resource_id = $request->get_param('convertkit_resource_id'); + // Load Restrict Content class. $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); // Run subscriber authentication. - $output_restrict_content->maybe_run_subscriber_authentication(); - - // Fetch Post ID, Resource Type and Resource ID for the view. - $post_id = $output_restrict_content->post_id; - $resource_type = $output_restrict_content->resource_type; - $resource_id = $output_restrict_content->resource_id; + $output_restrict_content->run_subscriber_authentication( + $email, + $post_id, + $resource_type, + $resource_id + ); // If an error occured, build the email form view with the error message. if ( is_wp_error( $output_restrict_content->error ) ) { @@ -218,7 +225,7 @@ public function register_routes() { return rest_ensure_response( array( 'success' => true, - 'url' => $output_restrict_content->get_url( false ), + 'url' => $output_restrict_content->get_url( $post_id, false ), ) ); }, @@ -228,7 +235,7 @@ public function register_routes() { } /** - * Checks if the request is a Restrict Content request with an email address. + * Checks if the request is a Restrict Content request with an email address and the user isn't using JS. * If so, calls the API depending on the Restrict Content resource that's required: * - tag: subscribes the email address to the tag, storing the subscriber ID in a cookie and redirecting * - product: calls the API to send the subscriber a magic link by email containing a code. See maybe_run_subscriber_verification() @@ -238,25 +245,16 @@ public function register_routes() { */ public function maybe_run_subscriber_authentication() { - // Bail if no nonce was specified via form submission or the WP REST API. - if ( ! array_key_exists( '_wpnonce', $_REQUEST ) && ! array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { + // Bail if no nonce was specified via form submission. + if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) { return; } // Bail if the request is a form submission and the nonce failed validation. - if ( array_key_exists( '_wpnonce', $_REQUEST ) ) { - if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) { - return; - } - } - - // Bail if the request is a WP REST API request and the nonce failed validation. - if ( array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { - if ( ! wp_verify_nonce( sanitize_key( $_SERVER['HTTP_X_WP_NONCE'] ), 'wp_rest' ) ) { - return; - } + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) { + return; } - + // Bail if the expected email, resource ID or Post ID are missing. if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) { return; @@ -276,6 +274,12 @@ public function maybe_run_subscriber_authentication() { return; } + // Sanitize inputs. + $email = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_email'] ) ); + $this->resource_type = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_resource_type'] ) ); + $this->resource_id = absint( $_REQUEST['convertkit_resource_id'] ); + $this->post_id = absint( $_REQUEST['convertkit_post_id'] ); + // Initialize the API. $this->api = new ConvertKit_API_V4( CONVERTKIT_OAUTH_CLIENT_ID, @@ -286,69 +290,84 @@ public function maybe_run_subscriber_authentication() { 'restrict_content' ); - // Sanitize inputs. - $email = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_email'] ) ); - $this->resource_type = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_resource_type'] ) ); - $this->resource_id = absint( $_REQUEST['convertkit_resource_id'] ); - $this->post_id = absint( $_REQUEST['convertkit_post_id'] ); + // Run subscriber authentication. + $result = $this->run_subscriber_authentication( $email, $this->post_id, $this->resource_type, $this->resource_id ); + + // Bail if an error occured. + if ( is_wp_error( $result ) ) { + $this->error = $result; + return; + } + + // Store the token so it's included in the subscriber code form. + $this->token = $result; + + } + + /** + * Runs subscriber authentication / subscription depending on the resource type. + * + * @since 3.1.0 + * + * @param string $email Email address. + * @param int $post_id Post ID. + * @param string $resource_type Resource type. + * @param int $resource_id Resource ID. + */ + public function run_subscriber_authentication( $email, $post_id, $resource_type, $resource_id ) { // Run subscriber authentication / subscription depending on the resource type. - switch ( $this->resource_type ) { + switch ( $resource_type ) { case 'product': case 'form': // Send email to subscriber with a link to authenticate they have access to the email address submitted. $result = $this->api->subscriber_authentication_send_code( $email, - $this->get_url() + $this->get_url( $post_id ) ); // Bail if an error occured. if ( is_wp_error( $result ) ) { - $this->error = $result; - return; + return $result; } // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. $subscriber = new ConvertKit_Subscriber(); $subscriber->forget(); - // Store the token so it's included in the subscriber code form. - $this->token = $result; - break; + // Return the token. + return $result; case 'tag': // If require login is enabled, show the login screen. if ( $this->restrict_content_settings->require_tag_login() ) { // Tag the subscriber, unless this is an AJAX request. if ( ! $this->is_rest_request() ) { - $result = $this->api->tag_subscribe( $this->resource_id, $email ); + $result = $this->api->tag_subscribe( $resource_id, $email ); // Bail if an error occured. if ( is_wp_error( $result ) ) { - $this->error = $result; - return; + return $result; } } // Send email to subscriber with a link to authenticate they have access to the email address submitted. $result = $this->api->subscriber_authentication_send_code( $email, - $this->get_url() + $this->get_url( $post_id ) ); // Bail if an error occured. if ( is_wp_error( $result ) ) { - $this->error = $result; - return; + return $result; } // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. $subscriber = new ConvertKit_Subscriber(); $subscriber->forget(); - // Store the token so it's included in the subscriber code form. - $this->token = $result; - break; + // Return the token. + return $result; } // If here, require login is disabled. @@ -362,8 +381,7 @@ public function maybe_run_subscriber_authentication() { // Bail if reCAPTCHA failed. if ( is_wp_error( $recaptcha_response ) ) { - $this->error = $recaptcha_response; - return; + return $recaptcha_response; } // Tag the subscriber. @@ -371,8 +389,7 @@ public function maybe_run_subscriber_authentication() { // Bail if an error occured. if ( is_wp_error( $result ) ) { - $this->error = $result; - return; + return $result; } // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. @@ -387,7 +404,7 @@ public function maybe_run_subscriber_authentication() { // If this isn't an AJAX request, redirect now to reload the Post. if ( ! $this->is_rest_request() ) { - $this->redirect(); + $this->redirect( $post_id ); } break; @@ -422,11 +439,6 @@ public function maybe_run_subscriber_verification() { return; } } - if ( array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { - if ( ! wp_verify_nonce( sanitize_key( $_SERVER['HTTP_X_WP_NONCE'] ), 'wp_rest' ) ) { - return; - } - } // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email. if ( ! $this->settings->has_access_and_refresh_token() ) { @@ -477,6 +489,17 @@ public function maybe_run_subscriber_verification() { } + public function run_subscriber_verification( $token, $subscriber_code ) { + // Verify the token and subscriber code. + $subscriber_id = $this->api->subscriber_authentication_verify( + $token, + $subscriber_code + ); + + // Return the subscriber ID. + return $subscriber_id; + } + /** * Registers the applicable content filter for maybe restricting content, depending * on the Theme or Page Builder used. @@ -725,14 +748,16 @@ private function store_subscriber_id_in_cookie( $subscriber_id ) { * a ck-cache-bust query parameter to beat caching plugins. * * @since 2.3.7 + * + * @param int $post_id Post ID. */ - private function redirect() { + private function redirect( $post_id) { // Redirect to the Post, appending a query parameter to the URL to prevent caching plugins and // aggressive cache hosting configurations from serving a cached page, which would // result in maybe_restrict_content() not showing an error message or permitting // access to the content. - wp_safe_redirect( $this->get_url( true ) ); + wp_safe_redirect( $this->get_url( $post_id, true ) ); exit; } @@ -742,13 +767,14 @@ private function redirect() { * * @since 2.1.0 * - * @param bool $cache_bust Include `ck-cache-bust` parameter in URL. - * @return string URL. + * @param int $post_id Post ID. + * @param bool $cache_bust Include `ck-cache-bust` parameter in URL. + * @return string URL. */ - public function get_url( $cache_bust = false ) { + public function get_url( $post_id, $cache_bust = false ) { // Get URL of Post. - $url = get_permalink( $this->post_id ); + $url = get_permalink( $post_id ); // If no cache busting required, return the URL now. if ( ! $cache_bust ) { From 112b354a4f456af6ca00e8ab6b3f73ca0b0352bf Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 17:02:52 +0800 Subject: [PATCH 05/22] WIP --- ...ass-convertkit-output-restrict-content.php | 171 +++++++++--------- 1 file changed, 87 insertions(+), 84 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 352f431cc..030fe9012 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -196,20 +196,21 @@ public function register_routes() { '/restrict-content/subscriber-verification', array( 'methods' => WP_REST_Server::CREATABLE, - 'callback' => function () { + 'callback' => function ( $request ) { + + // Fetch Post ID, Resource Type and Resource ID for the view. + $post_id = $request->get_param('convertkit_post_id'); + $token = $request->get_param('token'); + $subscriber_code = $request->get_param('subscriber_code'); + // Load Restrict Content class. $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); // Run subscriber authentication. - $output_restrict_content->maybe_run_subscriber_verification(); - - // Fetch Post ID, Resource Type and Resource ID for the view. - $post_id = $output_restrict_content->post_id; - $resource_type = $output_restrict_content->resource_type; - $resource_id = $output_restrict_content->resource_id; + $result = $output_restrict_content->run_subscriber_verification( $post_id, $token, $subscriber_code ); // If an error occured, build the code form view with the error message. - if ( is_wp_error( $output_restrict_content->error ) ) { + if ( is_wp_error( $result ) ) { ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; $output = trim( ob_get_clean() ); @@ -225,7 +226,7 @@ public function register_routes() { return rest_ensure_response( array( 'success' => true, - 'url' => $output_restrict_content->get_url( $post_id, false ), + 'url' => $output_restrict_content->get_url( $post_id, false ), /// @TODO Check you have post id. ) ); }, @@ -255,7 +256,7 @@ public function maybe_run_subscriber_authentication() { return; } - // Bail if the expected email, resource ID or Post ID are missing. + // Bail if the expected email, resource type, resource ID or Post ID are missing from the request. if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) { return; } @@ -302,6 +303,68 @@ public function maybe_run_subscriber_authentication() { // Store the token so it's included in the subscriber code form. $this->token = $result; + // Redirect now to reload the Post. + // $this->redirect( $post_id ); + + } + + /** + * Checks if the request contains a token and subscriber_code i.e. the subscriber clicked + * the link in the email sent by the maybe_run_subscriber_authentication() function above. + * + * This calls the API to verify the token and subscriber code, which tells us that the email + * address supplied truly belongs to the user, and that we can safely trust their subscriber ID + * to be valid. + * + * @since 2.1.0 + */ + public function maybe_run_subscriber_verification() { + + // Bail if the expected token and subscriber code is missing. + if ( ! array_key_exists( 'token', $_REQUEST ) ) { + return; + } + if ( ! array_key_exists( 'subscriber_code', $_REQUEST ) ) { + return; + } + + // If a nonce was specified, validate it now. + // It won't be provided if clicking the link in the magic link email. + if ( array_key_exists( '_wpnonce', $_REQUEST ) ) { + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_subscriber_code' ) ) { + return; + } + } + + // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email. + if ( ! $this->settings->has_access_and_refresh_token() ) { + return; + } + + // Store the token so it's included in the subscriber code form if verification fails. + $this->token = sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ); + + // Store the post ID if this is an AJAX request. + // This won't be included if clicking the link in the magic link email, so fall back to using + // get_the_ID() to get the post ID. + if ( array_key_exists( 'convertkit_post_id', $_REQUEST ) ) { + $this->post_id = absint( wp_unslash( $_REQUEST['convertkit_post_id'] ) ); + } else { + $this->post_id = get_the_ID(); + } + + // Run subscriber verification. + $subscriber_id = $this->run_subscriber_verification( $this->post_id, $this->token, $this->subscriber_code ); + + // Bail if an error occured. + if ( is_wp_error( $subscriber_id ) ) { + $this->error = $subscriber_id; + return; + } + + // Redirect now to reload the Post. + $this->redirect(); + } /** @@ -313,6 +376,8 @@ public function maybe_run_subscriber_authentication() { * @param int $post_id Post ID. * @param string $resource_type Resource type. * @param int $resource_id Resource ID. + * + * @return WP_Error|string Error or Token. */ public function run_subscriber_authentication( $email, $post_id, $resource_type, $resource_id ) { @@ -321,14 +386,14 @@ public function run_subscriber_authentication( $email, $post_id, $resource_type, case 'product': case 'form': // Send email to subscriber with a link to authenticate they have access to the email address submitted. - $result = $this->api->subscriber_authentication_send_code( + $token = $this->api->subscriber_authentication_send_code( $email, $this->get_url( $post_id ) ); // Bail if an error occured. - if ( is_wp_error( $result ) ) { - return $result; + if ( is_wp_error( $token ) ) { + return $token; } // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. @@ -336,12 +401,13 @@ public function run_subscriber_authentication( $email, $post_id, $resource_type, $subscriber->forget(); // Return the token. - return $result; + return $token; case 'tag': // If require login is enabled, show the login screen. if ( $this->restrict_content_settings->require_tag_login() ) { // Tag the subscriber, unless this is an AJAX request. + // @TODO Check. if ( ! $this->is_rest_request() ) { $result = $this->api->tag_subscribe( $resource_id, $email ); @@ -352,14 +418,14 @@ public function run_subscriber_authentication( $email, $post_id, $resource_type, } // Send email to subscriber with a link to authenticate they have access to the email address submitted. - $result = $this->api->subscriber_authentication_send_code( + $token = $this->api->subscriber_authentication_send_code( $email, $this->get_url( $post_id ) ); // Bail if an error occured. - if ( is_wp_error( $result ) ) { - return $result; + if ( is_wp_error( $token ) ) { + return $token; } // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. @@ -367,7 +433,7 @@ public function run_subscriber_authentication( $email, $post_id, $resource_type, $subscriber->forget(); // Return the token. - return $result; + return $token; } // If here, require login is disabled. @@ -401,61 +467,13 @@ public function run_subscriber_authentication( $email, $post_id, $resource_type, // Store subscriber ID in cookie. $this->store_subscriber_id_in_cookie( $subscriber_id ); - - // If this isn't an AJAX request, redirect now to reload the Post. - if ( ! $this->is_rest_request() ) { - $this->redirect( $post_id ); - } break; } } - /** - * Checks if the request contains a token and subscriber_code i.e. the subscriber clicked - * the link in the email sent by the maybe_run_subscriber_authentication() function above. - * - * This calls the API to verify the token and subscriber code, which tells us that the email - * address supplied truly belongs to the user, and that we can safely trust their subscriber ID - * to be valid. - * - * @since 2.1.0 - */ - public function maybe_run_subscriber_verification() { - - // Bail if the expected token and subscriber code is missing. - if ( ! array_key_exists( 'token', $_REQUEST ) ) { - return; - } - if ( ! array_key_exists( 'subscriber_code', $_REQUEST ) ) { - return; - } - - // If a nonce was specified, validate it now. - // It won't be provided if clicking the link in the magic link email. - if ( array_key_exists( '_wpnonce', $_REQUEST ) ) { - if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_subscriber_code' ) ) { - return; - } - } - - // If the Plugin Access Token has not been configured, we can't get this subscriber's ID by email. - if ( ! $this->settings->has_access_and_refresh_token() ) { - return; - } - - // Store the token so it's included in the subscriber code form if verification fails. - $this->token = sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ); - - // Store the post ID if this is an AJAX request. - // This won't be included if clicking the link in the magic link email, so fall back to using - // get_the_ID() to get the post ID. - if ( array_key_exists( 'convertkit_post_id', $_REQUEST ) ) { - $this->post_id = absint( wp_unslash( $_REQUEST['convertkit_post_id'] ) ); - } else { - $this->post_id = get_the_ID(); - } + public function run_subscriber_verification( $post_id, $token, $subscriber_code ) { // Initialize the API. $this->api = new ConvertKit_API_V4( @@ -475,29 +493,14 @@ public function maybe_run_subscriber_verification() { // Bail if an error occured. if ( is_wp_error( $subscriber_id ) ) { - $this->error = $subscriber_id; - return; + return $subscriber_id; } // Store subscriber ID in cookie. $this->store_subscriber_id_in_cookie( $subscriber_id ); - // If this isn't an AJAX request, redirect now to reload the Post. - if ( ! $this->is_rest_request() ) { - $this->redirect(); - } - - } - - public function run_subscriber_verification( $token, $subscriber_code ) { - // Verify the token and subscriber code. - $subscriber_id = $this->api->subscriber_authentication_verify( - $token, - $subscriber_code - ); - - // Return the subscriber ID. return $subscriber_id; + } /** From 63e1a173b05ee8c5a01ac4b9042776899511b613 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 17:28:55 +0800 Subject: [PATCH 06/22] First pass at working functionality for products --- ...ass-convertkit-output-restrict-content.php | 74 +++++++++++-------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 030fe9012..7c6196180 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -151,11 +151,8 @@ public function register_routes() { $resource_type = $request->get_param('convertkit_resource_type'); $resource_id = $request->get_param('convertkit_resource_id'); - // Load Restrict Content class. - $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); - // Run subscriber authentication. - $output_restrict_content->run_subscriber_authentication( + $result = $this->run_subscriber_authentication( $email, $post_id, $resource_type, @@ -163,7 +160,11 @@ public function register_routes() { ); // If an error occured, build the email form view with the error message. - if ( is_wp_error( $output_restrict_content->error ) ) { + if ( is_wp_error( $result ) ) { + // Set error to display on screen. + $this->error = $result; + + // Build email form view to return for output with error message. ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-email.php'; $output = trim( ob_get_clean() ); @@ -175,6 +176,10 @@ public function register_routes() { ); } + // Set token and Post ID for authentication code view. + $this->token = $result; + $this->post_id = $post_id; + // Build authentication code view to return for output. ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; @@ -203,14 +208,19 @@ public function register_routes() { $token = $request->get_param('token'); $subscriber_code = $request->get_param('subscriber_code'); - // Load Restrict Content class. - $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); - // Run subscriber authentication. - $result = $output_restrict_content->run_subscriber_verification( $post_id, $token, $subscriber_code ); + $result = $this->run_subscriber_verification( $post_id, $token, $subscriber_code ); // If an error occured, build the code form view with the error message. if ( is_wp_error( $result ) ) { + // Set error to display on screen. + $this->error = $result; + + // Set token and post ID for authentication code view. + $this->token = $token; + $this->post_id = $post_id; + + // Build code form view to return for output with error message. ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; $output = trim( ob_get_clean() ); @@ -226,7 +236,7 @@ public function register_routes() { return rest_ensure_response( array( 'success' => true, - 'url' => $output_restrict_content->get_url( $post_id, false ), /// @TODO Check you have post id. + 'url' => $this->get_url( $post_id, false ), // @TODO Check cache busting by enabling. ) ); }, @@ -236,7 +246,7 @@ public function register_routes() { } /** - * Checks if the request is a Restrict Content request with an email address and the user isn't using JS. + * 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. * If so, calls the API depending on the Restrict Content resource that's required: * - tag: subscribes the email address to the tag, storing the subscriber ID in a cookie and redirecting * - product: calls the API to send the subscriber a magic link by email containing a code. See maybe_run_subscriber_verification() @@ -281,16 +291,6 @@ public function maybe_run_subscriber_authentication() { $this->resource_id = absint( $_REQUEST['convertkit_resource_id'] ); $this->post_id = absint( $_REQUEST['convertkit_post_id'] ); - // Initialize the API. - $this->api = new ConvertKit_API_V4( - CONVERTKIT_OAUTH_CLIENT_ID, - CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $this->settings->get_access_token(), - $this->settings->get_refresh_token(), - $this->settings->debug_enabled(), - 'restrict_content' - ); - // Run subscriber authentication. $result = $this->run_subscriber_authentication( $email, $this->post_id, $this->resource_type, $this->resource_id ); @@ -303,14 +303,13 @@ public function maybe_run_subscriber_authentication() { // Store the token so it's included in the subscriber code form. $this->token = $result; - // Redirect now to reload the Post. - // $this->redirect( $post_id ); - } /** - * Checks if the request contains a token and subscriber_code i.e. the subscriber clicked - * the link in the email sent by the maybe_run_subscriber_authentication() function above. + * 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, + * which happens when the subscriber either: + * - clicked the link in the email sent by run_subscriber_authentication(), or + * - entered the code from the email on the screen * * This calls the API to verify the token and subscriber code, which tells us that the email * address supplied truly belongs to the user, and that we can safely trust their subscriber ID @@ -330,7 +329,7 @@ public function maybe_run_subscriber_verification() { // If a nonce was specified, validate it now. // It won't be provided if clicking the link in the magic link email. - if ( array_key_exists( '_wpnonce', $_REQUEST ) ) { + if ( array_key_exists( '_wpnonce', $_REQUEST ) && ! is_null( $_REQUEST['_wpnonce'] ) ) { if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_subscriber_code' ) ) { return; } @@ -354,7 +353,7 @@ public function maybe_run_subscriber_verification() { } // Run subscriber verification. - $subscriber_id = $this->run_subscriber_verification( $this->post_id, $this->token, $this->subscriber_code ); + $subscriber_id = $this->run_subscriber_verification( $this->post_id, sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ), sanitize_text_field( wp_unslash( $_REQUEST['subscriber_code'] ) ) ); // Bail if an error occured. if ( is_wp_error( $subscriber_id ) ) { @@ -363,7 +362,7 @@ public function maybe_run_subscriber_verification() { } // Redirect now to reload the Post. - $this->redirect(); + $this->redirect( $this->post_id ); } @@ -380,6 +379,23 @@ public function maybe_run_subscriber_verification() { * @return WP_Error|string Error or Token. */ public function run_subscriber_authentication( $email, $post_id, $resource_type, $resource_id ) { + + error_log( 'run_subscriber_authentication' ); + + error_log($email); + error_log($post_id); + error_log($resource_type); + error_log($resource_id); + + // Initialize the API. + $this->api = new ConvertKit_API_V4( + CONVERTKIT_OAUTH_CLIENT_ID, + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, + $this->settings->get_access_token(), + $this->settings->get_refresh_token(), + $this->settings->debug_enabled(), + 'restrict_content' + ); // Run subscriber authentication / subscription depending on the resource type. switch ( $resource_type ) { From b3290575a6c2d7f7d6f1cd36b4cde155a11cf367 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 18:18:12 +0800 Subject: [PATCH 07/22] Refactor for gated content by Tag --- ...ass-convertkit-output-restrict-content.php | 203 ++++++++---------- 1 file changed, 86 insertions(+), 117 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 7c6196180..950a66403 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -146,10 +146,10 @@ public function register_routes() { 'callback' => function ( $request ) { // Fetch Post ID, Resource Type and Resource ID for the view. - $email = $request->get_param('convertkit_email'); - $post_id = $request->get_param('convertkit_post_id'); - $resource_type = $request->get_param('convertkit_resource_type'); - $resource_id = $request->get_param('convertkit_resource_id'); + $email = $request->get_param( 'convertkit_email' ); + $post_id = $request->get_param( 'convertkit_post_id' ); + $resource_type = $request->get_param( 'convertkit_resource_type' ); + $resource_id = $request->get_param( 'convertkit_resource_id' ); // Run subscriber authentication. $result = $this->run_subscriber_authentication( @@ -204,9 +204,9 @@ public function register_routes() { 'callback' => function ( $request ) { // Fetch Post ID, Resource Type and Resource ID for the view. - $post_id = $request->get_param('convertkit_post_id'); - $token = $request->get_param('token'); - $subscriber_code = $request->get_param('subscriber_code'); + $post_id = $request->get_param( 'convertkit_post_id' ); + $token = $request->get_param( 'token' ); + $subscriber_code = $request->get_param( 'subscriber_code' ); // Run subscriber authentication. $result = $this->run_subscriber_verification( $post_id, $token, $subscriber_code ); @@ -236,7 +236,7 @@ public function register_routes() { return rest_ensure_response( array( 'success' => true, - 'url' => $this->get_url( $post_id, false ), // @TODO Check cache busting by enabling. + 'url' => $this->get_url( $post_id, true ), ) ); }, @@ -247,6 +247,7 @@ public function register_routes() { /** * 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. + * Also runs if restrict content by tag and require login is disabled, as we immediately tag and redirect if this is the case. * If so, calls the API depending on the Restrict Content resource that's required: * - tag: subscribes the email address to the tag, storing the subscriber ID in a cookie and redirecting * - product: calls the API to send the subscriber a magic link by email containing a code. See maybe_run_subscriber_verification() @@ -265,7 +266,7 @@ public function maybe_run_subscriber_authentication() { if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) { return; } - + // Bail if the expected email, resource type, resource ID or Post ID are missing from the request. if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) { return; @@ -285,14 +286,54 @@ public function maybe_run_subscriber_authentication() { return; } + // Initialize the API. + $this->api = new ConvertKit_API_V4( + CONVERTKIT_OAUTH_CLIENT_ID, + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, + $this->settings->get_access_token(), + $this->settings->get_refresh_token(), + $this->settings->debug_enabled(), + 'restrict_content' + ); + // Sanitize inputs. $email = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_email'] ) ); $this->resource_type = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_resource_type'] ) ); $this->resource_id = absint( $_REQUEST['convertkit_resource_id'] ); $this->post_id = absint( $_REQUEST['convertkit_post_id'] ); + // If Restrict Content is by tag, tag the subscriber. + if ( $this->resource_type === 'tag' ) { + // Tag subscriber. + $result = $this->api->tag_subscribe( $this->resource_id, $email ); + + // Bail if an error occured. + if ( is_wp_error( $result ) ) { + $this->error = $result; + return; + } + + // If require login is disabled, return now. + if ( ! $this->restrict_content_settings->require_tag_login() ) { + // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. + $subscriber = new ConvertKit_Subscriber(); + $subscriber->forget(); + + // Fetch the subscriber ID from the result. + $subscriber_id = $result['subscriber']['id']; + + // Store subscriber ID in cookie. + $this->store_subscriber_id_in_cookie( $subscriber_id ); + + // Redirect. + $this->redirect( $this->post_id ); + return; + } + } + + // If here, require login is enabled for tags or this is a product/form. // Run subscriber authentication. - $result = $this->run_subscriber_authentication( $email, $this->post_id, $this->resource_type, $this->resource_id ); + $result = $this->run_subscriber_authentication( $email, $this->post_id ); // Bail if an error occured. if ( is_wp_error( $result ) ) { @@ -373,19 +414,10 @@ public function maybe_run_subscriber_verification() { * * @param string $email Email address. * @param int $post_id Post ID. - * @param string $resource_type Resource type. - * @param int $resource_id Resource ID. - * - * @return WP_Error|string Error or Token. + * + * @return WP_Error|string Error or Token. */ - public function run_subscriber_authentication( $email, $post_id, $resource_type, $resource_id ) { - - error_log( 'run_subscriber_authentication' ); - - error_log($email); - error_log($post_id); - error_log($resource_type); - error_log($resource_id); + public function run_subscriber_authentication( $email, $post_id ) { // Initialize the API. $this->api = new ConvertKit_API_V4( @@ -397,98 +429,37 @@ public function run_subscriber_authentication( $email, $post_id, $resource_type, 'restrict_content' ); - // Run subscriber authentication / subscription depending on the resource type. - switch ( $resource_type ) { - case 'product': - case 'form': - // Send email to subscriber with a link to authenticate they have access to the email address submitted. - $token = $this->api->subscriber_authentication_send_code( - $email, - $this->get_url( $post_id ) - ); - - // Bail if an error occured. - if ( is_wp_error( $token ) ) { - return $token; - } - - // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. - $subscriber = new ConvertKit_Subscriber(); - $subscriber->forget(); - - // Return the token. - return $token; - - case 'tag': - // If require login is enabled, show the login screen. - if ( $this->restrict_content_settings->require_tag_login() ) { - // Tag the subscriber, unless this is an AJAX request. - // @TODO Check. - if ( ! $this->is_rest_request() ) { - $result = $this->api->tag_subscribe( $resource_id, $email ); - - // Bail if an error occured. - if ( is_wp_error( $result ) ) { - return $result; - } - } - - // Send email to subscriber with a link to authenticate they have access to the email address submitted. - $token = $this->api->subscriber_authentication_send_code( - $email, - $this->get_url( $post_id ) - ); - - // Bail if an error occured. - if ( is_wp_error( $token ) ) { - return $token; - } - - // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. - $subscriber = new ConvertKit_Subscriber(); - $subscriber->forget(); - - // Return the token. - return $token; - } - - // If here, require login is disabled. - // Check reCAPTCHA, tag subscriber and assign subscriber ID integer to cookie - // without email link. - $recaptcha = new ConvertKit_Recaptcha(); - $recaptcha_response = $recaptcha->verify_recaptcha( - ( isset( $_POST['g-recaptcha-response'] ) ? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) ) : '' ), - 'convertkit_restrict_content_tag' - ); - - // Bail if reCAPTCHA failed. - if ( is_wp_error( $recaptcha_response ) ) { - return $recaptcha_response; - } - - // Tag the subscriber. - $result = $this->api->tag_subscribe( $this->resource_id, $email ); - - // Bail if an error occured. - if ( is_wp_error( $result ) ) { - return $result; - } - - // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. - $subscriber = new ConvertKit_Subscriber(); - $subscriber->forget(); + // Send email to subscriber with a link to authenticate they have access to the email address submitted. + $token = $this->api->subscriber_authentication_send_code( + $email, + $this->get_url( $post_id ) + ); - // Fetch the subscriber ID from the result. - $subscriber_id = $result['subscriber']['id']; + // Bail if an error occured. + if ( is_wp_error( $token ) ) { + return $token; + } - // Store subscriber ID in cookie. - $this->store_subscriber_id_in_cookie( $subscriber_id ); - break; + // Clear any existing subscriber ID cookie, as the authentication flow has started by sending the email. + $subscriber = new ConvertKit_Subscriber(); + $subscriber->forget(); - } + // Return the token. + return $token; } + /** + * Runs subscriber verification. + * + * @since 3.1.0 + * + * @param int $post_id Post ID. + * @param string $token Token. + * @param string $subscriber_code Subscriber code. + * + * @return WP_Error|string Error or Signed Subscriber ID. + */ public function run_subscriber_verification( $post_id, $token, $subscriber_code ) { // Initialize the API. @@ -502,10 +473,7 @@ public function run_subscriber_verification( $post_id, $token, $subscriber_code ); // Verify the token and subscriber code. - $subscriber_id = $this->api->subscriber_authentication_verify( - sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ), - sanitize_text_field( wp_unslash( $_REQUEST['subscriber_code'] ) ) - ); + $subscriber_id = $this->api->subscriber_authentication_verify( $token, $subscriber_code ); // Bail if an error occured. if ( is_wp_error( $subscriber_id ) ) { @@ -515,6 +483,7 @@ public function run_subscriber_verification( $post_id, $token, $subscriber_code // Store subscriber ID in cookie. $this->store_subscriber_id_in_cookie( $subscriber_id ); + // Return signed subscriber ID. return $subscriber_id; } @@ -768,9 +737,9 @@ private function store_subscriber_id_in_cookie( $subscriber_id ) { * * @since 2.3.7 * - * @param int $post_id Post ID. + * @param int $post_id Post ID. */ - private function redirect( $post_id) { + private function redirect( $post_id ) { // Redirect to the Post, appending a query parameter to the URL to prevent caching plugins and // aggressive cache hosting configurations from serving a cached page, which would @@ -786,9 +755,9 @@ private function redirect( $post_id) { * * @since 2.1.0 * - * @param int $post_id Post ID. - * @param bool $cache_bust Include `ck-cache-bust` parameter in URL. - * @return string URL. + * @param int $post_id Post ID. + * @param bool $cache_bust Include `ck-cache-bust` parameter in URL. + * @return string URL. */ public function get_url( $post_id, $cache_bust = false ) { From 5bf0aa4a1bb459ac5b2b8795865d1ad204d9c761 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 21:40:45 +0800 Subject: [PATCH 08/22] Remove Rest API checks These are no longer needed now that maybe_run_* methods are purely for non-API requests following the logic refactor --- ...ass-convertkit-output-restrict-content.php | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 950a66403..9cfe8d79d 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -110,16 +110,7 @@ public function __construct() { $this->settings = new ConvertKit_Settings(); $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content(); - // Register REST API routes. add_action( 'rest_api_init', array( $this, 'register_routes' ) ); - - // Don't register any hooks if this is a WP REST API request, otherwise - // maybe_run_subscriber_authentication() and maybe_run_subscriber_verification() will run - // twice in a WP REST API request (once here, and once when called by WP REST API). - if ( $this->is_rest_request() || array_key_exists( 'HTTP_X_WP_NONCE', $_SERVER ) ) { - return; - } - add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 ); add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 ); add_action( 'wp', array( $this, 'register_content_filter' ), 5 ); @@ -1464,19 +1455,6 @@ function () use ( $post_id, $resource_id, $resource_type ) { } - /** - * Determines if the request is a WordPress REST API request. - * - * @since 3.1.0 - * - * @return bool - */ - private function is_rest_request() { - - return defined( 'REST_REQUEST' ) && REST_REQUEST; - - } - /** * Whether this request is from a search engine crawler. * From b5290b21e7ca3bebcebb81c0fad9dd3fa468946b Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 21:59:41 +0800 Subject: [PATCH 09/22] Added tests --- tests/Integration/RESTAPITest.php | 182 ++++++++++++++++++++++++------ 1 file changed, 146 insertions(+), 36 deletions(-) diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index 1f465eb14..46510f122 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -301,18 +301,28 @@ public function testRefreshResourcesRestrictContent() $this->assertArrayHasKeys( $data['products'][0], [ 'id', 'name', 'url', 'published' ] ); } + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Form ID and subscriber + * + * @since 3.1.0 + */ public function testRestrictContentSubscriberAuthenticationForm() { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + // Build request. - $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); - $request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); - $request->set_body_params([ - 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], - 'convertkit_resource_type' => 'form', - 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], - 'convertkit_post_id' => 1, - ]); + $request->set_body_params( + [ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'form', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'convertkit_post_id' => $post_id, + ] + ); // Send request. $response = rest_get_server()->dispatch( $request ); @@ -327,46 +337,64 @@ public function testRestrictContentSubscriberAuthenticationForm() $this->assertArrayHasKey( 'data', $data ); } + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Form ID and an invalid subscriber email is given + * + * @since 3.1.0 + */ public function testRestrictContentSubscriberAuthenticationFormInvalidEmail() { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + // Build request. - $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); - //$request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); - $request->set_body_params([ - 'convertkit_email' => 'fail@kit.com', - 'convertkit_resource_type' => 'form', - 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], - 'convertkit_post_id' => 1, - ]); + $request->set_body_params( + [ + 'convertkit_email' => 'fail@kit.com', + 'convertkit_resource_type' => 'form', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'convertkit_post_id' => $post_id, + ] + ); // Send request. $response = rest_get_server()->dispatch( $request ); // Assert response is successful. - //$this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status() ); // Assert response data has the expected keys and data. $data = $response->get_data(); - var_dump( $data ); - die(); $this->assertIsArray( $data ); - $this->assertTrue( $data['success'] ); + $this->assertFalse( $data['success'] ); $this->assertArrayHasKey( 'data', $data ); } + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Tag ID and subscriber + * + * @since 3.1.0 + */ public function testRestrictContentSubscriberAuthenticationTag() { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + // Build request. - $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); - $request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); - $request->set_body_params([ - 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], - 'convertkit_resource_type' => 'tag', - 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_TAG_ID'], - 'convertkit_post_id' => 1, - ]); + $request->set_body_params( + [ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'tag', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_TAG_ID'], + 'convertkit_post_id' => $post_id, + ] + ); // Send request. $response = rest_get_server()->dispatch( $request ); @@ -381,18 +409,64 @@ public function testRestrictContentSubscriberAuthenticationTag() $this->assertArrayHasKey( 'data', $data ); } + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Tag ID and an invalid subscriber email is given + * + * @since 3.1.0 + */ + public function testRestrictContentSubscriberAuthenticationTagInvalidEmail() + { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( + [ + 'convertkit_email' => 'fail@kit.com', + 'convertkit_resource_type' => 'tag', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_TAG_ID'], + 'convertkit_post_id' => $post_id, + ] + ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertFalse( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Product ID and subscriber + * + * @since 3.1.0 + */ public function testRestrictContentSubscriberAuthenticationProduct() { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + // Build request. - $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); - $request->set_header( 'X-WP-Nonce', \wp_create_nonce( 'wp_rest' ) ); - $request->set_body_params([ - 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], - 'convertkit_resource_type' => 'product', - 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_PRODUCT_ID'], - 'convertkit_post_id' => 1, - ]); + $request->set_body_params( + [ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'product', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_PRODUCT_ID'], + 'convertkit_post_id' => $post_id, + ] + ); // Send request. $response = rest_get_server()->dispatch( $request ); @@ -407,6 +481,42 @@ public function testRestrictContentSubscriberAuthenticationProduct() $this->assertArrayHasKey( 'data', $data ); } + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Product ID and an invalid subscriber email is given + * + * @since 3.1.0 + */ + public function testRestrictContentSubscriberAuthenticationProductInvalidEmail() + { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( + [ + 'convertkit_email' => 'fail@kit.com', + 'convertkit_resource_type' => 'product', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_PRODUCT_ID'], + 'convertkit_post_id' => $post_id, + ] + ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertFalse( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + /** * Act as an editor user. * From 35bece6b237061c85dfb3d474efa11851c2bb008 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 21:59:49 +0800 Subject: [PATCH 10/22] Coding standards, PHPStan compat. --- ...ass-convertkit-output-restrict-content.php | 72 +++++++------------ 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 9cfe8d79d..ebebd948b 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -106,11 +106,8 @@ class ConvertKit_Output_Restrict_Content { */ public function __construct() { - // Initialize classes that will be used. - $this->settings = new ConvertKit_Settings(); - $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content(); - add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + add_action( 'init', array( $this, 'initialize_classes' ) ); add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 ); add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 ); add_action( 'wp', array( $this, 'register_content_filter' ), 5 ); @@ -128,6 +125,9 @@ public function __construct() { */ public function register_routes() { + // Initialize classes that will be used. + $this->initialize_classes(); + // Register route to run subscriber authentication. register_rest_route( 'kit/v1', @@ -145,9 +145,7 @@ public function register_routes() { // Run subscriber authentication. $result = $this->run_subscriber_authentication( $email, - $post_id, - $resource_type, - $resource_id + $post_id ); // If an error occured, build the email form view with the error message. @@ -236,6 +234,26 @@ public function register_routes() { ); } + /** + * Initialize classes that will be used. + * + * @since 3.1.0 + */ + public function initialize_classes() { + + $this->settings = new ConvertKit_Settings(); + $this->restrict_content_settings = new ConvertKit_Settings_Restrict_Content(); + $this->api = new ConvertKit_API_V4( + CONVERTKIT_OAUTH_CLIENT_ID, + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, + $this->settings->get_access_token(), + $this->settings->get_refresh_token(), + $this->settings->debug_enabled(), + 'restrict_content' + ); + + } + /** * 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. * Also runs if restrict content by tag and require login is disabled, as we immediately tag and redirect if this is the case. @@ -277,16 +295,6 @@ public function maybe_run_subscriber_authentication() { return; } - // Initialize the API. - $this->api = new ConvertKit_API_V4( - CONVERTKIT_OAUTH_CLIENT_ID, - CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $this->settings->get_access_token(), - $this->settings->get_refresh_token(), - $this->settings->debug_enabled(), - 'restrict_content' - ); - // Sanitize inputs. $email = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_email'] ) ); $this->resource_type = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_resource_type'] ) ); @@ -410,16 +418,6 @@ public function maybe_run_subscriber_verification() { */ public function run_subscriber_authentication( $email, $post_id ) { - // Initialize the API. - $this->api = new ConvertKit_API_V4( - CONVERTKIT_OAUTH_CLIENT_ID, - CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $this->settings->get_access_token(), - $this->settings->get_refresh_token(), - $this->settings->debug_enabled(), - 'restrict_content' - ); - // Send email to subscriber with a link to authenticate they have access to the email address submitted. $token = $this->api->subscriber_authentication_send_code( $email, @@ -453,16 +451,6 @@ public function run_subscriber_authentication( $email, $post_id ) { */ public function run_subscriber_verification( $post_id, $token, $subscriber_code ) { - // Initialize the API. - $this->api = new ConvertKit_API_V4( - CONVERTKIT_OAUTH_CLIENT_ID, - CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $this->settings->get_access_token(), - $this->settings->get_refresh_token(), - $this->settings->debug_enabled(), - 'restrict_content' - ); - // Verify the token and subscriber code. $subscriber_id = $this->api->subscriber_authentication_verify( $token, $subscriber_code ); @@ -985,16 +973,6 @@ private function resource_exists() { */ private function subscriber_has_access( $subscriber_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter - // Initialize the API. - $this->api = new ConvertKit_API_V4( - CONVERTKIT_OAUTH_CLIENT_ID, - CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $this->settings->get_access_token(), - $this->settings->get_refresh_token(), - $this->settings->debug_enabled(), - 'restrict_content' - ); - // Depending on the resource type, determine if the subscriber has access to it. // This is deliberately a switch statement, because we will likely add in support // for restrict by tag and form later. From b2ef6f59182f0872ecab5a78f1dce1fc5f022028 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 18 Nov 2025 22:08:10 +0800 Subject: [PATCH 11/22] Add missing reCAPTCHA code --- .../class-convertkit-output-restrict-content.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index ebebd948b..bec47ae48 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -303,6 +303,19 @@ public function maybe_run_subscriber_authentication() { // If Restrict Content is by tag, tag the subscriber. if ( $this->resource_type === 'tag' ) { + // Check reCAPTCHA. + $recaptcha = new ConvertKit_Recaptcha(); + $recaptcha_response = $recaptcha->verify_recaptcha( + ( isset( $_POST['g-recaptcha-response'] ) ? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) ) : '' ), + 'convertkit_restrict_content_tag' + ); + + // Bail if reCAPTCHA failed. + if ( is_wp_error( $recaptcha_response ) ) { + $this->error = $recaptcha_response; + return; + } + // Tag subscriber. $result = $this->api->tag_subscribe( $this->resource_id, $email ); From d5e65e62bd81af1db7e7435eebbf28db51c9f7a5 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 13:46:50 +0800 Subject: [PATCH 12/22] Fix class initialisation --- includes/class-convertkit-output-restrict-content.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 43a0c9f0c..e7b1ff9d3 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -107,7 +107,7 @@ class ConvertKit_Output_Restrict_Content { public function __construct() { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); - add_action( 'init', array( $this, 'initialize_classes' ) ); + add_action( 'init', array( $this, 'initialize_classes' ), 2 ); add_action( 'init', array( $this, 'maybe_run_subscriber_authentication' ), 3 ); add_action( 'wp', array( $this, 'maybe_run_subscriber_verification' ), 4 ); add_action( 'wp', array( $this, 'register_content_filter' ), 5 ); From 43fe89f584a42f52811a932678d97415fe155039 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 14:35:28 +0800 Subject: [PATCH 13/22] Isolate failing integration test --- .github/workflows/tests.yml | 21 ++----------------- ...ass-convertkit-output-restrict-content.php | 9 +++++--- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e4c920b87..42a37f0eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,28 +52,11 @@ jobs: fail-fast: false matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.1', '8.2', '8.3', '8.4' ] #[ '7.4', '8.0', '8.1' ] + php-versions: [ '8.1' ] #[ '7.4', '8.0', '8.1' ] # Folder names within the 'tests' folder to run tests in parallel. test-groups: [ - 'EndToEnd/broadcasts/blocks-shortcodes', - 'EndToEnd/broadcasts/import-export', - 'EndToEnd/forms/blocks-shortcodes', - 'EndToEnd/forms/general', - 'EndToEnd/forms/post-types', - 'EndToEnd/general/other', - 'EndToEnd/general/plugin-screens', - 'EndToEnd/integrations/divi-builder', - 'EndToEnd/integrations/divi-theme', - 'EndToEnd/integrations/other', - 'EndToEnd/integrations/wlm', - 'EndToEnd/integrations/woocommerce', - 'EndToEnd/landing-pages', - 'EndToEnd/products', - 'EndToEnd/restrict-content/general', - 'EndToEnd/restrict-content/post-types', - 'EndToEnd/tags', - 'Integration' + 'Integration/RestAPITest:RestrictContentSubscriberAuthenticationForm' ] # Steps to install, configure and run tests diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index e7b1ff9d3..5f3b7cf4d 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -125,9 +125,6 @@ public function __construct() { */ public function register_routes() { - // Initialize classes that will be used. - $this->initialize_classes(); - // Register route to run subscriber authentication. register_rest_route( 'kit/v1', @@ -136,6 +133,9 @@ public function register_routes() { 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( $request ) { + // Initialize classes that will be used. + $this->initialize_classes(); + // Fetch Post ID, Resource Type and Resource ID for the view. $email = $request->get_param( 'convertkit_email' ); $post_id = $request->get_param( 'convertkit_post_id' ); @@ -192,6 +192,9 @@ public function register_routes() { 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( $request ) { + // Initialize classes that will be used. + $this->initialize_classes(); + // Fetch Post ID, Resource Type and Resource ID for the view. $post_id = $request->get_param( 'convertkit_post_id' ); $token = $request->get_param( 'token' ); From df6f9d98e092621b2218412d013b1b0ca35d1f81 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 14:40:19 +0800 Subject: [PATCH 14/22] Isolate test another way --- .github/workflows/tests.yml | 2 +- tests/Integration/RESTAPITest.php | 72 +++++++++++++++---------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 42a37f0eb..9dd58d99d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,7 +56,7 @@ jobs: # Folder names within the 'tests' folder to run tests in parallel. test-groups: [ - 'Integration/RestAPITest:RestrictContentSubscriberAuthenticationForm' + 'Integration' ] # Steps to install, configure and run tests diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index 46510f122..d1945fe9f 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -62,6 +62,42 @@ public function tearDown(): void parent::tearDown(); } + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Form ID and subscriber + * + * @since 3.1.0 + */ + public function testRestrictContentSubscriberAuthenticationForm() + { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( + [ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'form', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'convertkit_post_id' => $post_id, + ] + ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertTrue( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + /** * Test that the /wp-json/kit/v1/blocks REST API route returns a 401 when the user is not authorized. * @@ -301,42 +337,6 @@ public function testRefreshResourcesRestrictContent() $this->assertArrayHasKeys( $data['products'][0], [ 'id', 'name', 'url', 'published' ] ); } - /** - * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when - * requesting the subscriber authentication email to be sent for a given Form ID and subscriber - * - * @since 3.1.0 - */ - public function testRestrictContentSubscriberAuthenticationForm() - { - // Create a Post. - $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); - - // Build request. - $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); - $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); - $request->set_body_params( - [ - 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], - 'convertkit_resource_type' => 'form', - 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], - 'convertkit_post_id' => $post_id, - ] - ); - - // Send request. - $response = rest_get_server()->dispatch( $request ); - - // Assert response is successful. - $this->assertSame( 200, $response->get_status() ); - - // Assert response data has the expected keys and data. - $data = $response->get_data(); - $this->assertIsArray( $data ); - $this->assertTrue( $data['success'] ); - $this->assertArrayHasKey( 'data', $data ); - } - /** * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when * requesting the subscriber authentication email to be sent for a given Form ID and an invalid subscriber email is given From 8c9b83c4c0b0665249dcda415fa5586647b57be7 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 14:49:58 +0800 Subject: [PATCH 15/22] Initialize classes using singleton --- includes/class-convertkit-output-restrict-content.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 5f3b7cf4d..c63b14328 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -134,7 +134,7 @@ public function register_routes() { 'callback' => function ( $request ) { // Initialize classes that will be used. - $this->initialize_classes(); + WP_ConvertKit()->get_class( 'output_restrict_content' )->initialize_classes(); // Fetch Post ID, Resource Type and Resource ID for the view. $email = $request->get_param( 'convertkit_email' ); @@ -193,7 +193,7 @@ public function register_routes() { 'callback' => function ( $request ) { // Initialize classes that will be used. - $this->initialize_classes(); + WP_ConvertKit()->get_class( 'output_restrict_content' )->initialize_classes(); // Fetch Post ID, Resource Type and Resource ID for the view. $post_id = $request->get_param( 'convertkit_post_id' ); From 44eb9d919b35a40ab13d96000db59341ca7348aa Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 14:53:50 +0800 Subject: [PATCH 16/22] Debug failure --- tests/Integration/RESTAPITest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index d1945fe9f..143ebc401 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -93,9 +93,12 @@ public function testRestrictContentSubscriberAuthenticationForm() // Assert response data has the expected keys and data. $data = $response->get_data(); + var_dump( $data ); + die(); $this->assertIsArray( $data ); $this->assertTrue( $data['success'] ); $this->assertArrayHasKey( 'data', $data ); + $this->assertStringContainsString( 'authentication code', $data['data'] ); } /** From 8dabe19f7371fd4c75e2fe1cfa58548f2b50fbd3 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 14:58:07 +0800 Subject: [PATCH 17/22] Include error in JSON response --- includes/class-convertkit-output-restrict-content.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index c63b14328..5f4d3609b 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -161,6 +161,7 @@ public function register_routes() { array( 'success' => false, 'data' => $output, + 'error' => $result->get_error_message(), ) ); } From 3b92ebd8df8e21852d53e81b2ad0586782d7a6d7 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 15:00:40 +0800 Subject: [PATCH 18/22] Use singleton across REST API routes --- ...ass-convertkit-output-restrict-content.php | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 5f4d3609b..425e0ba95 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -134,7 +134,8 @@ public function register_routes() { 'callback' => function ( $request ) { // Initialize classes that will be used. - WP_ConvertKit()->get_class( 'output_restrict_content' )->initialize_classes(); + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); + $output_restrict_content->initialize_classes(); // Fetch Post ID, Resource Type and Resource ID for the view. $email = $request->get_param( 'convertkit_email' ); @@ -143,7 +144,7 @@ public function register_routes() { $resource_id = $request->get_param( 'convertkit_resource_id' ); // Run subscriber authentication. - $result = $this->run_subscriber_authentication( + $result = $output_restrict_content->run_subscriber_authentication( $email, $post_id ); @@ -151,7 +152,7 @@ public function register_routes() { // If an error occured, build the email form view with the error message. if ( is_wp_error( $result ) ) { // Set error to display on screen. - $this->error = $result; + $output_restrict_content->error = $result; // Build email form view to return for output with error message. ob_start(); @@ -167,8 +168,8 @@ public function register_routes() { } // Set token and Post ID for authentication code view. - $this->token = $result; - $this->post_id = $post_id; + $output_restrict_content->token = $result; + $output_restrict_content->post_id = $post_id; // Build authentication code view to return for output. ob_start(); @@ -194,7 +195,8 @@ public function register_routes() { 'callback' => function ( $request ) { // Initialize classes that will be used. - WP_ConvertKit()->get_class( 'output_restrict_content' )->initialize_classes(); + $output_restrict_content = WP_ConvertKit()->get_class( 'output_restrict_content' ); + $output_restrict_content->initialize_classes(); // Fetch Post ID, Resource Type and Resource ID for the view. $post_id = $request->get_param( 'convertkit_post_id' ); @@ -202,16 +204,16 @@ public function register_routes() { $subscriber_code = $request->get_param( 'subscriber_code' ); // Run subscriber authentication. - $result = $this->run_subscriber_verification( $post_id, $token, $subscriber_code ); + $result = $output_restrict_content->run_subscriber_verification( $post_id, $token, $subscriber_code ); // If an error occured, build the code form view with the error message. if ( is_wp_error( $result ) ) { // Set error to display on screen. - $this->error = $result; + $output_restrict_content->error = $result; // Set token and post ID for authentication code view. - $this->token = $token; - $this->post_id = $post_id; + $output_restrict_content->token = $token; + $output_restrict_content->post_id = $post_id; // Build code form view to return for output with error message. ob_start(); @@ -221,6 +223,7 @@ public function register_routes() { array( 'success' => false, 'data' => $output, + 'error' => $result->get_error_message(), ) ); } @@ -229,7 +232,7 @@ public function register_routes() { return rest_ensure_response( array( 'success' => true, - 'url' => $this->get_url( $post_id, true ), + 'url' => $output_restrict_content->get_url( $post_id, true ), ) ); }, From b9d602bf19977877b5020ede01b43433c6f4703f Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Nov 2025 15:05:56 +0800 Subject: [PATCH 19/22] Reinstate all tests --- .github/workflows/tests.yml | 19 ++++- ...ass-convertkit-output-restrict-content.php | 2 - tests/Integration/RESTAPITest.php | 75 +++++++++---------- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9dd58d99d..e4c920b87 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,10 +52,27 @@ jobs: fail-fast: false matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.1' ] #[ '7.4', '8.0', '8.1' ] + php-versions: [ '8.1', '8.2', '8.3', '8.4' ] #[ '7.4', '8.0', '8.1' ] # Folder names within the 'tests' folder to run tests in parallel. test-groups: [ + 'EndToEnd/broadcasts/blocks-shortcodes', + 'EndToEnd/broadcasts/import-export', + 'EndToEnd/forms/blocks-shortcodes', + 'EndToEnd/forms/general', + 'EndToEnd/forms/post-types', + 'EndToEnd/general/other', + 'EndToEnd/general/plugin-screens', + 'EndToEnd/integrations/divi-builder', + 'EndToEnd/integrations/divi-theme', + 'EndToEnd/integrations/other', + 'EndToEnd/integrations/wlm', + 'EndToEnd/integrations/woocommerce', + 'EndToEnd/landing-pages', + 'EndToEnd/products', + 'EndToEnd/restrict-content/general', + 'EndToEnd/restrict-content/post-types', + 'EndToEnd/tags', 'Integration' ] diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 425e0ba95..97a2aba26 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -162,7 +162,6 @@ public function register_routes() { array( 'success' => false, 'data' => $output, - 'error' => $result->get_error_message(), ) ); } @@ -223,7 +222,6 @@ public function register_routes() { array( 'success' => false, 'data' => $output, - 'error' => $result->get_error_message(), ) ); } diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index 143ebc401..46510f122 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -62,45 +62,6 @@ public function tearDown(): void parent::tearDown(); } - /** - * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when - * requesting the subscriber authentication email to be sent for a given Form ID and subscriber - * - * @since 3.1.0 - */ - public function testRestrictContentSubscriberAuthenticationForm() - { - // Create a Post. - $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); - - // Build request. - $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); - $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); - $request->set_body_params( - [ - 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], - 'convertkit_resource_type' => 'form', - 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], - 'convertkit_post_id' => $post_id, - ] - ); - - // Send request. - $response = rest_get_server()->dispatch( $request ); - - // Assert response is successful. - $this->assertSame( 200, $response->get_status() ); - - // Assert response data has the expected keys and data. - $data = $response->get_data(); - var_dump( $data ); - die(); - $this->assertIsArray( $data ); - $this->assertTrue( $data['success'] ); - $this->assertArrayHasKey( 'data', $data ); - $this->assertStringContainsString( 'authentication code', $data['data'] ); - } - /** * Test that the /wp-json/kit/v1/blocks REST API route returns a 401 when the user is not authorized. * @@ -340,6 +301,42 @@ public function testRefreshResourcesRestrictContent() $this->assertArrayHasKeys( $data['products'][0], [ 'id', 'name', 'url', 'published' ] ); } + /** + * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when + * requesting the subscriber authentication email to be sent for a given Form ID and subscriber + * + * @since 3.1.0 + */ + public function testRestrictContentSubscriberAuthenticationForm() + { + // Create a Post. + $post_id = static::factory()->post->create( [ 'post_title' => 'Test Post' ] ); + + // Build request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/restrict-content/subscriber-authentication' ); + $request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( + [ + 'convertkit_email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + 'convertkit_resource_type' => 'form', + 'convertkit_resource_id' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'convertkit_post_id' => $post_id, + ] + ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertTrue( $data['success'] ); + $this->assertArrayHasKey( 'data', $data ); + } + /** * Test that the /wp-json/kit/v1/restrict-content/subscriber-authentication REST API route when * requesting the subscriber authentication email to be sent for a given Form ID and an invalid subscriber email is given From 4770649ed277c1d4dafab28c4912927de445d72c Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 20 Nov 2025 11:31:28 +0800 Subject: [PATCH 20/22] Rename functions from run_* to more specific API calls for clarity --- ...lass-convertkit-output-restrict-content.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/includes/class-convertkit-output-restrict-content.php b/includes/class-convertkit-output-restrict-content.php index 97a2aba26..d07fcfdc9 100644 --- a/includes/class-convertkit-output-restrict-content.php +++ b/includes/class-convertkit-output-restrict-content.php @@ -144,7 +144,7 @@ public function register_routes() { $resource_id = $request->get_param( 'convertkit_resource_id' ); // Run subscriber authentication. - $result = $output_restrict_content->run_subscriber_authentication( + $result = $output_restrict_content->subscriber_authentication_send_code( $email, $post_id ); @@ -203,7 +203,7 @@ public function register_routes() { $subscriber_code = $request->get_param( 'subscriber_code' ); // Run subscriber authentication. - $result = $output_restrict_content->run_subscriber_verification( $post_id, $token, $subscriber_code ); + $result = $output_restrict_content->subscriber_authentication_verify( $post_id, $token, $subscriber_code ); // If an error occured, build the code form view with the error message. if ( is_wp_error( $result ) ) { @@ -350,7 +350,7 @@ public function maybe_run_subscriber_authentication() { // If here, require login is enabled for tags or this is a product/form. // Run subscriber authentication. - $result = $this->run_subscriber_authentication( $email, $this->post_id ); + $result = $this->subscriber_authentication_send_code( $email, $this->post_id ); // Bail if an error occured. if ( is_wp_error( $result ) ) { @@ -411,7 +411,7 @@ public function maybe_run_subscriber_verification() { } // Run subscriber verification. - $subscriber_id = $this->run_subscriber_verification( $this->post_id, sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ), sanitize_text_field( wp_unslash( $_REQUEST['subscriber_code'] ) ) ); + $subscriber_id = $this->subscriber_authentication_verify( $this->post_id, sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ), sanitize_text_field( wp_unslash( $_REQUEST['subscriber_code'] ) ) ); // Bail if an error occured. if ( is_wp_error( $subscriber_id ) ) { @@ -425,7 +425,7 @@ public function maybe_run_subscriber_verification() { } /** - * Runs subscriber authentication / subscription depending on the resource type. + * Sends an email to the subscriber with a code and link to authenticate they have access to the email address submitted. * * @since 3.1.0 * @@ -434,7 +434,7 @@ public function maybe_run_subscriber_verification() { * * @return WP_Error|string Error or Token. */ - public function run_subscriber_authentication( $email, $post_id ) { + public function subscriber_authentication_send_code( $email, $post_id ) { // Send email to subscriber with a link to authenticate they have access to the email address submitted. $token = $this->api->subscriber_authentication_send_code( @@ -457,7 +457,9 @@ public function run_subscriber_authentication( $email, $post_id ) { } /** - * Runs subscriber verification. + * Verifies the token and subscriber code, which tells us that the email + * address supplied truly belongs to the user, and that we can safely + * trust their subscriber ID to be valid. * * @since 3.1.0 * @@ -467,7 +469,7 @@ public function run_subscriber_authentication( $email, $post_id ) { * * @return WP_Error|string Error or Signed Subscriber ID. */ - public function run_subscriber_verification( $post_id, $token, $subscriber_code ) { + public function subscriber_authentication_verify( $post_id, $token, $subscriber_code ) { // Verify the token and subscriber code. $subscriber_id = $this->api->subscriber_authentication_verify( $token, $subscriber_code ); From 944bf8bae9081b6ae040d8ce035bb644b73e1b07 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 20 Nov 2025 11:31:41 +0800 Subject: [PATCH 21/22] Update comments to make clearer what the returned data is --- resources/frontend/js/restrict-content.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/resources/frontend/js/restrict-content.js b/resources/frontend/js/restrict-content.js index ae5299df9..48dd67290 100644 --- a/resources/frontend/js/restrict-content.js +++ b/resources/frontend/js/restrict-content.js @@ -127,17 +127,17 @@ function convertKitRestrictContentCloseModal() { } /** - * Submits the given email address to maybe_run_subscriber_authentication(), which - * will return either: + * Submits the given email address to the WP REST API kit/v1/restrict-content/subscriber-authentication + * endpoint, which will return either: * - the email form view, with an error message e.g. invalid email, * - the code form view, where the user can enter the OTP. * * @since 2.3.8 * * @param {string} nonce WordPress nonce. - * @param {string} email Email address. resource_type Resource Type (tag|product). - * @param {string} resource_type Resource Type (tag|product). - * @param {string} resource_id Resource ID (ConvertKit Tag or Product ID). + * @param {string} email Email address. + * @param {string} resource_type Resource Type (form|tag|product). + * @param {string} resource_id Resource ID (Kit Form,Tag or Product ID). * @param {number} post_id WordPress Post ID being viewed / accessed. */ function convertKitRestrictContentSubscriberAuthenticationSendCode( @@ -178,7 +178,9 @@ function convertKitRestrictContentSubscriberAuthenticationSendCode( '#convertkit-restrict-content-modal-content' ).innerHTML = result.message; } else { - // Output response, which will be a form with/without an error message. + // Output response, which will be either: + // - the email form view, with an error message e.g. invalid email, + // - the code form view, where the user can enter the OTP. document.querySelector( '#convertkit-restrict-content-modal-content' ).innerHTML = result.data; @@ -200,8 +202,8 @@ function convertKitRestrictContentSubscriberAuthenticationSendCode( } /** - * Submits the given email address to maybe_run_subscriber_verification(), which - * will return either: + * Submits the given email address to the WP REST API kit/v1/restrict-content/subscriber-verification + * endpoint, which will return either: * - the code form view, with an error message e.g. invalid code entered, * - the Post's URL, with a `ck-cache-bust` parameter appended, which can then be loaded to show the content. * From 51cb3016fd52c4a86dc13c986b39dbf938a11680 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 20 Nov 2025 11:46:53 +0800 Subject: [PATCH 22/22] Coding standards on tests --- tests/EndToEnd/landing-pages/PageLandingPageCest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/EndToEnd/landing-pages/PageLandingPageCest.php b/tests/EndToEnd/landing-pages/PageLandingPageCest.php index 466bca0b3..7d31404ad 100644 --- a/tests/EndToEnd/landing-pages/PageLandingPageCest.php +++ b/tests/EndToEnd/landing-pages/PageLandingPageCest.php @@ -542,7 +542,7 @@ public function testAddNewPageUsingDefinedLandingPageWithWPRocket(EndToEndTester * Test that the Landing Page specified in the Page Settings works when * creating and viewing a new WordPress Page, with the Rocket LazyLoad * Plugin active (https://wordpress.org/plugins/rocket-lazy-load/). - * + * * This differs from the WP-Rocket Plugin. * * @since 3.1.0 @@ -558,7 +558,7 @@ public function testAddNewPageUsingDefinedLandingPageWithRocketLazyLoadPlugin(En $I->haveOptionInDatabase( 'rocket_lazyload_options', [ - 'images' => 1, + 'images' => 1, 'iframes' => 1, ] );