get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN ('publish', 'draft', 'pending', 'private', 'future') AND ID > %d ORDER BY ID ASC LIMIT %d", 'jet-form-builder', $last_id, self::REBUILD_BATCH_SIZE ) ); if ( empty( $form_ids ) ) { break; } $is_last_page = count( $form_ids ) < self::REBUILD_BATCH_SIZE; foreach ( $form_ids as $form_id ) { $form_id = (int) $form_id; $last_id = max( $last_id, $form_id ); $post = get_post( $form_id ); if ( ! $post instanceof \WP_Post ) { continue; } $found = self::collect_from_content( $post->post_content ); update_post_meta( $form_id, self::META_KEY, $found ); foreach ( $found as $name ) { $callbacks[ $name ] = true; } // Checked per form (not only once per batch) so even the last, short page — // which never hits the `$is_last_page` break below since there is no further // page to fetch — is still time-boxed. A page short enough to end the scan can // still contain forms with very large post_content, and without this the last // page ran fully unbounded regardless of size. if ( self::rebuild_time_budget_exceeded( $started_at ) ) { self::persist_progress_and_yield( $last_id, $callbacks ); } } if ( $is_last_page ) { break; } if ( self::rebuild_time_budget_exceeded( $started_at ) ) { self::persist_progress_and_yield( $last_id, $callbacks ); } } delete_option( self::REBUILD_PROGRESS_OPTION ); update_option( self::OPTION_KEY, array_values( array_keys( $callbacks ) ), false ); } /** * Whether the rebuild has been running long enough that it should persist progress and * yield the request instead of continuing. The budget is filterable so tests can force * an early "incomplete" result without waiting on wall-clock time. * * @param float $started_at `microtime( true )` value captured when the rebuild started. */ protected static function rebuild_time_budget_exceeded( float $started_at ): bool { $budget = (float) apply_filters( 'jet-form-builder/ssr-callback-allowlist/rebuild-time-budget', self::REBUILD_TIME_BUDGET_SECONDS ); return ( microtime( true ) - $started_at ) >= $budget; } /** * Writes the resume cursor and throws to yield the request. Write the cursor FIRST, then * commit: with `autocommit = 0`, an explicit `COMMIT` ends the current transaction and * the next write opens a new implicit one. Committing before writing the cursor would * leave the cursor itself in that fresh, uncommitted transaction — exactly what * `Auto_Migrator::run()`'s catch-and-rollback of the exception below would then discard, * defeating the whole point of committing early. * * @since 3.6.5.3 * * @param int $last_id * @param array $callbacks * * @throws \Jet_Form_Builder\Migrations\Migration_Incomplete_Exception Always. */ /** * Normalizes `$progress['callbacks']` into the `[ name => true ]` associative-set shape * this class has always used in memory, regardless of which on-disk shape produced it. * * The already-released 3.6.5.2 build persisted `callbacks` as a plain list of names * (`array( 'is_email', 'is_numeric' )`) and converted it back via * `array_fill_keys( $progress['callbacks'] ?? array(), true )` on load. A later revision * of this method (still on this branch, never released) started reading * `$progress['callbacks']` directly as if it were already the associative-set shape — * for a real 3.6.5.2 site whose rebuild was interrupted mid-scan (a resume cursor is * only persisted when the forms table doesn't fit in one time budget) and then updates * straight to this version, the persisted list's names become integer array keys, and * `array_values( array_keys( $callbacks ) )` at the end of `rebuild_from_all_forms()` * would silently store those integers as the "restored callback names" instead of the * actual names (review finding, issues-tracker #20361 follow-up). Accepting both shapes * here keeps that upgrade path correct without depending on which build a given site's * in-progress rebuild happened to start under. * * @since 3.6.5.3 * * @param mixed $progress The raw `get_option( self::REBUILD_PROGRESS_OPTION )` result. * * @return array */ private static function normalize_progress_callbacks( $progress ): array { if ( ! is_array( $progress ) || ! isset( $progress['callbacks'] ) || ! is_array( $progress['callbacks'] ) ) { return array(); } $raw = $progress['callbacks']; // Associative-set shape (current/new format): every key is already a string name. // A plain list (legacy 3.6.5.2 format) has only integer keys, so this check tells // the two apart without needing to inspect a version flag that was never recorded. if ( array_keys( $raw ) === array_keys( array_keys( $raw ) ) ) { return array_fill_keys( array_filter( $raw, 'is_string' ), true ); } return array_filter( $raw, static function ( $key ) { return is_string( $key ); }, ARRAY_FILTER_USE_KEY ); } private static function persist_progress_and_yield( int $last_id, array $callbacks ) { update_option( self::REBUILD_PROGRESS_OPTION, array( 'last_id' => $last_id, 'callbacks' => $callbacks, ), false ); \Jet_Form_Builder\Db_Queries\Execution_Builder::instance()->transaction_commit(); throw new \Jet_Form_Builder\Migrations\Migration_Incomplete_Exception( 'SSR allowlist rebuild exceeded its time budget; will resume on the next request.' ); } }