Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions includes/Admin/Activation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
/**
* Runs on plugin activation.
*
* @package WordPress\AI\Admin
* @since x.x.x
*/

declare( strict_types=1 );

namespace WordPress\AI\Admin;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
* Class - Activation.
*
* @internal
*
* @since x.x.x
*/
final class Activation {
/**
* Runs on plugin activation.
*
* @since x.x.x
*/
public static function activation_callback(): void {
// Check and run any pending upgrades.
Upgrades::do_upgrades();
}
}
140 changes: 140 additions & 0 deletions includes/Admin/Upgrades.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php
/**
* Handles any routines or migrations necessary when upgrading to a new version of the plugin.
*
* @package WordPress\AI\Admin
* @since x.x.x
*/

declare( strict_types=1 );

namespace WordPress\AI\Admin;

use WordPress\AI\Admin\Upgrades\V0_5_0;
use WordPress\AI\Admin\Upgrades\V0_6_0;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
* Class - Upgrades.
*
* @internal
*
* @since x.x.x
*/
final class Upgrades {
/**
* The key to store the version.
*
* @since x.x.x
*/
private const VERSION_OPTION_KEY = 'wpai_version';

/**
* The key to store failed upgrade information for the admin notice.
*
* @since x.x.x
*/
private const FAILED_UPGRADE_OPTION_KEY = 'wpai_failed_upgrade_message';

/**
* Upgrade classes.
*
* New upgrade routine classes should be added here, in order of oldest to newest.
*
* @since x.x.x
*
* @var class-string<\WordPress\AI\Admin\Upgrades\Abstract_Upgrade>[]
*/
private const UPGRADE_CLASSES = array( // phpcs:ignore SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition -- This is used as a single const.
V0_5_0::class,
V0_6_0::class,
);

/**
* Initialize the class.
*
* @since x.x.x
*/
public function init(): void {
// Runs as a fallback, in case the activation hook is missed.
add_action( 'admin_init', array( $this, 'do_upgrades' ) );

add_action( 'admin_notices', array( $this, 'failed_upgrade_notice' ) );
}

/**
* Checks for and runs any pending upgrades.
*
* @since x.x.x
*/
public static function do_upgrades(): void {
$db_version = get_option( self::VERSION_OPTION_KEY, '' );

foreach ( self::UPGRADE_CLASSES as $upgrade_class ) {
/**
* Skip upgrades for newer versions.
* @todo Remove the !empty() check once we no long need to support < v0.5.0 and '' means a new install.
*/
if ( ! empty( $db_version ) && version_compare( $db_version, $upgrade_class::$version, '>=' ) ) {
continue;
}

$upgrade = new $upgrade_class( $db_version );
$result = $upgrade->run();

// Store the error message and stop if the upgrade failed.
if ( is_wp_error( $result ) ) {
update_option(
self::FAILED_UPGRADE_OPTION_KEY,
array(
'version' => $upgrade_class::$version,
'error' => $result->get_error_message(),
)
);
return;
}

$db_version = $upgrade_class::$version;
}

// If all upgrades completed successfully, the plugin was successfully upgraded to the latest version.
delete_option( self::FAILED_UPGRADE_OPTION_KEY );
update_option( self::VERSION_OPTION_KEY, WPAI_VERSION );
}

/**
* Displays an admin notice if a plugin upgrade failed, with the error message.
*
* @since x.x.x
*/
public function failed_upgrade_notice(): void {
// Skip if there's no failures.
$failed_upgrade = get_option( self::FAILED_UPGRADE_OPTION_KEY, false );
if ( ! $failed_upgrade ) {
return;
}

// If the error is set but empty, clean it up.
if ( empty( $failed_upgrade['version'] ) || empty( $failed_upgrade['error'] ) ) {
delete_option( self::FAILED_UPGRADE_OPTION_KEY );
return;
}

// Display the error message.
wp_admin_notice(
sprintf(
/* translators: 1. The version the upgrade failed on, 2. The error message. */
esc_html__( 'WordPress AI failed to upgrade to %1$s. Migration version %2$s failed with the following error: %3$s. Please deactivate and reactivate the plugin to try again.', 'ai' ),
WPAI_VERSION,
esc_html( $failed_upgrade['version'] ),
esc_html( $failed_upgrade['error'] )
),
array(
'type' => 'error',
'dismissible' => false,
)
);
}
}
97 changes: 97 additions & 0 deletions includes/Admin/Upgrades/Abstract_Upgrade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/**
* Abstract class for handling plugin upgrade routines.
*
* The class encapsulates the logic for performing any necessary database migrations or other upgrade tasks when the plugin is updated to a new version. Each class represents a specific plugin version where the upgrade became necessary and contains the logic to perform the upgrade from the previous version(s).
*
* Error are caught during the upgrade routine and stored in an option for use in an admin notice.
*
* @package WordPress\AI\Admin\Upgrades
* @since x.x.x
*/

declare( strict_types=1 );

namespace WordPress\AI\Admin\Upgrades;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
* Abstract class for handling plugin upgrade routines.
*
* @internal
*
* @since x.x.x
*/
abstract class Abstract_Upgrade {

/**
* The version number associated with this upgrade.
*
* This should be overloaded
*
* @since x.x.x
*
* @var string
*/
public static string $version = '';

/**
* The version of the plugin in the database.
*
* This is the version we're migrating from.
*
* @since x.x.x
*
* @var string
*/
protected string $db_version = '';

/**
* Constructor.
*
* @since x.x.x
*
* @param string $db_version The version of the plugin in the database.
*
* @throws \InvalidArgumentException Throws an exception if the provided version is invalid.
*/
public function __construct( string $db_version ) {
// @todo remove the !empty() check when we don't need to migrate from < 0.5.0 and can treat '' as a new install.
if ( ! empty( $db_version ) && ! version_compare( $db_version, '0.0.0', '>' ) ) {
throw new \InvalidArgumentException( 'Invalid database version provided for upgrade.' );
}

$this->db_version = $db_version;
}

/**
* Performs the upgrade routine.
*
* @since x.x.x
*
* @return true|\WP_Error True on success, or a WP_Error on failure.
*/
public function run() {
if ( version_compare( $this->db_version, static::$version, '>=' ) ) {
// No upgrade needed.
return true;
}

try {
$this->upgrade();
} catch ( \Throwable $e ) {
return new \WP_Error( 'wpai_upgrade_failed', $e->getMessage() );
}

return true;
}

/**
* The upgrade process.
*
* @throws \Exception Throws an exception if the upgrade fails.
*/
abstract protected function upgrade(): void;
}
81 changes: 81 additions & 0 deletions includes/Admin/Upgrades/V0_5_0.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
/**
* Upgrade routines for version 0.5.0.
*
* Originally this file was located in `includes/Migrations/Credential_Migration.php`, which is why the since tags don't match.
*
* @package WordPress\AI\Admin\Upgrades
* @since x.x.x
*/

declare( strict_types=1 );

namespace WordPress\AI\Admin\Upgrades;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
* Upgrade routine for migrating provider credentials to the new Connectors-based storage format.
*
* @since x.x.x
* @internal
*/
class V0_5_0 extends Abstract_Upgrade {
/**
* The legacy option that stored all provider credentials as an array.
*
* @since x.x.x
* @var string
*/
private const OLD_OPTION = 'wp_ai_client_provider_credentials';

/**
* The map of provider slugs to their new Connectors option names.
*/
private const PROVIDER_MAP = array( // phpcs:ignore SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition -- This is used as a array.
'openai' => 'connectors_ai_openai_api_key',
'google' => 'connectors_ai_google_api_key',
'anthropic' => 'connectors_ai_anthropic_api_key',
);

/**
* {@inheritDoc}
*
* @since x.x.x
*/
public static string $version = '0.5.0';

/**
* {@inheritDoc}
*
* Copies legacy provider credentials to the new per-provider options.
*
* Reads the old combined credentials option and, for each known provider,
* copies the credential to the new option only when the new option is empty.
*
* @since x.x.x
*/
protected function upgrade(): void {
$old_credentials = get_option( self::OLD_OPTION, array() );

if ( empty( $old_credentials ) || ! is_array( $old_credentials ) ) {
return;
}

foreach ( self::PROVIDER_MAP as $provider => $new_option ) {
if ( empty( $old_credentials[ $provider ] ) ) {
continue;
}

// Only migrate if the new option slot is empty.
if ( '' !== get_option( $new_option, '' ) ) {
continue;
}

update_option( $new_option, $old_credentials[ $provider ] );
}

delete_option( self::OLD_OPTION );
}
}
Loading
Loading