array_merge( array_keys( ContentTypes::types() ), array( 'wp_block', 'wp_template', 'wp_template_part' ) ), 'post_status' => array( 'publish', 'draft', 'private', 'pending', 'future' ), 'posts_per_page' => 100, 'paged' => max( 1, $page ), 'orderby' => 'ID', 'order' => 'ASC', ) ); $ids = array(); foreach ( $query->posts as $post ) { if ( self::contains_form( $post->post_content, $form_id ) ) { $ids[] = $post->ID; } } $result = array( 'ids' => $ids, 'pages' => $query->max_num_pages, ); set_transient( $key, $result, HOUR_IN_SECONDS ); return $result; } /** * One line describing placement beyond this form's blocks and shortcodes, * provided by an add-on. Empty when there is nothing to add. * * @param int $form_id Form ID. * @return string */ public static function summary( $form_id ) { return (string) Extensions::filter( 'accessible_forms_placement_summary', '', (int) $form_id ); } public static function url( $form_id, $args = array() ) { return add_query_arg( array_merge( array( 'post_type' => 'accessible_form', 'page' => self::SLUG, 'form_id' => (int) $form_id, ), $args ), admin_url( 'edit.php' ) ); } /** Check direct blocks, shortcodes, nested groups, and synced patterns. */ public static function contains_form( $content, $form_id, $visited = array(), $depth = 0 ) { if ( $depth > 32 || $form_id < 1 ) { return false; } if ( preg_match_all( '/' . get_shortcode_regex( array( 'accessible_form' ) ) . '/s', $content, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { if ( '[' === $match[1] && ']' === $match[6] ) { continue; } $attrs = shortcode_parse_atts( $match[3] ); if ( is_array( $attrs ) && \AccessibleForms\FormKey::resolve( $attrs['id'] ?? 0, $attrs['key'] ?? '' ) === (int) $form_id ) { return true; } } } return self::blocks_contain( parse_blocks( $content ), (int) $form_id, $visited, $depth ); } private static function blocks_contain( $blocks, $form_id, $visited, $depth ) { if ( $depth > 32 ) { return false; } foreach ( $blocks as $block ) { if ( 'accessible-forms/form' === $block['blockName'] && \AccessibleForms\FormKey::resolve( $block['attrs']['formId'] ?? 0, $block['attrs']['formKey'] ?? '' ) === $form_id ) { return true; } if ( 'core/block' === $block['blockName'] ) { $id = absint( $block['attrs']['ref'] ?? 0 ); if ( $id && ! in_array( $id, $visited, true ) ) { $reference = get_post( $id ); if ( $reference && 'wp_block' === $reference->post_type && 'publish' === $reference->post_status && self::contains_form( $reference->post_content, $form_id, array_merge( $visited, array( $id ) ), $depth + 1 ) ) { return true; } } } if ( ! empty( $block['innerBlocks'] ) && self::blocks_contain( $block['innerBlocks'], $form_id, $visited, $depth + 1 ) ) { return true; } } return false; } /** * Heading context for every known placement of a form, for content advice. * Scans at most 500 posts; cached until any page, pattern or template changes. * * @param int $form_id Form ID. * @return array{places: array, more: bool} */ public static function heading_contexts( $form_id ) { $key = 'accessible_forms_headings_' . md5( get_option( 'accessible_forms_placement_generation', 'initial' ) . ':' . implode( ',', array_keys( ContentTypes::types() ) ) . ':' . absint( $form_id ) ); $cached = get_transient( $key ); if ( is_array( $cached ) ) { return $cached; } $places = array(); $more = false; for ( $page = 1; $page <= 5; $page++ ) { $batch = self::scan_batch( $form_id, $page ); foreach ( $batch['ids'] as $id ) { $post = get_post( $id ); $level = $post ? self::heading_before( $post->post_content, $form_id ) : false; if ( false !== $level ) { $places[] = array( 'id' => (int) $id, 'title' => $post->post_title ?: __( '(no title)', 'accessible-forms-by-accessible-org' ), 'type' => $post->post_type, 'level' => $level, ); } } if ( $page >= (int) $batch['pages'] ) { break; } $more = 5 === $page; } $result = array( 'places' => $places, 'more' => $more, ); set_transient( $key, $result, HOUR_IN_SECONDS ); return $result; } /** * Level of the last heading before the first placement of a form. * * Walks blocks in reading order, including nested blocks and synced * patterns; for classic content it reads the HTML before the shortcode. * A page's own title is not in its content, so a form with no earlier * heading follows an assumed level-1 title. * * @param string $content Post content. * @param int $form_id Form ID. * @return int|false Heading level (1-6), or false when the form is not in this content. */ public static function heading_before( $content, $form_id ) { $state = array( 'level' => 1, 'found' => false, ); self::walk_headings( parse_blocks( $content ), (int) $form_id, $state, array(), 0 ); return $state['found'] ? $state['level'] : false; } /** * Depth-first walk that stops at the form. * * @param array $blocks Parsed blocks. * @param int $form_id Form ID. * @param array $state Last level seen and whether the form was reached. * @param int[] $visited Synced pattern IDs already entered. * @param int $depth Recursion depth. */ private static function walk_headings( array $blocks, $form_id, array &$state, array $visited, $depth ) { if ( $depth > 32 ) { return; } foreach ( $blocks as $block ) { if ( $state['found'] ) { return; } $name = $block['blockName'] ?? null; if ( 'accessible-forms/form' === $name && \AccessibleForms\FormKey::resolve( $block['attrs']['formId'] ?? 0, $block['attrs']['formKey'] ?? '' ) === $form_id ) { $state['found'] = true; return; } if ( 'core/heading' === $name ) { $state['level'] = max( 1, min( 6, (int) ( $block['attrs']['level'] ?? 2 ) ) ); continue; } if ( in_array( $name, array( 'core/post-title', 'core/query-title' ), true ) ) { $state['level'] = max( 1, min( 6, (int) ( $block['attrs']['level'] ?? ( 'core/post-title' === $name ? 2 : 1 ) ) ) ); continue; } if ( 'core/block' === $name ) { $id = absint( $block['attrs']['ref'] ?? 0 ); $reference = $id && ! in_array( $id, $visited, true ) ? get_post( $id ) : null; if ( $reference && 'wp_block' === $reference->post_type && 'publish' === $reference->post_status ) { self::walk_headings( parse_blocks( $reference->post_content ), $form_id, $state, array_merge( $visited, array( $id ) ), $depth + 1 ); } continue; } $html = (string) ( $block['innerHTML'] ?? '' ); // Classic content or a freeform block containing the shortcode. $cut = self::shortcode_offset( $html, $form_id ); if ( false !== $cut ) { self::last_html_heading( substr( $html, 0, $cut ), $state ); $state['found'] = true; return; } if ( empty( $block['innerBlocks'] ) ) { self::last_html_heading( $html, $state ); } else { self::walk_headings( $block['innerBlocks'], $form_id, $state, $visited, $depth + 1 ); } } } /** * Byte offset of this form's shortcode in some HTML, or false. * * @param string $html HTML. * @param int $form_id Form ID. * @return int|false */ private static function shortcode_offset( $html, $form_id ) { if ( false === strpos( $html, '[accessible_form' ) ) { return false; } if ( preg_match_all( '/' . get_shortcode_regex( array( 'accessible_form' ) ) . '/s', $html, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE ) ) { foreach ( $matches as $match ) { $attrs = shortcode_parse_atts( $match[3][0] ); if ( '[' !== $match[1][0] && is_array( $attrs ) && \AccessibleForms\FormKey::resolve( $attrs['id'] ?? 0, $attrs['key'] ?? '' ) === $form_id ) { return $match[0][1]; } } } return false; } /** * Record the last

–

in some HTML. * * @param string $html HTML. * @param array $state Walk state. */ private static function last_html_heading( $html, array &$state ) { if ( preg_match_all( '/]/i', $html, $matches ) ) { $state['level'] = (int) end( $matches[1] ); } } public static function shortcode( $form_id ) { return '[accessible_form id="' . (int) $form_id . '" key="' . \AccessibleForms\FormKey::get( $form_id ) . '"]'; } public static function block_content( $form_id ) { return serialize_block( array( 'blockName' => 'accessible-forms/form', 'attrs' => array( 'formId' => (int) $form_id, 'formKey' => \AccessibleForms\FormKey::get( $form_id ), ), 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), ) ); } /** Read-only link: the existing page is changed only inside the editor. */ public static function editor_url( $page_id, $form_id, $created = false, $position = 'after' ) { $page = get_post( $page_id ); if ( ! $page || ! isset( ContentTypes::types( true )[ $page->post_type ] ) || 'trash' === $page->post_status || ! current_user_can( 'edit_post', $page_id ) || ! Schema::get( $form_id ) ) { return ''; } return add_query_arg( array( 'af_insert_form' => (int) $form_id, 'af_page_created' => $created ? 1 : 0, 'af_position' => 'before' === $position ? 'before' : 'after', '_af_placement_nonce' => wp_create_nonce( 'accessible_forms_place_' . $form_id . '_' . $page_id ), ), get_edit_post_link( $page_id, 'raw' ) ); } /** Testable operation; handler additionally checks the submission nonce. */ public static function create_draft( $form_id, $title, $post_type = 'page' ) { $type = ContentTypes::types( true )[ $post_type ] ?? null; if ( ! $type || ! current_user_can( $type->cap->create_posts ) ) { return new \WP_Error( 'accessible_forms_permission', __( 'You do not have permission to create this content type.', 'accessible-forms-by-accessible-org' ) ); } if ( ! Schema::get( $form_id ) ) { return new \WP_Error( 'accessible_forms_unpublished', __( 'Publish the form before placing it in your content.', 'accessible-forms-by-accessible-org' ) ); } $title = sanitize_text_field( $title ); if ( '' === $title || preg_match_all( '/./us', $title ) > 200 ) { return new \WP_Error( 'accessible_forms_title', __( 'Enter a title of 200 characters or fewer.', 'accessible-forms-by-accessible-org' ) ); } $content = use_block_editor_for_post_type( $post_type ) ? self::block_content( $form_id ) : self::shortcode( $form_id ); return wp_insert_post( wp_slash( array( 'post_type' => $post_type, 'post_status' => 'draft', 'post_title' => $title, 'post_content' => $content, 'post_author' => get_current_user_id(), ) ), true ); } public static function handle_create() { $form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; check_admin_referer( 'accessible_forms_create_page_' . $form_id ); $title = isset( $_POST['page_title'] ) ? sanitize_text_field( wp_unslash( $_POST['page_title'] ) ) : ''; $type = sanitize_key( wp_unslash( $_POST['content_type'] ?? 'page' ) ); $id = self::create_draft( $form_id, $title, $type ); if ( is_wp_error( $id ) ) { wp_die( esc_html( $id->get_error_message() ), '', array( 'back_link' => true ) ); } wp_safe_redirect( self::editor_url( $id, $form_id, true ) ); exit; } /** Only signed, authorized editor requests receive an insertion control. */ public static function editor_assets( $hook ) { if ( 'post.php' !== $hook ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nonce and page capability checked below. $form_id = isset( $_GET['af_insert_form'] ) ? absint( $_GET['af_insert_form'] ) : 0; $page_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; $nonce = isset( $_GET['_af_placement_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_af_placement_nonce'] ) ) : ''; $created = ! empty( $_GET['af_page_created'] ); $position = isset( $_GET['af_position'] ) && 'before' === sanitize_key( wp_unslash( $_GET['af_position'] ) ) ? 'before' : 'after'; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( ! $form_id || ! wp_verify_nonce( $nonce, 'accessible_forms_place_' . $form_id . '_' . $page_id ) || ! current_user_can( 'edit_post', $page_id ) || ! isset( ContentTypes::types( true )[ get_post_type( $page_id ) ] ) || ! Schema::get( $form_id ) ) { return; } $page = get_post( $page_id ); $is_block = use_block_editor_for_post( $page ); $deps = $is_block ? array( 'wp-dom-ready', 'wp-data', 'wp-blocks', 'wp-notices', 'wp-shortcode', 'wp-i18n' ) : array( 'wp-dom-ready', 'wp-shortcode', 'wp-i18n' ); wp_enqueue_script( 'accessible-forms-insert', ACCESSIBLE_FORMS_URL . 'assets/insert-form.js', $deps, \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-insert', 'accessible-forms-by-accessible-org' ); wp_localize_script( 'accessible-forms-insert', 'AccessibleFormsPlacement', array( 'formId' => $form_id, 'formKey' => \AccessibleForms\FormKey::get( $form_id ), 'pageId' => $page_id, 'title' => get_the_title( $form_id ), 'isBlock' => $is_block, 'created' => $created, 'position' => $position, 'alreadyPresent' => self::contains_form( $page->post_content, $form_id ), ) ); if ( ! $is_block ) { add_action( 'admin_notices', static function () use ( $created, $position ) { echo '

' . esc_html( $created ? __( 'Draft created with your form. Review it, then publish when ready.', 'accessible-forms-by-accessible-org' ) : __( 'Insert your selected form at the chosen position, then review and save your changes.', 'accessible-forms-by-accessible-org' ) ) . '

'; } ); } } public static function render() { PlacementScreen::render(); } }