# Migration

The Pro plugin's `Migration` class — what runs on activation, deactivation, plugin update, and on every request. Small surface, easy to misread; this doc captures the actual contract.

## Context

WordPress plugins commonly use a `Migration` / `Installer` class to perform one-time data shifts when the plugin version changes (e.g. renaming option keys, updating data schemas). Pro's `Migration` class also does **lite-version installation** — when Pro is freshly installed, it triggers a Lite install/activation if Lite isn't already present.

The class lives at `includes/Classes/Migration.php` (106 lines) and is wired into four hooks from the plugin entry file `essential_adons_elementor.php`.

## Verified facts

### Class anatomy

```php
namespace Essential_Addons_Elementor\Pro\Classes;

class Migration
{
    use \Essential_Addons_Elementor\Pro\Traits\Library;   // empty_dir, get_plugin_data, install_plugin, upgrade_plugin, is_plugin_installed, get_plugin_version, safe_path
    use \Essential_Addons_Elementor\Pro\Traits\Core;       // set_default_values, make_lite_available, plugin_licensing
    // ...
}
```

The class is **not a singleton.** Every hook handler in the entry file instantiates a fresh `new Migration` — fine because the methods are stateless.

### Hook surface

Four hooks in `essential_adons_elementor.php`:

| Hook                          | Pro method                                        | When fires                                                  |
| ----------------------------- | ------------------------------------------------- | ----------------------------------------------------------- |
| `register_activation_hook`    | `Migration::plugin_activation_hook()`             | Pro is activated (first time or re-activation)              |
| `register_deactivation_hook`  | `Migration::plugin_deactivation_hook()` (+ option cleanup + cron clear) | Pro is deactivated                       |
| `upgrader_process_complete`   | `Migration::plugin_upgrade_hook( $upgrader, $options )` | After WP completes a plugin update (any plugin — Pro filters by basename) |
| `wp_loaded`                   | `Migration::migrator()`                           | Every front-end + admin request, after WP is fully loaded   |

### Activation hook

```php
public function plugin_activation_hook()
{
    // remove old cache files
    if ( defined( 'EAEL_ASSET_PATH' ) ) {
        $this->empty_dir( EAEL_ASSET_PATH );
    }

    $this->set_default_values();

    // make lite version available
    if ( function_exists( 'wp_get_environment_type' ) ) {
        if ( wp_get_environment_type() !== 'development' ) {
            set_transient( 'eael_install_lite', true, 1800 );
        }
    } else {
        set_transient( 'eael_install_lite', true, 1800 );
    }
}
```

**Three actions:**

1. **Clear asset cache** — `EAEL_ASSET_PATH` is a Lite constant pointing at the per-page CSS/JS cache directory. If Lite isn't loaded yet at activation time, this is a no-op (`defined` check).
2. **Set default widget enablement** — `set_default_values()` (from `Core` trait) writes a `$defaults` array of slugs into the EA dashboard option, marking all Pro widgets/extensions as enabled by default for new installs.
3. **Schedule a Lite install** — sets the `eael_install_lite` transient (30-minute TTL). The next `wp_loaded` request will see this transient and call `make_lite_available()`. **In `wp_get_environment_type() === 'development'`**, this is skipped to avoid messing with developer setups.

### Deactivation hook

```php
public function plugin_deactivation_hook()
{
    if ( defined( 'EAEL_ASSET_PATH' ) ) {
        $this->empty_dir( EAEL_ASSET_PATH );
    }
}
```

Plus, in the entry file callback (not in the method):

```php
register_deactivation_hook( __FILE__, function () {
    $migration = new Migration;
    $migration->plugin_deactivation_hook();
    delete_option( '_eael_initial_sync' );
    wp_clear_scheduled_hook( 'eael_sync_initial_orders' );
    wp_clear_scheduled_hook( 'eael_sync_daily_orders' );
} );
```

Pro on deactivation:

1. Clears Lite's per-page asset cache (if `EAEL_ASSET_PATH` is defined)
2. Deletes the `_eael_initial_sync` option (likely WooCommerce / Static_Product sync state)
3. Clears two scheduled cron hooks: `eael_sync_initial_orders` and `eael_sync_daily_orders`

**What it does NOT do** — and this is the right call:

- Does NOT delete license key / status (uninstall, not deactivation, would be the right place)
- Does NOT delete Pro widget settings stored in Elementor post meta (post meta is user content)
- Does NOT deactivate Lite (Lite is independent; Pro deactivation should not affect it)

### Upgrade hook

```php
public function plugin_upgrade_hook( $upgrader_object, $options ) {
    if ( isset( $options['action'], $options['type'] ) && $options['action'] === 'update' && $options['type'] === 'plugin' ) {
        if ( isset( $options['plugins'][ EAEL_PRO_PLUGIN_BASENAME ] ) ) {
            if ( defined( 'EAEL_ASSET_PATH' ) ) {
                $this->empty_dir( EAEL_ASSET_PATH );
            }
        }
    }
}
```

Fires after WordPress completes any plugin update. Pro filters by `EAEL_PRO_PLUGIN_BASENAME` so it only acts when **this** plugin was the one updated. Action: clears the asset cache so the freshly-built `.min.js`/`.min.css` from the new version are served, not stale Lite-cached CSS strings.

**Edge case:** when Lite is updated (separate `$options['plugins']` entry), Pro's hook fires too but takes no action — the basename check filters out the Lite case. Lite's own upgrade hook handles its own cache invalidation.

### `migrator()` — runs every request on `wp_loaded`

```php
public function migrator()
{
    if ( get_option( 'eael_pro_version' ) != EAEL_PRO_PLUGIN_VERSION ) {
        update_option( 'eael_pro_version', EAEL_PRO_PLUGIN_VERSION );

        // Tricky update here - @since 3.0.4
        if ( function_exists( 'wp_get_environment_type' ) ) {
            if ( wp_get_environment_type() !== 'development' ) {
                set_transient( 'eael_install_lite', true, 1800 );
            }
        }
    }

    if ( (boolean) get_transient( 'eael_install_lite' ) === true ) {
        $this->make_lite_available();
    }
}
```

Two-stage logic:

1. **Version-bump detection** — compares stored `eael_pro_version` option with current `EAEL_PRO_PLUGIN_VERSION` constant. If different (first run after update, or first request after fresh activation), update the option AND re-set the `eael_install_lite` transient (the "tricky update" — a way to re-trigger Lite-install after a Pro version bump).
2. **Lite install trigger** — if the transient is set (from activation, upgrade, or version-bump), call `make_lite_available()`.

**No version-keyed migration steps.** Today's `migrator()` does NOT have per-version branches like `if ( version_compare( $old_version, '6.0.0', '<' ) ) { /* migrate 5.x -> 6.x */ }`. If a future version needs a data shift, this is where it goes.

### `make_lite_available()` — auto-install / activate Lite

```php
public function make_lite_available()
{
    $basename = 'essential-addons-for-elementor-lite/essential_adons_elementor.php';
    $plugin_data = $this->get_plugin_data( 'essential-addons-for-elementor-lite' );  // wp.org API

    if ( $this->is_plugin_installed( $basename ) ) {
        if ( isset( $plugin_data->version )
             && $this->get_plugin_version( $basename ) != $plugin_data->version ) {
            $this->upgrade_plugin( $basename );  // ← attempts upgrade once
        }

        if ( is_plugin_active( $basename ) ) {
            return delete_transient( 'eael_install_lite' );
        } else {
            activate_plugin( $this->safe_path( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $basename ), '', false, false );
            return delete_transient( 'eael_install_lite' );
        }
    } else {
        // install + activate
        if ( isset( $plugin_data->download_link ) ) {
            if ( $this->install_plugin( $plugin_data->download_link ) ) {
                return delete_transient( 'eael_install_lite' );
            }
        }
    }

    return false;
}
```

**Behaviour:**

- If Lite is installed AND outdated → attempt upgrade (uses WP's plugin upgrader internally; can fail silently on filesystem permission issues)
- If Lite is installed AND inactive → activate it
- If Lite is installed AND active → no-op, just delete the transient
- If Lite is NOT installed → fetch download link from wp.org API, install, activate

**Constraints:**

- Requires `manage_options` / `install_plugins` cap implicitly via `install_plugin` — the calling user must have the cap. Since `migrator` runs on every `wp_loaded`, a logged-in admin needs to make a request before this trigger fires.
- The `get_plugin_data` call hits `api.wordpress.org/plugins/info/1.0/` via the Library trait — **same RCE-risk path flagged in `external-api-integrations.md`**. HTTP + `unserialize`. Critical.

### What runs at what stage — combined view

```
Plugin Activation
    Migration::plugin_activation_hook
        ├─ empty_dir( EAEL_ASSET_PATH )
        ├─ set_default_values()                              ← enables all Pro widgets/extensions by default
        └─ set_transient( 'eael_install_lite', true, 1800 )

Plugin Deactivation
    Migration::plugin_deactivation_hook + entry-file inline:
        ├─ empty_dir( EAEL_ASSET_PATH )
        ├─ delete_option( '_eael_initial_sync' )
        ├─ wp_clear_scheduled_hook( 'eael_sync_initial_orders' )
        └─ wp_clear_scheduled_hook( 'eael_sync_daily_orders' )

Plugin Update (any plugin)
    Migration::plugin_upgrade_hook
        └─ if updated plugin === Pro: empty_dir( EAEL_ASSET_PATH )

Every wp_loaded request
    Migration::migrator
        ├─ if eael_pro_version option != PLUGIN_VERSION:
        │     ├─ update option
        │     └─ set transient (Lite re-install signal)
        └─ if transient 'eael_install_lite' is set:
              └─ make_lite_available()
                    ├─ get_plugin_data() → wp.org HTTP fetch
                    ├─ install / upgrade / activate Lite as needed
                    └─ delete_transient( 'eael_install_lite' )
```

## What's missing

1. **No uninstall handler.** WordPress lets plugins define an `uninstall.php` or `register_uninstall_hook` for cleanup when the plugin is **deleted** (not just deactivated). Pro has neither. Result: license key, license status, EA dashboard option, Pro widget enablement defaults, all remain in the DB after Pro is deleted. Action: add `uninstall.php` that wipes `eael_pro_version`, license options, default-values option, scheduled hooks, asset cache — but only when an explicit "delete user data on uninstall" preference is set (some users want to keep settings across reinstalls).
2. **`migrator()` runs on every request.** Two `get_option` calls + 1 `get_transient` call per request, even when nothing has changed. Cost is minimal (autoloaded options, no DB write in the steady state) but flagged as a hot-path observation.
3. **`make_lite_available()` calls wp.org plugin-info on every request** (via `get_plugin_data`) when the transient is set. The transient is short-lived (30 min) so this only happens immediately after a Pro version bump or fresh activation, but during that window every admin pageload hits wp.org. Cache the plugin-info response.
4. **No version-keyed migration steps.** If a future release needs to rename a Pro option key or transform widget settings, the framework is `if ( $stored_version !== EAEL_PRO_PLUGIN_VERSION )` but there's no scaffolding for per-version migration callbacks. Add a `$migrations = [ '6.0.0' => 'migrate_to_6', ... ]` map and walk it ordered by version.
5. **`upgrade_plugin` (Lite) can fail silently.** The `make_lite_available` flow attempts an upgrade but doesn't surface failures. If wp.org is unreachable or filesystem permissions block the upgrade, the user has no signal.
6. **`set_default_values()` is in `Traits/Core.php` but called from `Migration` (and from Bootstrap during init).** Two call sites for the same operation. On activation, it writes initial values. On every request, Bootstrap also calls it (via `register_hooks`). Verify there's no double-write race during activation.
7. **The `wp_clear_scheduled_hook` calls in deactivation only clear `eael_sync_*` hooks.** If Pro registers additional cron hooks (e.g. license check, future telemetry), they won't be cleared on deactivation. Add a per-feature inventory of registered cron hooks and clear them all.
8. **No notice when `make_lite_available()` fails.** If wp.org is down, install fails silently. Admin sees no message, just no Lite. Surface via `Notice` class.
9. **`upgrader_process_complete` callback empties the asset cache.** What about the `_elementor_css` post meta cache or transient? When a Pro asset path changes between versions, Elementor's per-post CSS cache may reference the old path. Verify whether Lite's `Asset_Builder` invalidates that — and if not, document the gap.

## Acceptance

This doc accurately reflects:

- Migration is hooked to 4 lifecycle points (activation, deactivation, upgrade, `wp_loaded`) ✓
- `migrator()` runs every request and checks `eael_pro_version` option ✓
- `make_lite_available()` is the auto-install/upgrade/activate flow for Lite ✓
- Deactivation extras (option delete + cron clear for `eael_sync_*`) live in the entry file callback, not in the Migration method ✓
- Development environment is exempted from auto-Lite-install behaviour ✓
- No uninstall handler today ✓
- No version-keyed migration steps today ✓

If the migration flow changes, update this doc in the same PR.

## Pairs with

- `pro-lite-bridge.md` — Lite version gate (`> 4.6.3`) is checked at runtime; `make_lite_available` ensures Lite is installed in the first place
- `asset-loading.md` — `EAEL_ASSET_PATH` cache lives in Lite; Pro's migration clears it on activation, deactivation, and upgrade
- `external-api-integrations.md` — `get_plugin_data()` hits `api.wordpress.org` via the Library trait (RCE-risk path)

## Related

- WordPress Plugin Handbook — activation, deactivation, uninstall hooks
- `includes/Traits/Library.php` — the helpers Migration uses (`empty_dir`, `install_plugin`, `upgrade_plugin`, `get_plugin_data`)
- `includes/Traits/Core.php` — `set_default_values`, `make_lite_available`

## Out of scope

- Lite's own activation / deactivation / migration flow
- Elementor's plugin update mechanism (managed by Elementor)
- WP-CLI–driven activation (different code path; not covered here)
