'POST', 'callback' => array($this, 'unlock_content'), 'permission_callback' => '__return_true', 'args' => array( 'password' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => function ( $value ) { return wp_unslash( $value ); }, ), 'type' => array( 'required' => true, 'type' => 'string', 'enum' => array( 'password', 'passwords', 'password_list', 'password_lists' ), 'sanitize_callback' => 'sanitize_text_field', ), 'post_id' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'area_id' => array( 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'block_id' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'list_id' => array( 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'lists' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'redirect' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', ), 'protection' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'acf' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'term_id' => array( 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'post_type' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_key', ), ), ) ); // Hash password endpoint. register_rest_route( 'passster/v1', '/hash', array( 'methods' => 'POST', 'callback' => array($this, 'hash_password'), 'permission_callback' => '__return_true', 'args' => array( 'password' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => function ( $value ) { return wp_unslash( $value ); }, ), 'post_id' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), ) ); // reCAPTCHA/hCaptcha validation endpoint. register_rest_route( 'passster/v1', '/captcha', array( 'methods' => 'POST', 'callback' => array($this, 'validate_captcha'), 'permission_callback' => '__return_true', 'args' => array( 'token' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'type' => array( 'required' => true, 'type' => 'string', 'enum' => array( 'recaptcha_v2', 'recaptcha_v3', 'hcaptcha', 'turnstile' ), 'sanitize_callback' => 'sanitize_text_field', ), 'post_id' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'area_id' => array( 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'redirect' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', ), 'protection' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'captcha_id' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), ), ) ); // Logout endpoint (for concurrent sessions). register_rest_route( 'passster/v1', '/logout', array( 'methods' => 'POST', 'callback' => array($this, 'handle_logout'), 'permission_callback' => '__return_true', ) ); } /** * Unlock content endpoint. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response|\WP_Error */ public function unlock_content( \WP_REST_Request $request ) { $options = get_option( 'passster', array() ); $input = $request->get_param( 'password' ); $type = $request->get_param( 'type' ); $post_id = $request->get_param( 'post_id' ); $area_id = $request->get_param( 'area_id' ); $block_id = $request->get_param( 'block_id' ); $list_id = $request->get_param( 'list_id' ); $lists = $request->get_param( 'lists' ); $redirect = $request->get_param( 'redirect' ); $protection = $request->get_param( 'protection' ); $acf = $request->get_param( 'acf' ); $term_id = absint( $request->get_param( 'term_id' ) ); $post_type_param = sanitize_key( (string) $request->get_param( 'post_type' ) ); // Default error response. $error_message = $options['error'] ?? __( 'Invalid password.', 'content-protector' ); $remove_spaces = apply_filters( 'passster_remove_spaces_from_list', true ); if ( empty( $protection ) ) { $protection = false; } // Category archive protection: term_id is passed directly from the form. if ( $term_id > 0 ) { $result = $this->validate_category_unlock( $input, $type, $term_id, null, '', $redirect, $options, $remove_spaces ); if ( $result['valid'] ) { do_action( 'passster_validation_success', $input ); PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); $term_redirect = get_term_meta( $term_id, 'passster_redirect_url', true ); if ( !empty( $term_redirect ) ) { return new \WP_REST_Response(array( 'success' => true, 'redirect' => esc_url_raw( $term_redirect ), ), 200); } return new \WP_REST_Response(array( 'success' => true, 'requires_reload' => true, ), 200); } return new \WP_REST_Response(array( 'success' => false, 'error' => $error_message, ), 200); } // Post type archive protection: post_type is passed directly from the form // (no single post ID exists to key an unlock request off of on an archive page). if ( !empty( $post_type_param ) && class_exists( 'passster\\PS_Post_Type_Lock' ) && PS_Post_Type_Lock::is_post_type_protected( $post_type_param ) ) { $post_type_config = PS_Post_Type_Lock::get_post_type_config( $post_type_param ); $result = $this->validate_post_type_unlock( $input, $type, $post_type_config, null, '', $remove_spaces ); if ( $result['valid'] ) { do_action( 'passster_validation_success', $input ); PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); if ( !empty( $post_type_config['passster_activate_misc_settings'] ) && !empty( $post_type_config['passster_redirect_url'] ) ) { return new \WP_REST_Response(array( 'success' => true, 'redirect' => esc_url_raw( $post_type_config['passster_redirect_url'] ), ), 200); } return new \WP_REST_Response(array( 'success' => true, 'requires_reload' => true, ), 200); } return new \WP_REST_Response(array( 'success' => false, 'error' => $error_message, ), 200); } // Parent page protection inheritance. $parent_id = wp_get_post_parent_id( $post_id ); if ( $parent_id ) { $activate_protection = get_post_meta( $parent_id, 'passster_activate_protection', true ); $children_protection = get_post_meta( $parent_id, 'passster_protect_child_pages', true ); if ( $activate_protection && $children_protection ) { $post_id = $parent_id; } } // Prepare content. $post = get_post( $post_id ); $content = ''; if ( $post ) { $content = apply_filters( 'passster_compatibility_actions', $post->post_content, $post_id ); } // ACF field support. if ( !empty( $acf ) ) { $content = \get_field( $acf, $post_id ); } // Category/taxonomy protection: if the post itself has no protection, // check if it belongs to a protected category and validate against term meta. $post_protection = get_post_meta( $post_id, 'passster_activate_protection', true ); $term_data = null; if ( !$post_protection && 'full' === $protection ) { if ( class_exists( 'passster\\PS_Category_Lock' ) ) { $term_data = PS_Category_Lock::get_instance()->get_protected_term_for_post( $post_id ); } if ( $term_data ) { // Use term redirect if no redirect was sent from the frontend. if ( empty( $redirect ) ) { $redirect = get_term_meta( $term_data['term_id'], 'passster_redirect_url', true ); } $result = $this->validate_category_unlock( $input, $type, $term_data['term_id'], $post, $content, $redirect, $options, $remove_spaces ); if ( $result['valid'] ) { $response_data = array( 'success' => true, ); if ( !empty( $redirect ) ) { $response_data['redirect'] = $redirect; } else { // For category/taxonomy protection we can't return the full archive HTML via REST. // Redirect to the term archive instead. $term = get_term( $term_data['term_id'] ); $term_link = ( $term && !is_wp_error( $term ) ? get_term_link( $term ) : '' ); $response_data['redirect'] = ( $term_link ?: wp_get_referer() ); } do_action( 'passsster_track_record', $post_id, $input, 'full' ); do_action( 'passster_validation_success', $input ); PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); return new \WP_REST_Response($response_data, 200); } // Category protection exists but validation failed. return new \WP_REST_Response(array( 'success' => false, 'error' => $error_message, ), 200); } // Post type level protection: applies only when the post has neither its // own protection nor a protected category/term (both take precedence). if ( class_exists( 'passster\\PS_Post_Type_Lock' ) ) { $post_type = get_post_type( $post_id ); if ( $post_type && PS_Post_Type_Lock::is_post_type_protected( $post_type ) ) { $config = PS_Post_Type_Lock::get_post_type_config( $post_type ); if ( empty( $redirect ) && !empty( $config['passster_activate_misc_settings'] ) && !empty( $config['passster_redirect_url'] ) ) { $redirect = $config['passster_redirect_url']; } $result = $this->validate_post_type_unlock( $input, $type, $config, $post, $content, $remove_spaces ); if ( $result['valid'] ) { $response_data = array( 'success' => true, ); if ( !empty( $redirect ) ) { $response_data['redirect'] = $redirect; } elseif ( $this->content_uses_page_builder( $result['content'], $post_id ) ) { $response_data['requires_reload'] = true; } else { $response_data['content'] = apply_filters( 'the_content', str_replace( '{post-id}', $post_id, $result['content'] ) ); } do_action( 'passsster_track_record', $post_id, $input, 'full' ); do_action( 'passster_validation_success', $input ); PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); return new \WP_REST_Response($response_data, 200); } // Post type protection exists but validation failed. return new \WP_REST_Response(array( 'success' => false, 'error' => $error_message, ), 200); } } } // Validate based on type. $valid = false; $result_content = ''; // Block-based protection (Gutenberg blocks). if ( !empty( $block_id ) ) { switch ( $type ) { case 'password': $result = $this->get_block_password( $post_id, $block_id ); if ( !empty( $result['password'] ) && $input === $result['password'] ) { $valid = true; $result_content = $result['content']; } break; case 'passwords': $result = $this->get_block_passwords( $post_id, $block_id ); if ( !empty( $result['passwords'] ) ) { $passwords_str = $result['passwords']; if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); if ( in_array( $input, $passwords, true ) ) { $valid = true; $result_content = $result['content']; } } break; } } elseif ( \passster_fs()->is_plan_or_trial__premium_only( 'pro' ) ) { // PRO: shortcode/area/full protection. switch ( $type ) { case 'password': $result = $this->validate_password_pro( $input, $post_id, $area_id, $protection, $post, $content, $redirect, $options ); $valid = $result['valid']; $result_content = $result['content']; break; case 'passwords': $result = $this->validate_passwords_pro( $input, $post_id, $area_id, $protection, $post, $content, $redirect, $options, $remove_spaces ); $valid = $result['valid']; $result_content = $result['content']; break; case 'password_list': $result = $this->validate_password_list_pro( $input, $list_id, $post_id, $area_id, $protection, $post, $content, $redirect, $options, $remove_spaces ); $valid = $result['valid']; $result_content = $result['content']; break; case 'password_lists': $result = $this->validate_password_lists_pro( $input, $lists, $post_id, $area_id, $protection, $post, $content, $redirect, $options, $remove_spaces ); $valid = $result['valid']; $result_content = $result['content']; break; } } else { // Free version: only password type. if ( 'password' === $type ) { $result = $this->validate_password_free( $input, $post_id, $area_id, $protection, $post, $content, $redirect, $options ); $valid = $result['valid']; $result_content = $result['content']; } } if ( !$valid ) { return new \WP_REST_Response(array( 'success' => false, 'error' => $error_message, ), 200); } // Success - prepare response. $response_data = array( 'success' => true, ); if ( !empty( $redirect ) ) { $response_data['redirect'] = $redirect; } else { // Page builders (Divi, WPBakery, Elementor, etc.) require their full frontend // context (scripts, styles, theme builder) to render correctly. In a REST API // response this is not available, so signal the client to do a full page reload // instead of attempting inline content injection. if ( empty( $block_id ) && $this->content_uses_page_builder( $result_content, $post_id ) ) { $response_data['requires_reload'] = true; } else { // Block content is already rendered via render_block(); apply the_content filter only for shortcode/area protection. $response_data['content'] = ( empty( $block_id ) ? apply_filters( 'the_content', str_replace( '{post-id}', $post_id, $result_content ) ) : $result_content ); } } // Determine source for tracking. $source = 'shortcode'; if ( !empty( $block_id ) ) { $source = 'block'; } elseif ( 'full' === $protection ) { $source = 'full'; } elseif ( !empty( $area_id ) || 'area' === $protection ) { $source = 'area'; } do_action( 'passsster_track_record', $post_id, $input, $source ); do_action( 'passster_validation_success', $input ); PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); return new \WP_REST_Response($response_data, 200); } /** * Validate single password (PRO). * * @param string $input User input. * @param int $post_id Post ID. * @param int $area_id Area ID. * @param string|bool $protection Protection type. * @param \WP_Post $post Post object. * @param string $content Post content. * @param string $redirect Redirect URL. * @param array $options Plugin options. * @return array */ private function validate_password_pro( $input, $post_id, $area_id, $protection, $post, $content, $redirect, $options ) { switch ( $protection ) { case 'full': $password = get_post_meta( $post_id, 'passster_password', true ); if ( !empty( $password ) && $input === $password ) { if ( $post && 'publish' === $post->post_status ) { return array( 'valid' => true, 'content' => $content, ); } } break; case 'area': if ( !empty( $area_id ) ) { $password = get_post_meta( $area_id, 'passster_password', true ); if ( !empty( $password ) && $input === $password ) { $area = get_post( $area_id ); if ( $area && 'protected_areas' === $area->post_type && 'publish' === $area->post_status ) { $area_content = apply_filters( 'passster_compatibility_actions', $area->post_content, $area_id ); return array( 'valid' => true, 'content' => $area_content, ); } } } break; default: $shortcode_content = apply_filters( 'passster_compatibility_actions', PS_Helper::get_shortcode_content( $content, $input ) ); if ( !empty( $shortcode_content ) ) { return array( 'valid' => true, 'content' => $shortcode_content, ); } elseif ( !empty( $options['toggle_ajax'] ) && 'on' === $options['toggle_ajax'] ) { return array( 'valid' => true, 'content' => '', ); } break; } return array( 'valid' => false, 'content' => '', ); } /** * Validate multiple passwords (PRO). * * @param string $input User input. * @param int $post_id Post ID. * @param int $area_id Area ID. * @param string|bool $protection Protection type. * @param \WP_Post $post Post object. * @param string $content Post content. * @param string $redirect Redirect URL. * @param array $options Plugin options. * @param bool $remove_spaces Whether to remove spaces. * @return array */ private function validate_passwords_pro( $input, $post_id, $area_id, $protection, $post, $content, $redirect, $options, $remove_spaces ) { switch ( $protection ) { case 'full': $passwords_str = get_post_meta( $post_id, 'passster_passwords', true ); if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); if ( !empty( $passwords ) && in_array( $input, $passwords, true ) ) { if ( $post && 'publish' === $post->post_status ) { return array( 'valid' => true, 'content' => $content, ); } } break; case 'area': if ( !empty( $area_id ) ) { $passwords_str = get_post_meta( $area_id, 'passster_passwords', true ); if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); if ( !empty( $passwords ) && in_array( $input, $passwords, true ) ) { $area = get_post( $area_id ); if ( $area && 'protected_areas' === $area->post_type && 'publish' === $area->post_status ) { $area_content = apply_filters( 'passster_compatibility_actions', $area->post_content, $area_id ); return array( 'valid' => true, 'content' => $area_content, ); } } } break; default: $shortcode_content = apply_filters( 'passster_compatibility_actions', PS_Helper::get_shortcode_content( $content, $input ) ); if ( !empty( $shortcode_content ) ) { return array( 'valid' => true, 'content' => $shortcode_content, ); } elseif ( !empty( $options['toggle_ajax'] ) && 'on' === $options['toggle_ajax'] ) { return array( 'valid' => true, 'content' => '', ); } break; } return array( 'valid' => false, 'content' => '', ); } /** * Validate password from single list (PRO). * * @param string $input User input. * @param int $list_id Password list ID. * @param int $post_id Post ID. * @param int $area_id Area ID. * @param string|bool $protection Protection type. * @param \WP_Post $post Post object. * @param string $content Post content. * @param string $redirect Redirect URL. * @param array $options Plugin options. * @param bool $remove_spaces Whether to remove spaces. * @return array */ private function validate_password_list_pro( $input, $list_id, $post_id, $area_id, $protection, $post, $content, $redirect, $options, $remove_spaces ) { if ( empty( $list_id ) ) { return array( 'valid' => false, 'content' => '', ); } $passwords_str = get_post_meta( $list_id, 'passster_passwords', true ); if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); return $this->validate_single_list( $input, $passwords, $list_id, $post_id, $area_id, $protection, $post, $content, $options ); } /** * Validate password from multiple lists (PRO). * * @param string $input User input. * @param string $lists Pipe-separated list IDs. * @param int $post_id Post ID. * @param int $area_id Area ID. * @param string|bool $protection Protection type. * @param \WP_Post $post Post object. * @param string $content Post content. * @param string $redirect Redirect URL. * @param array $options Plugin options. * @param bool $remove_spaces Whether to remove spaces. * @return array */ private function validate_password_lists_pro( $input, $lists, $post_id, $area_id, $protection, $post, $content, $redirect, $options, $remove_spaces ) { if ( empty( $lists ) ) { return array( 'valid' => false, 'content' => '', ); } $password_list_ids = explode( '|', $lists ); foreach ( $password_list_ids as $pid ) { $passwords_str = get_post_meta( $pid, 'passster_passwords', true ); if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); $result = $this->validate_single_list( $input, $passwords, $pid, $post_id, $area_id, $protection, $post, $content, $options ); if ( $result['valid'] ) { return $result; } } return array( 'valid' => false, 'content' => '', ); } /** * Validate input against a single password list with protection type handling. * * @param string $input User input. * @param array $passwords Passwords array. * @param int $list_id Password list ID. * @param int $post_id Post ID. * @param int $area_id Area ID. * @param string|bool $protection Protection type. * @param \WP_Post $post Post object. * @param string $content Post content. * @param array $options Plugin options. * @return array */ private function validate_single_list( $input, $passwords, $list_id, $post_id, $area_id, $protection, $post, $content, $options ) { $valid = false; $result_content = ''; switch ( $protection ) { case 'full': if ( !empty( $passwords ) && in_array( $input, $passwords, true ) ) { if ( $post && 'publish' === $post->post_status ) { $valid = true; $result_content = $content; } do_action( 'passster_validation_success_list', $input, $list_id, $post_id ); PS_Conditional::maybe_expire_password_from_list__premium_only( $input, $passwords, $list_id ); } break; case 'area': if ( !empty( $area_id ) && !empty( $passwords ) && in_array( $input, $passwords, true ) ) { $area = get_post( $area_id ); if ( $area && 'protected_areas' === $area->post_type && 'publish' === $area->post_status ) { $valid = true; $result_content = apply_filters( 'passster_compatibility_actions', $area->post_content, $area_id ); } do_action( 'passster_validation_success_list', $input, $list_id, $area_id ); PS_Conditional::maybe_expire_password_from_list__premium_only( $input, $passwords, $list_id ); } break; default: if ( in_array( $input, $passwords, true ) ) { $shortcode_content = apply_filters( 'passster_compatibility_actions', PS_Helper::get_shortcode_content( $content, $list_id ) ); if ( !empty( $shortcode_content ) ) { $valid = true; $result_content = $shortcode_content; } elseif ( !empty( $options['toggle_ajax'] ) && 'on' === $options['toggle_ajax'] ) { $valid = true; } do_action( 'passster_validation_success_list', $input, $list_id, $post_id ); PS_Conditional::maybe_expire_password_from_list__premium_only( $input, $passwords, $list_id ); } break; } return array( 'valid' => $valid, 'content' => $result_content, ); } /** * Validate single password (Free version). * * @param string $input User input. * @param int $post_id Post ID. * @param int $area_id Area ID. * @param string|bool $protection Protection type. * @param \WP_Post $post Post object. * @param string $content Post content. * @param string $redirect Redirect URL. * @param array $options Plugin options. * @return array */ private function validate_password_free( $input, $post_id, $area_id, $protection, $post, $content, $redirect, $options ) { switch ( $protection ) { case 'full': $password = get_post_meta( $post_id, 'passster_password', true ); if ( !empty( $password ) && $input === $password ) { if ( $post && 'publish' === $post->post_status ) { return array( 'valid' => true, 'content' => $content, ); } } break; case 'area': if ( !empty( $area_id ) ) { $password = get_post_meta( $area_id, 'passster_password', true ); if ( !empty( $password ) && $input === $password ) { $area = get_post( $area_id ); if ( $area && 'protected_areas' === $area->post_type && 'publish' === $area->post_status ) { $area_content = apply_filters( 'passster_compatibility_actions', $area->post_content, $area_id ); return array( 'valid' => true, 'content' => $area_content, ); } } } break; default: $shortcode_content = apply_filters( 'passster_compatibility_actions', PS_Helper::get_shortcode_content( $content, $input ) ); if ( !empty( $shortcode_content ) ) { return array( 'valid' => true, 'content' => $shortcode_content, ); } elseif ( !empty( $options['toggle_ajax'] ) && 'on' === $options['toggle_ajax'] ) { return array( 'valid' => true, 'content' => '', ); } break; } return array( 'valid' => false, 'content' => '', ); } /** * Get password from block attributes. * * @param int $post_id Post ID. * @param string $block_id Block ID. * @return array */ /** * Validate unlock for category-protected posts (password stored in term meta). * * @param string $input User input. * @param string $type Protection type (password, passwords, password_list, password_lists). * @param int $term_id Protected term ID. * @param \WP_Post $post Post object. * @param string $content Post content. * @param string $redirect Redirect URL. * @param array $options Plugin options. * @param bool $remove_spaces Whether to remove spaces from password lists. * @return array */ private function validate_category_unlock( $input, $type, $term_id, $post, $content, $redirect, $options, $remove_spaces ) { switch ( $type ) { case 'password': $password = get_term_meta( $term_id, 'passster_password', true ); if ( !empty( $password ) && $input === $password ) { return array( 'valid' => true, 'content' => $content, ); } break; case 'passwords': break; case 'password_list': break; case 'password_lists': break; } return array( 'valid' => false, 'content' => '', ); } /** * Validate unlock for posts protected wholesale via post type level protection * (password stored in the post type's configuration, not on the post itself). * * @param string $input User input. * @param string $type Protection type (password, passwords, password_list, password_lists). * @param array $config Post type configuration. * @param \WP_Post $post Post object. * @param string $content Post content. * @param bool $remove_spaces Whether to remove spaces from password lists. * @return array */ private function validate_post_type_unlock( $input, $type, array $config, $post, $content, $remove_spaces ) { // $post is null for archive-level unlocks, where there's no single post to check. if ( $post && 'publish' !== $post->post_status ) { return array( 'valid' => false, 'content' => '', ); } switch ( $type ) { case 'password': if ( !empty( $config['passster_password'] ) && $input === $config['passster_password'] ) { return array( 'valid' => true, 'content' => $content, ); } break; case 'passwords': $passwords_str = $config['passster_passwords']; if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); if ( !empty( $passwords ) && in_array( $input, $passwords, true ) ) { return array( 'valid' => true, 'content' => $content, ); } break; case 'password_list': $list_id = $config['passster_password_list']; if ( !empty( $list_id ) ) { $passwords_str = get_post_meta( $list_id, 'passster_passwords', true ); if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); if ( in_array( $input, $passwords, true ) ) { do_action( 'passster_validation_success_list', $input, $list_id, ( $post ? $post->ID : 0 ) ); PS_Conditional::maybe_expire_password_from_list__premium_only( $input, $passwords, $list_id ); return array( 'valid' => true, 'content' => $content, ); } } break; case 'password_lists': foreach ( (array) $config['passster_password_lists'] as $list_id ) { $passwords_str = get_post_meta( $list_id, 'passster_passwords', true ); if ( $remove_spaces ) { $passwords_str = str_replace( ' ', '', $passwords_str ); } $passwords = explode( ',', $passwords_str ); if ( in_array( $input, $passwords, true ) ) { do_action( 'passster_validation_success_list', $input, $list_id, ( $post ? $post->ID : 0 ) ); PS_Conditional::maybe_expire_password_from_list__premium_only( $input, $passwords, $list_id ); return array( 'valid' => true, 'content' => $content, ); } } break; } return array( 'valid' => false, 'content' => '', ); } private function get_block_password( $post_id, $block_id ) { $post = get_post( $post_id ); if ( !$post ) { return array( 'password' => '', 'content' => '', ); } $blocks = parse_blocks( $post->post_content ); $block = $this->find_block_by_id( $blocks, $block_id ); if ( !$block ) { return array( 'password' => '', 'content' => '', ); } $password = $block['attrs']['password'] ?? ''; $content = $this->render_inner_blocks( $block ); return array( 'password' => $password, 'content' => $content, ); } /** * Get passwords from block attributes. * * @param int $post_id Post ID. * @param string $block_id Block ID. * @return array */ private function get_block_passwords( $post_id, $block_id ) { $post = get_post( $post_id ); if ( !$post ) { return array( 'passwords' => '', 'content' => '', ); } $blocks = parse_blocks( $post->post_content ); $block = $this->find_block_by_id( $blocks, $block_id ); if ( !$block ) { return array( 'passwords' => '', 'content' => '', ); } $passwords = $block['attrs']['passwords'] ?? ''; $content = $this->render_inner_blocks( $block ); return array( 'passwords' => $passwords, 'content' => $content, ); } /** * Find block by ID recursively. * * @param array $blocks Blocks array. * @param string $block_id Block ID. * @return array|null */ private function find_block_by_id( $blocks, $block_id ) { foreach ( $blocks as $block ) { if ( 'passster/content-lock' === $block['blockName'] ) { if ( isset( $block['attrs']['blockId'] ) && $block['attrs']['blockId'] === $block_id ) { return $block; } } // Check inner blocks. if ( !empty( $block['innerBlocks'] ) ) { $found = $this->find_block_by_id( $block['innerBlocks'], $block_id ); if ( $found ) { return $found; } } } return null; } /** * Check if content uses a page builder that requires a full page reload to render. * * @param string $content Post content. * @param int $post_id Post ID. * @return bool */ private function content_uses_page_builder( $content, $post_id = 0 ) { // Divi Builder shortcodes. if ( $content && strpos( $content, '[et_pb_' ) !== false ) { return true; } // WPBakery Page Builder. if ( $content && strpos( $content, '[vc_row' ) !== false ) { return true; } // Fusion Builder (Avada). if ( $content && strpos( $content, '[fusion_builder' ) !== false ) { return true; } // Elementor stores its layout in post meta, not in post_content. if ( $post_id && 'builder' === get_post_meta( $post_id, '_elementor_edit_mode', true ) ) { return true; } return false; } /** * Render inner blocks content. * * @param array $block Block data. * @return string */ private function render_inner_blocks( $block ) { if ( empty( $block['innerBlocks'] ) ) { return ''; } $content = ''; foreach ( $block['innerBlocks'] as $inner_block ) { $content .= render_block( $inner_block ); } return $content; } /** * Hash password endpoint. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public function hash_password( \WP_REST_Request $request ) { $password = $request->get_param( 'password' ); $post_id = $request->get_param( 'post_id' ); $real_password = get_post_meta( $post_id, 'passster_password', true ); if ( empty( $real_password ) || !hash_equals( $real_password, $password ) ) { return new \WP_REST_Response(array( 'success' => false, 'error' => __( 'Invalid password.', 'content-protector' ), ), 200); } $hashed = hash_hmac( 'sha256', $password, get_option( 'passster_secure_key' ) ); PS_Helper::remember_unlock( $hashed ); return new \WP_REST_Response(array( 'success' => true, 'hash' => $hashed, ), 200); } /** * Validate captcha endpoint. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public function validate_captcha( \WP_REST_Request $request ) { $options = get_option( 'passster', array() ); $token = $request->get_param( 'token' ); $type = $request->get_param( 'type' ); $post_id = $request->get_param( 'post_id' ); $area_id = $request->get_param( 'area_id' ); $redirect = $request->get_param( 'redirect' ); $protection = $request->get_param( 'protection' ); $captcha_id = $request->get_param( 'captcha_id' ); if ( empty( $protection ) ) { $protection = false; } if ( empty( $captcha_id ) ) { // Default captcha ID based on type. $captcha_id = str_replace( array('_v2', '_v3'), '', $type ); } $error_message = $options['error'] ?? __( 'Captcha validation failed.', 'content-protector' ); // Validate captcha based on type. $valid = false; switch ( $type ) { case 'recaptcha_v2': case 'recaptcha_v3': $valid = $this->verify_recaptcha( $token, $options, $type ); break; case 'hcaptcha': $valid = $this->verify_hcaptcha( $token, $options ); break; case 'turnstile': $valid = $this->verify_turnstile( $token, $options ); break; } if ( !$valid ) { return new \WP_REST_Response(array( 'success' => false, 'error' => $error_message, ), 200); } // Get content based on protection type (mirrors AJAX logic). $content = ''; $requires_reload = false; $captcha_protection_types = array('recaptcha', 'turnstile'); if ( 'full' !== $protection ) { if ( 'area' === $protection ) { if ( !empty( $area_id ) ) { $area = get_post( $area_id ); $area_protection_type = get_post_meta( $area_id, 'passster_protection_type', true ); if ( $area && 'protected_areas' === $area->post_type && 'publish' === $area->post_status && in_array( $area_protection_type, $captcha_protection_types, true ) ) { $content = apply_filters( 'passster_compatibility_actions', $area->post_content, $area_id ); } } } else { // Shortcode protection - extract content using captcha_id. $post = get_post( $post_id ); if ( $post ) { $post_content = apply_filters( 'passster_compatibility_actions', $post->post_content, $post_id ); $content = apply_filters( 'passster_compatibility_actions', PS_Helper::get_shortcode_content( $post_content, $captcha_id, 'captcha' ) ); } } } else { $post = get_post( $post_id ); $post_protection = get_post_meta( $post_id, 'passster_activate_protection', true ); $post_protection_type = get_post_meta( $post_id, 'passster_protection_type', true ); if ( $post && 'publish' === $post->post_status && $post_protection && in_array( $post_protection_type, $captcha_protection_types, true ) ) { $content = apply_filters( 'passster_compatibility_actions', $post->post_content, $post_id ); } elseif ( $post && 'publish' === $post->post_status && !$post_protection && class_exists( 'passster\\PS_Category_Lock' ) && PS_Category_Lock::get_instance()->get_protected_term_for_post( $post_id ) ) { $requires_reload = true; } } // Track record. $source = 'shortcode'; if ( 'full' === $protection ) { $source = 'full'; } elseif ( !empty( $area_id ) || 'area' === $protection ) { $source = 'area'; } do_action( 'passsster_track_record', $post_id, $captcha_id, $source ); // Success response. $response_data = array( 'success' => true, ); PS_Helper::remember_unlock( hash_hmac( 'sha256', 'captcha-verified', get_option( 'passster_secure_key' ) ) ); if ( !empty( $redirect ) ) { $response_data['redirect'] = $redirect; } elseif ( $requires_reload ) { $response_data['requires_reload'] = true; } else { $response_data['content'] = $content; } return new \WP_REST_Response($response_data, 200); } /** * Verify reCAPTCHA token. * * @param string $token Token from client. * @param array $options Plugin options. * @param string $type recaptcha_v2 or recaptcha_v3. * @return bool */ private function verify_recaptcha( $token, $options, $type ) { $secret = $options['recaptcha_secret'] ?? ''; if ( empty( $secret ) ) { return false; } $response = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', array( 'body' => array( 'secret' => $secret, 'response' => $token, ), ) ); if ( is_wp_error( $response ) ) { return false; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 'recaptcha_v2' === $type ) { return !empty( $body['success'] ); } // v3 - check score. return !empty( $body['success'] ) && isset( $body['score'] ) && $body['score'] >= 0.5 && isset( $body['action'] ) && 'validate_input' === $body['action']; } /** * Verify hCaptcha token. * * @param string $token Token from client. * @param array $options Plugin options. * @return bool */ private function verify_hcaptcha( $token, $options ) { $secret = $options['recaptcha_secret'] ?? ''; if ( empty( $secret ) ) { return false; } $response = wp_remote_post( 'https://hcaptcha.com/siteverify', array( 'body' => array( 'secret' => $secret, 'response' => $token, ), ) ); if ( is_wp_error( $response ) ) { return false; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return !empty( $body['success'] ); } /** * Verify Turnstile token. * * @param string $token Token from client. * @param array $options Plugin options. * @return bool */ private function verify_turnstile( $token, $options ) { $secret = $options['turnstile_secret'] ?? ''; if ( empty( $secret ) ) { return false; } $response = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', array( 'body' => array( 'secret' => $secret, 'response' => $token, ), ) ); if ( is_wp_error( $response ) ) { return false; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return !empty( $body['success'] ); } /** * Handle logout endpoint. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public function handle_logout( \WP_REST_Request $request ) { // Clear concurrent session if PRO and class exists. if ( \passster_fs()->is_plan_or_trial__premium_only( 'pro' ) && class_exists( 'passster\\PS_Concurrent' ) ) { $cookie = ( isset( $_COOKIE['passster'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['passster'] ) ) : '' ); if ( !empty( $cookie ) && method_exists( PS_Concurrent::class, 'clear_session__premium_only' ) ) { PS_Concurrent::clear_session__premium_only( $cookie ); } } return new \WP_REST_Response(array( 'success' => true, ), 200); } }