maybe_restore( array(), $value ); } /** * Fires whenever schema_ai_settings changes. Only actually attempts a * restore when a key was just configured on an otherwise-empty install. * * @param mixed $old_value Previous option value. * @param mixed $new_value New option value. */ public function maybe_restore( $old_value, $new_value ) { $old_value = is_array( $old_value ) ? $old_value : array(); $new_value = is_array( $new_value ) ? $new_value : array(); $api_key = ! empty( $new_value['api_key'] ) ? $new_value['api_key'] : ''; if ( empty( $api_key ) ) { return; } // Only for a genuinely fresh/empty local config: no site type or key // configured BEFORE this save (i.e. this is the key being entered for // the first time on this install). Deliberately checks $old_value, not // $new_value, for site_type: the setup wizard submits the API key and // the chosen site type together in the same request, so checking the // just-submitted value would always see a site type and skip restore // on every real fresh install going through the wizard, exactly the // case this is meant to catch. if ( ! empty( $old_value['site_type'] ) || ! empty( $old_value['api_key'] ) ) { return; } // Already attempted on this install — don't retry every settings save. if ( get_option( self::RESULT_OPTION ) ) { return; } $this->run_restore( $api_key ); } /** * Fetch the backup bundle from the SaaS backend and apply it. * * @param string $api_key The freshly configured API key. */ private function run_restore( $api_key ) { $response = wp_remote_get( SCHEMA_AI_API_BASE . '/sites/restore-bundle?siteUrl=' . rawurlencode( home_url( '/' ) ), array( 'headers' => array( 'X-API-Key' => $api_key ), 'timeout' => 20, ) ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return; // Silent — will simply retry on the next settings save if not marked as attempted. } $data = json_decode( wp_remote_retrieve_body( $response ), true ); $bundle = isset( $data['data'] ) ? $data['data'] : null; if ( empty( $bundle ) ) { // No backup for this site (new install that's never synced before) — // mark as attempted so we don't keep polling on every settings save. update_option( self::RESULT_OPTION, array( 'found' => false, 'at' => time() ), false ); return; } $restored_pages = 0; if ( ! empty( $bundle['pluginSettings'] ) && is_array( $bundle['pluginSettings'] ) ) { $settings = get_option( 'schema_ai_settings', array() ); // Keep the API key the admin just entered; restore everything else. $settings = array_merge( $bundle['pluginSettings'], array( 'api_key' => $api_key ) ); update_option( 'schema_ai_settings', $settings ); } // The entity profile in pluginSettings is the plugin's last-synced copy, // which can be stale if the profile was edited from the dashboard while // the plugin was uninstalled. Overlay the authoritative copy with its real // timestamp so that edit survives — and so the plugin's next sync doesn't // wrongly win the last-write-wins check and revert it. if ( ! empty( $bundle['entitySettings'] ) && is_array( $bundle['entitySettings'] ) && class_exists( 'Schema_AI_Entity_Settings' ) ) { $entity_updated_at = ! empty( $bundle['entitySettingsUpdatedAt'] ) ? (string) $bundle['entitySettingsUpdatedAt'] : gmdate( 'c' ); Schema_AI_Entity_Settings::apply_remote( $bundle['entitySettings'], $entity_updated_at ); } if ( ! empty( $bundle['pages'] ) && is_array( $bundle['pages'] ) ) { $restored_pages = $this->restore_pages( $bundle['pages'] ); } update_option( self::RESULT_OPTION, array( 'found' => true, 'pages' => $restored_pages, 'at' => time(), ), false ); } /** * Re-insert schema rows for posts that still exist on this install. * * @param array $pages Array of { postId, schemas: [ { type, data, isAutoGenerated } ] }. * @return int Number of pages restored. */ private function restore_pages( $pages ) { global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_schemas' ); $restored = 0; foreach ( $pages as $page ) { $post_id = isset( $page['postId'] ) ? absint( $page['postId'] ) : 0; if ( ! $post_id || ! get_post( $post_id ) ) { continue; // Post no longer exists on this install — nothing to attach it to. } if ( empty( $page['schemas'] ) || ! is_array( $page['schemas'] ) ) { continue; } $now = current_time( 'mysql' ); $inserted = false; foreach ( $page['schemas'] as $schema ) { if ( empty( $schema['type'] ) || empty( $schema['data'] ) || ! is_array( $schema['data'] ) ) { continue; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; write operation (INSERT). $ok = $wpdb->insert( $table, array( 'post_id' => $post_id, 'schema_type' => sanitize_text_field( $schema['type'] ), 'schema_data' => wp_json_encode( $schema['data'] ), 'is_auto_generated' => ! empty( $schema['isAutoGenerated'] ) ? 1 : 0, 'validation_status' => 'pending', 'created_at' => $now, 'updated_at' => $now, ), array( '%d', '%s', '%s', '%d', '%s', '%s', '%s' ) ); if ( false !== $ok ) { $inserted = true; // Same async-validation hook create_item() schedules — keyed // by schema row id, not post id. wp_schedule_single_event( time() + 10, 'schema_ai_validate', array( $wpdb->insert_id ) ); } } if ( $inserted ) { $restored++; } } return $restored; } /** * One-time admin notice reporting what was restored. */ public function render_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } $result = get_option( self::RESULT_OPTION ); if ( empty( $result ) || empty( $result['found'] ) || ! empty( $result['dismissed'] ) ) { return; } $pages = isset( $result['pages'] ) ? (int) $result['pages'] : 0; echo '
'; printf( /* translators: %d: number of pages restored */ esc_html__( 'AI Schema Gen restored your settings and schema for %d page(s) from your previous connection.', 'ai-schema-gen' ), (int) $pages ); echo '