get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s", $metaKey ) ); if ( empty( $rows ) ) { return; } $affected = array(); foreach ( $rows as $row ) { $data = maybe_unserialize( $row->meta_value ); if ( ! is_array( $data ) ) { continue; } // Skip already-disabled forms — they cannot be the // "enabled-but-broken" bug-state by definition. $isEnabled = ! empty( $data['enable'] ) || ! empty( $data['enabled'] ); if ( ! $isEnabled ) { continue; } $dto = FormSettingsDTO::fromArray( $data ); $missing = $dto->getMissingRequiredFields(); if ( empty( $missing ) ) { continue; } // Flip the stored value to disabled. We mutate the stored // array directly rather than re-serialising the DTO so // extension-data fields contributed by addons via the // f12_doi_settings_dto_* filters survive untouched. $data['enable'] = 0; $data['enabled'] = false; update_post_meta( (int) $row->post_id, $metaKey, $data ); $affected[] = array( 'form_id' => (int) $row->post_id, 'missing' => array_values( $missing ), ); /** * Fired by the completeness-sweep migration AND by the * controller-side auto-disable path (plan §2.2). Listeners * should be additive and tolerate multiple firings for the * same form_id over time. * * @since 4.5.0 * * @param string $formId Form ID as a string (composite IDs * for Elementor — the migration always * passes the bare post_id since that * is the storage key). * @param array $missing Stable required-field IDs. */ do_action( 'f12_doi_form_auto_disabled_incomplete', (string) $row->post_id, $missing ); } if ( ! empty( $affected ) ) { // Persist the list so the admin-notice surface can render // it on the next admin page-load. Cleared on dismiss // (handled by AdminNoticeIncompleteForms). update_option( self::AFFECTED_FORMS_OPTION, $affected, false ); } } }