WP_REST_Server::CREATABLE, 'args' => array( // Email: Validate email is included in the request, is a valid email address // and sanitize the email address. 'convertkit_email' => array( 'required' => true, 'validate_callback' => function ( $param ) { return is_string( $param ) && is_email( $param ); }, 'sanitize_callback' => 'sanitize_email', ), // Post ID: Validate post ID is included in the request and is an integer. 'convertkit_post_id' => array( 'required' => true, 'validate_callback' => function ( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint', ), // Resource Type: Validate resource type is a string, if included in the request. // It's not included when logging in using the Member Content Login block. 'convertkit_resource_type' => array( 'required' => false, 'validate_callback' => function ( $param ) { return is_string( $param ); }, 'sanitize_callback' => 'sanitize_text_field', ), // Resource ID: Validate resource ID is an integer, if included in the request. // It's not included when logging in using the Member Content Login block. 'convertkit_resource_id' => array( 'required' => false, 'validate_callback' => function ( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint', ), // Spam protection response, if a spam protection provider is enabled. 'spam_protection_response' => array( 'required' => false, 'validate_callback' => function ( $param ) { return is_string( $param ); }, 'sanitize_callback' => 'sanitize_text_field', ), // Whether to display the heading above the login form. // It's not displayed by the Member Content Login block, as it refers to // reading the Member Content the subscriber is logging in to view. 'display_heading' => array( 'required' => false, 'default' => true, 'validate_callback' => function ( $param ) { return is_bool( $param ); }, ), ), 'callback' => function ( $request ) { // Initialize classes that will be used. $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' ); $post_id = $request->get_param( 'convertkit_post_id' ); $resource_type = $request->get_param( 'convertkit_resource_type' ); $resource_id = $request->get_param( 'convertkit_resource_id' ); // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings). $result = $output_restrict_content->verify_spam_protection( $request->get_param( 'spam_protection_response' ) ); // If spam protection failed, build the email form view with the error message. if ( is_wp_error( $result ) ) { $output_restrict_content->error = $result; ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/' . ( $request->get_param( 'display_heading' ) ? 'login-modal-content-email.php' : 'login-email.php' ); $output = trim( ob_get_clean() ); return rest_ensure_response( array( 'success' => false, 'data' => $output, ) ); } // Run subscriber authentication. $result = $output_restrict_content->subscriber_authentication_send_code( $email, $post_id ); // If an error occurred, build the email form view with the error message. if ( is_wp_error( $result ) ) { // Set error to display on screen. $output_restrict_content->error = $result; // Build email form view to return for output with error message. ob_start(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/' . ( $request->get_param( 'display_heading' ) ? 'login-modal-content-email.php' : 'login-email.php' ); $output = trim( ob_get_clean() ); return rest_ensure_response( array( 'success' => false, 'data' => $output, ) ); } // Set token and Post ID for authentication code view. $output_restrict_content->token = $result; $output_restrict_content->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'; $output = trim( ob_get_clean() ); return rest_ensure_response( array( 'success' => true, 'data' => $output, ) ); }, // No authentication required, as this is on the frontend site. '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, 'args' => array( // Post ID: Validate post ID is an integer if included in the request. 'convertkit_post_id' => array( 'required' => false, 'validate_callback' => function ( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint', ), // Token: Validate token is included in the request and is a string. 'token' => array( 'required' => true, 'validate_callback' => function ( $param ) { return is_string( $param ); }, 'sanitize_callback' => 'sanitize_text_field', ), // Subscriber Code: Validate subscriber code is included in the request and is a string. 'subscriber_code' => array( 'required' => true, 'validate_callback' => function ( $param ) { return is_string( $param ); }, 'sanitize_callback' => 'sanitize_text_field', ), ), 'callback' => function ( $request ) { // Initialize classes that will be used. $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' ); $token = $request->get_param( 'token' ); $subscriber_code = $request->get_param( 'subscriber_code' ); // Run subscriber authentication. $result = $output_restrict_content->subscriber_authentication_verify( $post_id, $token, $subscriber_code ); // If an error occurred, build the code form view with the error message. if ( is_wp_error( $result ) ) { // Set error to display on screen. $output_restrict_content->error = $result; // Set token and post ID for authentication code view. $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(); include CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal-content-code.php'; $output = trim( ob_get_clean() ); return rest_ensure_response( array( 'success' => false, 'data' => $output, ) ); } // Return success with the URL to the Post, including the `ck-cache-bust` parameter. return rest_ensure_response( array( 'success' => true, 'url' => $output_restrict_content->get_url( $post_id, true ), ) ); }, // No authentication required, as this is on the frontend site. 'permission_callback' => '__return_true', ) ); } /** * 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. * If so, calls the API depending on the Restrict Content resource that's required: * - tag: subscribes the email address to the tag, and calls the API to send the subscriber a magic link by email containing a code. * - form + product: calls the API to send the subscriber a magic link by email containing a code. * * See maybe_run_subscriber_verification() for logic once they click the link in the email or enter the code on screen. * * @since 2.1.0 */ public function maybe_run_subscriber_authentication() { // 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 ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_restrict_content_login' ) ) { return; } // Bail if the expected email or Post ID are missing from the request. if ( ! array_key_exists( 'convertkit_email', $_REQUEST ) ) { return; } if ( ! array_key_exists( 'convertkit_post_id', $_REQUEST ) ) { 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; } // Sanitize inputs. $email = sanitize_text_field( wp_unslash( $_REQUEST['convertkit_email'] ) ); $this->resource_type = ( array_key_exists( 'convertkit_resource_type', $_REQUEST ) ? sanitize_text_field( wp_unslash( $_REQUEST['convertkit_resource_type'] ) ) : '' ); $this->resource_id = ( array_key_exists( 'convertkit_resource_id', $_REQUEST ) ? absint( $_REQUEST['convertkit_resource_id'] ) : 0 ); $this->post_id = absint( $_REQUEST['convertkit_post_id'] ); // If Restrict Content is by tag, tag the subscriber. if ( $this->resource_type === 'tag' ) { // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings). $spam_protection = new ConvertKit_Spam_Protection(); $spam_check = $spam_protection->verify( 'convertkit_restrict_content_tag' ); // Bail if spam protection failed. if ( is_wp_error( $spam_check ) ) { $this->error = $spam_check; return; } // Tag subscriber. $result = $this->api->tag_subscribe( $this->resource_id, $email ); // Bail if an error occurred. if ( is_wp_error( $result ) ) { $this->error = $result; return; } } else { // Check spam protection (reCAPTCHA or Cloudflare Turnstile, depending on Plugin settings). $spam_check = $this->verify_spam_protection(); // Bail if spam protection failed. if ( is_wp_error( $spam_check ) ) { $this->error = $spam_check; return; } } // Run subscriber authentication. $result = $this->subscriber_authentication_send_code( $email, $this->post_id ); // Bail if an error occurred. if ( is_wp_error( $result ) ) { $this->error = $result; return; } // Store the token so it's included in the subscriber code form. $this->token = $result; } /** * 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 * 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 ) && ! is_null( $_REQUEST['_wpnonce'] ) ) { 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->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 occurred. if ( is_wp_error( $subscriber_id ) ) { $this->error = $subscriber_id; return; } // Redirect now to reload the Post. $this->redirect( $this->post_id ); } /** * Logs the subscriber out by deleting their subscriber ID cookie, when the * log out button is clicked in the Member Content Login block. * * @since 3.4.2 */ public function maybe_run_subscriber_logout() { // Bail if no logout request was made. if ( ! array_key_exists( 'convertkit_logout', $_REQUEST ) ) { return; } // Bail if no nonce was specified. if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) { return; } // Bail if the nonce failed validation. if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_member_content_logout' ) ) { return; } // Delete the subscriber ID cookie. $subscriber = new ConvertKit_Subscriber(); $subscriber->forget(); // Reload the Post, so the login form displays. wp_safe_redirect( $this->get_url( get_the_ID(), true ) ); exit(); } /** * Verifies the spam protection response for the login form, using the spam * protection provider enabled in the Plugin's settings. * * @since 3.4.2 * * @param bool|string $response Spam protection response, if supplied by a REST API request. * @return bool|WP_Error */ public function verify_spam_protection( $response = false ) { $spam_protection = new ConvertKit_Spam_Protection(); $provider = $spam_protection->get_active_provider(); // Return true if no spam protection provider is enabled. if ( $provider === false ) { return true; } // Verify the response included in the REST API request. if ( ! empty( $response ) ) { return $provider->verify( $response, 'convertkit_member_content_login' ); } // Verify the response included in the form submission. return $spam_protection->verify( 'convertkit_member_content_login' ); } /** * Enqueues the CSS and JS required by the login form and modal. * * @since 3.4.2 */ public function enqueue_scripts_and_styles() { // Only load styles if the Disable CSS option is off. if ( ! $this->settings->css_disabled() ) { convertkit_enqueue_frontend_css(); } // Bail if scripts are disabled. if ( $this->settings->scripts_disabled() ) { return; } // Enqueue scripts. convertkit_enqueue_frontend_js(); // Define variables. wp_localize_script( 'convertkit-js', '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(), ) ); } /** * Outputs the login modal in the footer, ensuring it is only output once * when a Post contains multiple Member Content Login blocks. * * @since 3.4.2 * * @param int $post_id Post ID. * @param bool|int $resource_id Resource ID. * @param bool|string $resource_type Resource Type. */ public function output_login_modal( $post_id, $resource_id = 0, $resource_type = '' ) { if ( $this->login_modal_output ) { return; } $this->login_modal_output = true; add_action( 'wp_footer', function () use ( $post_id, $resource_id, $resource_type ) { include_once CONVERTKIT_PLUGIN_PATH . '/views/frontend/restrict-content/login-modal.php'; } ); } /** * 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 * * @param string $email Email address. * @param int $post_id Post ID. * * @return WP_Error|string Error or Token. */ 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( $email, $this->get_url( $post_id ) ); // Bail if an error occurred. 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; } /** * 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 * * @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 subscriber_authentication_verify( $post_id, $token, $subscriber_code ) { // Verify the token and subscriber code. $subscriber_id = $this->api->subscriber_authentication_verify( $token, $subscriber_code ); // Bail if an error occurred. if ( is_wp_error( $subscriber_id ) ) { return $subscriber_id; } // Store subscriber ID in cookie. $this->store_subscriber_id_in_cookie( $subscriber_id ); // Return signed subscriber ID. return $subscriber_id; } /** * Registers the applicable content filter for maybe restricting content, depending * on the Theme or Page Builder used. * * @since 2.7.7 */ public function register_content_filter() { // Use the standard `the_content` filter, which works for most Themes // and Page Builders. add_filter( 'the_content', array( $this, 'maybe_restrict_content' ) ); /** * Allow specific Themes and Page Builders to use a different filter * for Restrict Content functionality. * * @since 2.7.7 */ do_action( 'convertkit_restrict_content_register_content_filter' ); } /** * Displays (or hides) content on a singular Page, Post or Custom Post Type's Content, * depending on whether the visitor is an authenticated ConvertKit subscriber and has * subscribed to the ConvertKit Product or Tag. * * @since 2.1.0 * * @param string $content Post Content. * @return string Post Content with content restricted/not restricted */ public function maybe_restrict_content( $content ) { // Bail if the Restrict Content setting is not enabled on this Page. if ( ! $this->is_restricted_content() ) { return $content; } // Bail if the Page is being edited in a frontend Page Builder / Editor by a logged // in WordPress user who has the capability to edit the Page. // This ensures the User can view all content to edit it, instead of seeing the Restrict Content // view. if ( current_user_can( 'edit_post', get_the_ID() ) && WP_ConvertKit()->is_admin_or_frontend_editor() ) { return $content; } // Get resource type (Product or Tag) that the visitor must be subscribed against to access this content. $this->resource_type = $this->get_resource_type(); // Return the Post Content, unedited, if the Resource Type is false. if ( ! $this->resource_type ) { return $content; } // Get resource ID (Product ID or Tag ID) that the visitor must be subscribed against to access this content. $this->resource_id = $this->get_resource_id(); // Return the full Post Content, unedited, if the Resource ID is false, as this means // no restrict content setting has been defined for this Post. if ( ! $this->resource_id ) { return $content; } // Return the full Post Content, unedited, if the request is from a crawler. if ( $this->restrict_content_settings->permit_crawlers() && $this->is_crawler() ) { return $content; } // Return if this request is after the user entered their email address, // which means we're going through the authentication flow. if ( $this->in_authentication_flow() ) { return $this->restrict_content( $content ); } // Get the subscriber ID, either from the request or an existing cookie. $subscriber_id = $this->get_subscriber_id_from_request(); // If no subscriber ID exists, the visitor cannot view the content. if ( ! $subscriber_id ) { return $this->restrict_content( $content ); } // If the subscriber is not subscribed to the product, restrict the content. if ( ! $this->subscriber_has_access( $subscriber_id ) ) { // Show an error before the call to action, to tell the subscriber why they still cannot // view the content. switch ( $this->resource_type ) { case 'form': $message = $this->restrict_content_settings->get_by_key( 'no_access_text_form' ); break; case 'tag': $message = $this->restrict_content_settings->get_by_key( 'no_access_text_tag' ); break; case 'product': default: $message = $this->restrict_content_settings->get_by_key( 'no_access_text' ); break; } // Define error for output. $this->error = new WP_Error( 'convertkit_restrict_content_subscriber_no_access', esc_html( $message ) ); return $this->restrict_content( $content ); } // If here, the subscriber has subscribed to the product. // Show the full Post Content. return $content; } /** * Changes how WordPress' get_adjacent_post() function queries Pages, to determine what * the previous Page link is when using the Previous navigation block on a Page that * has the Restrict Content setting defined. * * By default, get_adjacent_post() will query by post_date, which we change to menu_order. * * @since 2.1.0 * * @param string $where The `WHERE` clause in the SQL. * @param bool $in_same_term Whether post should be in a same taxonomy term. * @param array $excluded_terms Array of excluded term IDs. * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true. * @param WP_Post $post WP_Post object. * @return string Modified `WHERE` clause */ public function maybe_change_previous_post_where_clause( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter // Bail if the Restrict Content setting is not enabled on this Page. if ( ! $this->is_restricted_content() ) { return $where; } // Bail if the Page doesn't match the current Page being viewed, or has no parent Page. if ( ! $this->has_parent_page( $post ) ) { return $where; } // Build replacement where statement. $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order < ' . $post->menu_order; // Replace existing where statement with new statement. $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) ); // Return. return $where; } /** * Changes how WordPress' get_adjacent_post() function queries Pages, to determine what * the next Page link is when using the Previous navigation block on a Page that * has the Restrict Content setting defined. * * By default, get_adjacent_post() will query by post_date, which we change to menu_order. * * @since 2.1.0 * * @param string $where The `WHERE` clause in the SQL. * @param bool $in_same_term Whether post should be in a same taxonomy term. * @param array $excluded_terms Array of excluded term IDs. * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true. * @param WP_Post $post WP_Post object. * @return string Modified `WHERE` clause */ public function maybe_change_next_post_where_clause( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter // Bail if the Restrict Content setting is not enabled on this Page. if ( ! $this->is_restricted_content() ) { return $where; } // Bail if the Page doesn't match the current Page being viewed, or has no parent Page. if ( ! $this->has_parent_page( $post ) ) { return $where; } // Build replacement where statement. $new_where = 'p.post_parent = ' . $post->post_parent . ' AND p.menu_order > ' . $post->menu_order; // Replace existing where statement with new statement. $where = 'WHERE ' . $new_where . ' ' . substr( $where, strpos( $where, 'AND p.post_type = \'' . $post->post_type . '\' ' ) ); // Return. return $where; } /** * Changes how WordPress' get_adjacent_post() function orders Pages, to determine what * the next and previous Page links are when using Previous / Next navigation blocks * on a Page that has the Restrict Content setting defined. * * By default, get_adjacent_post() will sort by Post Date, which we change to Page Order * (called menu_order in WordPress). * * @since 2.1.0 * * @param string $order_by SQL ORDER BY statement. * @param WP_Post $post WordPress Post. * @param string $order Order. * @return string Modified SQL ORDER BY statement. */ public function maybe_change_previous_next_post_order_by_clause( $order_by, $post, $order ) { // Bail if the Restrict Content setting is not enabled on this Page. if ( ! $this->is_restricted_content() ) { return $order_by; } // Bail if the Page doesn't match the current Page being viewed, or has no parent Page. if ( ! $this->has_parent_page( $post ) ) { return $order_by; } // Order by Page order (menu_order), highest to lowest, instead of post_date. return 'ORDER BY p.menu_order ' . $order . ' LIMIT 1'; } /** * Stores the given subscriber ID in the ck_subscriber_id cookie. * * @since 2.3.7 * * @param string|int $subscriber_id Subscriber ID (int if restrict by tag, signed subscriber id string if restrict by product). */ private function store_subscriber_id_in_cookie( $subscriber_id ) { // Store subscriber ID in cookie. // We don't need to use validate_and_store_subscriber_id() as we just validated the subscriber via authentication above. $subscriber = new ConvertKit_Subscriber(); $subscriber->set( $subscriber_id ); } /** * Redirects to the current URL, removing any query parameters (such as tokens), and appending * a ck-cache-bust query parameter to beat caching plugins. * * @since 2.3.7 * * @param int $post_id 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 // result in maybe_restrict_content() not showing an error message or permitting // access to the content. wp_safe_redirect( $this->get_url( $post_id, true ) ); exit; } /** * Returns the URL for the current request, excluding any query parameters. * * @since 2.1.0 * * @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 ) { // Get URL of Post. $url = get_permalink( $post_id ); // If no cache busting required, return the URL now. if ( ! $cache_bust ) { return $url; } // Append 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. return add_query_arg( array( 'ck-cache-bust' => microtime(), ), $url ); } /** * Determines if the request is for a WordPress Page that has the Restrict Content * setting defined. * * @since 2.1.0 * * @return bool */ private function is_restricted_content() { // Bail if not a singular Post Type. if ( ! is_singular() ) { return false; } // If the Plugin Access Token has not been configured, we can't determine the validity of this subscriber ID // or which resource(s) they have access to. if ( ! $this->settings->has_access_and_refresh_token() ) { return false; } // Get Post ID. $this->post_id = get_the_ID(); // Initialize Settings and Post Setting classes. $this->post_settings = new ConvertKit_Post( $this->post_id ); // Return whether the Post's settings are set to restrict content. return $this->post_settings->restrict_content_enabled(); } /** * Determines if the user entered a valid email address, and need to be prompted * to enter a code sent to their email address. * * @since 2.1.0 * * @return bool */ private function in_authentication_flow() { return ( $this->token !== false ); } /** * Checks if the given WordPress Page matches the Page ID viewed, and has a parent. * * @since 2.1.0 * * @param WP_Post $post WordPress Post. * @return bool Has parent page */ private function has_parent_page( $post ) { // Bail if the Page doesn't match the current Page being viewed. // This prevents us accidentally interfering with other previous / next link queries, which shouldn't happen // as we check if we're viewing a restricted content page above. if ( $post->ID !== $this->post_id ) { return false; } // Bail if the Page doesn't have a parent Page. // We don't want to modify the default sort behaviour in this instance. if ( $post->post_parent === 0 ) { return false; } return true; } /** * Get the Post's Restricted Content resource type. * * @since 2.1.0 * * @return bool|string Resource Type (product). */ private function get_resource_type() { // Initialize Post Setting classes. $this->post_settings = new ConvertKit_Post( $this->post_id ); // Get resource type. $resource_type = $this->post_settings->get_restrict_content_type(); /** * Define the ConvertKit Resource Type that the visitor must be subscribed against * to access this content, overriding the Post setting. * * Return false or an empty string to not restrict content. * * @since 2.1.0 * * @param string $resource_type Resource Type (product) * @param int $post_id Post ID */ $resource_type = apply_filters( 'convertkit_output_restrict_content_get_resource_type', $resource_type, $this->post_id ); // If resource type is blank, set it to false. if ( empty( $resource_type ) ) { $resource_type = false; } // Return. return $resource_type; } /** * Get the Post's Restricted Content resource ID. * * @since 2.1.0 * * @return int Resource ID (product ID). */ private function get_resource_id() { // Initialize Post Setting classes. $this->post_settings = new ConvertKit_Post( $this->post_id ); // Get resource ID. $resource_id = $this->post_settings->get_restrict_content_id(); /** * Define the ConvertKit Resource ID that the visitor must be subscribed against * to access this content, overriding the Post setting. * * Return 0 to not restrict content. * * @since 2.1.0 * * @param int $resource_id Resource ID * @param int $post_id Post ID */ $resource_id = apply_filters( 'convertkit_output_restrict_content_get_resource_id', $resource_id, $this->post_id ); // Return. return $resource_id; } /** * Queries the API to confirm whether the resource exists. * * @since 2.3.3 * * @return bool */ private function resource_exists() { switch ( $this->resource_type ) { case 'product': // Get Product. $products = new ConvertKit_Resource_Products( 'restrict_content' ); $product = $products->get_by_id( $this->resource_id ); // If the Product does not exist, return false. if ( ! $product ) { return false; } // Product exists in ConvertKit. return true; case 'form': // Get Form. $forms = new ConvertKit_Resource_Forms( 'restrict_content' ); $form = $forms->get_by_id( $this->resource_id ); // If the Form does not exist, return false. if ( ! $form ) { return false; } // Form exists in ConvertKit. return true; case 'tag': // Get Tag. $tags = new ConvertKit_Resource_Tags( 'restrict_content' ); $tag = $tags->get_by_id( $this->resource_id ); // If the Tag does not exist, return false. if ( ! $tag ) { return false; } // Tag exists in ConvertKit. return true; default: return false; } } /** * Determines if the given subscriber has an active subscription to * the given resource and its ID. * * @since 2.1.0 * * @param string|int $subscriber_id Signed Subscriber ID or Subscriber ID. * @return bool Can view restricted content */ private function subscriber_has_access( $subscriber_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter switch ( $this->resource_type ) { case 'product': return $this->subscriber_has_access_to_product_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) ); case 'form': return $this->subscriber_has_access_to_form_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) ); case 'tag': return $this->subscriber_has_access_to_tag_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) ); } // If here, the subscriber does not have access. return false; } /** * Determines if the given signed subscriber ID has an active subscription to * the given product. * * @since 2.7.1 * * @param string $signed_subscriber_id Signed Subscriber ID. * @param int $product_id Product ID. * @return bool Has access to product */ private function subscriber_has_access_to_product_by_signed_subscriber_id( $signed_subscriber_id, $product_id ) { // Get products that the subscriber has access to. $result = $this->api->profile( $signed_subscriber_id ); // If an error occurred, the subscriber ID is invalid. if ( is_wp_error( $result ) ) { return false; } // If no products exist, there's no access. if ( ! $result['products'] || ! count( $result['products'] ) ) { return false; } // Return if the subscriber is subscribed to the product or not. return in_array( $product_id, $result['products'], true ); } /** * Determines if the given signed subscriber ID has an active subscription to * the given form. * * @since 2.7.3 * * @param string $signed_subscriber_id Signed Subscriber ID. * @param int $form_id Form ID. * @return bool Has access to form */ private function subscriber_has_access_to_form_by_signed_subscriber_id( $signed_subscriber_id, $form_id ) { // Get products that the subscriber has access to. $result = $this->api->profile( $signed_subscriber_id ); // If an error occurred, the subscriber ID is invalid. if ( is_wp_error( $result ) ) { return false; } // If no forms exist, there's no access. if ( ! $result['forms'] || ! count( $result['forms'] ) ) { return false; } // Return if the subscriber is subscribed to the form or not. return in_array( $form_id, $result['forms'], true ); } /** * Determines if the given signed subscriber ID has an active subscription to * the given tag. * * @since 2.7.1 * * @param string $signed_subscriber_id Signed Subscriber ID. * @param int $tag_id Tag ID. * @return bool Has access to tag */ private function subscriber_has_access_to_tag_by_signed_subscriber_id( $signed_subscriber_id, $tag_id ) { // Get products that the subscriber has access to. $result = $this->api->profile( $signed_subscriber_id ); // If an error occurred, the subscriber ID is invalid. if ( is_wp_error( $result ) ) { return false; } // If no tags exist, there's no access. if ( ! $result['tags'] || ! count( $result['tags'] ) ) { return false; } // Return if the subscriber is subscribed to the tag or not. return in_array( $tag_id, $result['tags'], true ); } /** * Gets the subscriber ID from the request (either the cookie or the URL). * * @since 2.1.0 * * @return int|string Subscriber ID or Signed ID */ public function get_subscriber_id_from_request() { // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID. $subscriber = new ConvertKit_Subscriber(); $subscriber_id = $subscriber->get_subscriber_id(); // If an error occurred, the subscriber ID in the request/cookie is not a valid subscriber. if ( is_wp_error( $subscriber_id ) ) { return 0; } return $subscriber_id; } /** * Restrict the given Post Content by showing a preview of the content, and appending * the call to action to subscribe or authenticate. * * @since 2.1.0 * * @param string $content Post Content. * @return string Post Content preview with call to action */ private function restrict_content( $content ) { // Check that the resource exists before restricting the content. // This handles cases where e.g. a Tag or Product has been deleted in ConvertKit, // but the Page / Post still references the (now deleted) resource to restrict content with // under the 'Member Content' setting. if ( ! $this->resource_exists() ) { // Return the full Post Content, as we can't restrict it to a Product or Tag that no longer exists. return $content; } // Fetch the content preview. $content_preview = $this->get_content_preview( $content ); /** * Define the output for the content preview when the visitor is not * an authenticated subscriber. * * @since 2.4.1 * * @param string $content_preview Content preview. * @param int $post_id Post ID. */ $content_preview = apply_filters( 'convertkit_output_restrict_content_content_preview', $content_preview, $this->post_id ); // Fetch the call to action. $call_to_action = $this->get_call_to_action( $this->post_id ); /** * Define the output for the call to action, displayed below the content preview, * when the visitor is not an authenticated subscriber. * * @since 2.4.1 * * @param string $call_to_action Call to Action. * @param int $post_id Post ID. */ $call_to_action = apply_filters( 'convertkit_output_restrict_content_call_to_action', $call_to_action, $this->post_id ); // Fetch container CSS classes. $container_css_classes = explode( ' ', $this->restrict_content_settings->get_by_key( 'container_css_classes' ) ); /** * Define the container CSS classes to wrap the content preview and call to action within. * * @since 3.1.4 * * @param array $container_css_classes Container CSS classes. * @param int $post_id Post ID. */ $container_css_classes = apply_filters( 'convertkit_output_restrict_content_container_css_classes', $container_css_classes, $this->post_id ); // Remove empty CSS classes. $container_css_classes = array_filter( $container_css_classes ); // If container CSS classes are set, return the content preview and call to action wrapped in the container. if ( count( $container_css_classes ) ) { return '