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
' . 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' ) ) . '