post_type && '' === $page->post_password && in_array( $page->post_status, array( 'publish', 'draft', 'pending', 'private', 'future' ), true ) && current_user_can( 'edit_post', $id ); } public static function page_options( $search = '', $number = 1 ) { $query = new \WP_Query( array( 'post_type' => 'page', 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future' ), 'has_password' => false, 'posts_per_page' => 20, 'paged' => max( 1, min( 1000, (int) $number ) ), 's' => wp_html_excerpt( sanitize_text_field( $search ), 200, '' ), 'orderby' => array( 'title' => 'ASC', 'ID' => 'ASC' ) ) ); $items = array(); foreach ( $query->posts as $page ) { if ( self::can_use_page( $page->ID ) ) { $title = $page->post_title ?: __( '(no title)', 'accessible-forms-by-accessible-org' ); /* translators: 1: content title, 2: unique content ID. */ $items[] = array( 'id' => $page->ID, 'title' => sprintf( __( '%1$s ยท #%2$d', 'accessible-forms-by-accessible-org' ), $title, $page->ID ) ); } } return array( 'items' => $items, 'pages' => (int) $query->max_num_pages ); } public static function pages() { check_ajax_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); $id = absint( $_POST['post_ID'] ?? 0 ); if ( 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( array(), 403 ); } wp_send_json_success( self::page_options( sanitize_text_field( wp_unslash( $_POST['search'] ?? '' ) ), absint( $_POST['page'] ?? 1 ) ) ); } public static function create() { check_ajax_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); $id = absint( $_POST['post_ID'] ?? 0 ); if ( 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( array( 'message' => __( 'You cannot preview this form.', 'accessible-forms-by-accessible-org' ) ), 403 ); } [ $schema, $issues ] = Schema::validate( Admin::submitted_schema( $id ) ); if ( $issues ) { wp_send_json_error( array( 'message' => __( 'Fix the blocking problems before opening the test preview.', 'accessible-forms-by-accessible-org' ) ), 400 ); } $page_id = absint( $_POST['af_test_page'] ?? 0 ); if ( $page_id && ! self::can_use_page( $page_id ) ) { wp_send_json_error( array( 'message' => __( 'Choose a page you can edit to test this form with your theme.', 'accessible-forms-by-accessible-org' ) ), 403 ); } $token = bin2hex( random_bytes( 16 ) ); $key = self::KEY . get_current_user_id() . '_' . $id; $value = array( 'token' => $token, 'schema' => $schema, 'page_id' => $page_id ); // At most one snapshot per author and form; it expires and contains no answers. set_transient( $key, $value, 30 * MINUTE_IN_SECONDS ); if ( get_transient( $key ) !== $value ) { wp_send_json_error( array( 'message' => __( 'The test preview could not be prepared. Try again.', 'accessible-forms-by-accessible-org' ) ), 503 ); } $url = $page_id ? add_query_arg( array( 'page_id' => $page_id, 'af_test_form' => $id, 'af_theme_test' => $token, '_af_test_nonce' => wp_create_nonce( self::ACTION . '_' . $id ) ), home_url( '/' ) ) : add_query_arg( array( 'action' => self::ACTION, 'form_id' => $id, 'test' => $token, '_wpnonce' => wp_create_nonce( self::ACTION . '_' . $id ) ), admin_url( 'admin-post.php' ) ); wp_send_json_success( array( 'url' => $url ) ); } /** A capability- and author-bound snapshot, shared by both preview surfaces. */ public static function snapshot( $id, $token ) { if ( 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { return new \WP_Error( 'forbidden', __( 'You cannot preview this form.', 'accessible-forms-by-accessible-org' ), 403 ); } $value = get_transient( self::KEY . get_current_user_id() . '_' . $id ); if ( ! is_string( $token ) || '' === $token || ! is_array( $value ) || ! is_array( $value['schema'] ?? null ) || ! hash_equals( (string) ( $value['token'] ?? '' ), $token ) ) { return new \WP_Error( 'expired', __( 'This private test preview expired or was replaced. Open a fresh preview from Review & publish.', 'accessible-forms-by-accessible-org' ), 410 ); } if ( Schema::validate( $value['schema'] )[1] ) { return new \WP_Error( 'invalid', __( 'The current accessibility checks require changes. Return to the form editor and fix the listed problems.', 'accessible-forms-by-accessible-org' ), 409 ); } return $value; } public static function active() { return null !== self::$themed; } public static function form_id() { return self::$themed['id'] ?? 0; } /** Authenticate before querying private page content or handling any test answers. */ public static function theme_request() { if ( ! isset( $_GET['af_theme_test'] ) ) { return; } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Recognized page-cache constant; private previews must bypass caching. if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); } nocache_headers(); header( 'Referrer-Policy: no-referrer' ); header( 'X-Robots-Tag: noindex, nofollow' ); $id = absint( $_GET['af_test_form'] ?? 0 ); check_admin_referer( self::ACTION . '_' . $id, '_af_test_nonce' ); $token = is_string( $_GET['af_theme_test'] ) ? sanitize_key( wp_unslash( $_GET['af_theme_test'] ) ) : ''; $value = self::snapshot( $id, $token ); if ( is_wp_error( $value ) ) { wp_die( esc_html( $value->get_error_message() ), '', array( 'response' => (int) $value->get_error_data() ) ); } $page = absint( $value['page_id'] ?? 0 ); if ( ! $page || $page !== absint( $_GET['page_id'] ?? 0 ) || ! self::can_use_page( $page ) ) { wp_die( esc_html__( 'You cannot open this private page preview.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 403 ) ); } self::$themed = array( 'id' => $id, 'page' => $page, 'schema' => $value['schema'], 'nonce' => wp_create_nonce( self::ACTION . '_answers_' . $id . '_' . $token ) ); remove_action( 'template_redirect', array( \AccessibleForms\Submission::class, 'maybe_handle' ), 1 ); add_filter( 'redirect_canonical', '__return_false' ); add_action( 'pre_get_posts', array( self::class, 'theme_query' ) ); add_filter( 'the_content', array( self::class, 'theme_content' ), 30 ); add_action( 'wp_footer', array( self::class, 'missing_content' ), 4 ); add_action( 'template_redirect', array( self::class, 'theme_response' ), 0 ); \AccessibleForms\register_frontend_assets(); wp_enqueue_script( 'accessible-forms' ); $method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'post' === $method ) { check_admin_referer( self::ACTION . '_answers_' . $id . '_' . $token, 'af_test_nonce' ); // Nested answers are cleaned and validated; no public submission processing runs. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nested answers are unslashed and cleaned by Schema::clean_input, then validated field by field. $raw = isset( $_POST['af'] ) && is_array( $_POST['af'] ) ? Schema::clean_input( wp_unslash( $_POST['af'] ) ) : array(); self::$state = self::state( $value['schema'], $raw ); } } public static function theme_query( $query ) { if ( $query->is_main_query() && self::active() ) { $query->set( 'post_type', 'page' ); $query->set( 'page_id', self::$themed['page'] ); $query->set( 'post_status', get_post_status( self::$themed['page'] ) ); } } public static function theme_response() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The preview and answer nonces are checked in theme_request() before this callback is registered. if ( self::active() && ! empty( $_POST['af_ajax'] ) ) { $html = self::markup( self::$themed['id'], self::$themed['schema'], self::$state, self::$themed['nonce'] ); wp_send_json( array( 'ok' => ! empty( self::$state['sent'] ), 'html' => $html, 'focus' => self::focus_target( $html, self::$state ) ) ); } } /** Replace this form's embeds; other plugin forms cannot submit from a private test. */ public static function render_in_page( $id ) { if ( (int) $id !== self::$themed['id'] ) { return ''; } self::$rendered = true; return self::markup( $id, self::$themed['schema'], self::$state, self::$themed['nonce'] ); } public static function theme_content( $content ) { if ( ! self::active() || (int) get_the_ID() !== self::$themed['page'] || ! in_the_loop() ) { return $content; } $note = ''; // Use its actual embed location when present; otherwise append within the page content. if ( ! self::$rendered ) { $content .= self::render_in_page( self::$themed['id'] ); } return $note . $content; } public static function missing_content() { if ( self::active() && ! self::$rendered ) { echo '
' . esc_html__( 'This page template did not render the form or normal page content. Choose another editable page, or use the keyboard test preview. Test custom page-builder placements on staging before sharing them.', 'accessible-forms-by-accessible-org' ) . '
' . esc_html__( 'Return to Review & publish', 'accessible-forms-by-accessible-org' ) . '