get( $name ); // Not in core yet but still in the fallback store (the site just upgraded): move it now. if ( null === $value && self::uses_core() ) { $value = self::migrate_one( $name ); } return $value; } /** * Store a secret. * * @param string $name Secret name. * @param string $value Plaintext value. * * @return true|\WP_Error */ public static function set( string $name, string $value ) { $result = self::backend()->set( $name, $value ); // A newer value in core must not be shadowed by an older fallback copy. if ( true === $result && self::uses_core() ) { self::fallback()->delete( $name ); } return $result; } /** * Delete a secret from every store. * * @param string $name Secret name. * * @return true|\WP_Error */ public static function delete( string $name ) { $result = self::backend()->delete( $name ); if ( self::uses_core() ) { self::fallback()->delete( $name ); } return $result; } /** * Whether a secret exists, readable or not. * * @param string $name Secret name. * * @return bool */ public static function exists( string $name ): bool { return null !== self::get( $name ); } /** * Move one secret from the fallback store into core. * * Only deletes the fallback copy after core hands back the same value. * * @param string $name Secret name. * * @return string|null|\WP_Error The value now held by core, null if the fallback has none, WP_Error on any failure (fallback copy kept). */ private static function migrate_one( string $name ) { $legacy = self::fallback()->get( $name ); if ( null === $legacy || is_wp_error( $legacy ) ) { return $legacy; } $stored = self::backend()->set( $name, $legacy ); if ( is_wp_error( $stored ) ) { return $stored; } if ( self::backend()->get( $name ) !== $legacy ) { return new \WP_Error( 'adminoptim_secret_migration_unverified', __( 'A secret could not be verified after moving it, so the original was kept.', 'admin-optimizer' ) ); } self::fallback()->delete( $name ); return $legacy; } /** * Move every remaining fallback secret into core, once core is available. * * Runs for administrators on admin_init, does nothing after it has finished, * and backs off for an hour after a failed run. Secrets that cannot be read * (for example after a key change) are left where they are. * * @return void */ public static function maybe_migrate_to_core(): void { if ( ! self::uses_core() || get_option( self::MIGRATED_OPTION ) || get_transient( self::MIGRATE_RETRY_TRANSIENT ) || ! current_user_can( 'manage_options' ) ) { return; } $failed = false; foreach ( self::fallback()->list_names() as $name ) { if ( is_wp_error( self::migrate_one( $name ) ) ) { $failed = true; } } if ( $failed ) { set_transient( self::MIGRATE_RETRY_TRANSIENT, 1, HOUR_IN_SECONDS ); return; } // Nothing left in the fallback store: its wrapped root key is no longer needed. delete_option( AO_Secrets_Fallback_Store::ROOT_OPTION ); update_option( self::MIGRATED_OPTION, 1, false ); } }