PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.1
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / migrations.php

migrations.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.1, at includes/migrations.php

502 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — one-time data migrations.
4 *
5 * A tiny, option-versioned migration runner modeled on the lazy schema
6 * installer in `includes/desktop-files/schema.php`: a stored option holds
7 * the highest migration version that has run; on every admin load we
8 * compare it against {@see OPENSTATION_MIGRATION_VERSION} and run any
9 * pending migrations exactly once. Guarded so it is a cheap no-op after
10 * the first successful pass.
11 *
12 * On a site with no history the runner fires at activation instead, so
13 * nothing here ever has to infer the past of a site from evidence that
14 * site wrote after it was installed. See
15 * {@see openstation_run_migrations_on_activation}.
16 *
17 * @package OpenStation
18 */
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Highest migration version shipped by the plugin.
24 *
25 * Bump this (and add a matching branch in
26 * {@see openstation_run_pending_migrations}) whenever a new one-time
27 * migration is needed.
28 *
29 * A new migration runs on every install, including brand-new ones: on a
30 * site with no history the runner fires at activation
31 * ({@see openstation_run_migrations_on_activation}) rather than on the
32 * first `admin_init`.
33 *
34 * - 1: native list windows flipped from opt-out (default ON) to opt-in
35 * Beta (default OFF). Clears the five `native*Enabled` flags from every
36 * user who had them persisted so the whole install reverts to opt-in.
37 * - 2: post & taxonomy-term AI analysis was removed (the copilot now only
38 * analyzes comments for spam, and the assistant finds content via native
39 * WordPress search). Unschedules any queued `desktop_mode_ai_analyze_post`
40 * / `desktop_mode_ai_analyze_term` cron events left over from prior versions.
41 * - 3: the copilot dropped its self-managed AI credentials in favour of
42 * WordPress 7.0 Connectors. Deletes the platform key option and strips the
43 * per-user `apiKey` / `apiKeys` / `provider` / `transport` fields from the
44 * stored OS settings so no provider secret lingers in the database.
45 * - 4: the OpenStation brand. Moves anyone still sitting on the PRE-brand
46 * defaults — accent `wp-blue`, wallpaper `dark` — onto the new ones,
47 * Pulse and Galaxy. Without it the rebrand only reaches fresh accounts:
48 * the stored snapshot is authoritative over the shipped default, so an
49 * existing desk keeps a blue accent on every focus ring, tab underline
50 * and sort arrow.
51 * - 5: flags the users who were using Desktop Mode before the rename, so
52 * the shell can explain the new name once to the people it happened
53 * to and to nobody else. Sets user meta and nothing else — see
54 * {@see openstation_migrate_flag_rebrand_notice} for why that is a
55 * separate migration from 4.
56 */
57 const OPENSTATION_MIGRATION_VERSION = 5;
58
59 /**
60 * Option storing the highest migration version that has run. autoload=no.
61 *
62 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
63 * persisted or externally-visible identifier, so renaming it would
64 * orphan data already written by live installs (or break a live
65 * URL). The mismatch between this constant's name and its value is
66 * deliberate — it is NOT a half-finished rename.
67 */
68 const OPENSTATION_MIGRATION_OPTION = 'desktop_mode_migration_version';
69
70 /**
71 * Runs any pending migrations, then records the new high-water mark.
72 *
73 * Idempotent: bails immediately when the stored version is already at
74 * or above the shipped version, so it is safe to fire on every request.
75 *
76 * @return void
77 */
78 function openstation_maybe_run_migrations() {
79 $installed = (int) get_option( OPENSTATION_MIGRATION_OPTION, 0 );
80 if ( $installed >= OPENSTATION_MIGRATION_VERSION ) {
81 return;
82 }
83
84 openstation_run_pending_migrations( $installed );
85
86 update_option( OPENSTATION_MIGRATION_OPTION, OPENSTATION_MIGRATION_VERSION, false );
87 }
88 add_action( 'admin_init', 'openstation_maybe_run_migrations' );
89
90 /**
91 * Runs the pending migrations at activation, on a site with no history.
92 *
93 * Migration 5 infers who used the shell before the rename from user meta
94 * that a site can write to itself between activation and the first
95 * `admin_init` (the portal auto-enable). Running at activation is the
96 * one moment that window is still shut, so the same runner reaches the
97 * same conclusion about the same site and cannot be fooled by evidence
98 * that arrives later.
99 *
100 * The whole runner, not a subset: migrations 2 and 3 clear leftover AI
101 * cron events and a stored provider credential, neither of which any
102 * user meta predicts.
103 *
104 * @return void
105 */
106 function openstation_run_migrations_on_activation() {
107 // Migrations have already run here; their high-water mark is the
108 // truth and the runner would be a no-op anyway.
109 if ( false !== get_option( OPENSTATION_MIGRATION_OPTION, false ) ) {
110 return;
111 }
112
113 // The site has history, so this is a reactivation and not a new
114 // install. Leave it to `admin_init`, where migration 5 reads meta
115 // that is genuinely older than this request.
116 $prior_users = openstation_users_with_prior_desktop_use();
117 if ( ! empty( $prior_users ) ) {
118 return;
119 }
120
121 openstation_maybe_run_migrations();
122 }
123 register_activation_hook( OPENSTATION_FILE, 'openstation_run_migrations_on_activation' );
124
125 /**
126 * Dispatches each migration whose version is newer than what has run.
127 *
128 * @param int $from The highest migration version already applied.
129 * @return void
130 */
131 function openstation_run_pending_migrations( $from ) {
132 $from = (int) $from;
133
134 if ( $from < 1 ) {
135 openstation_migrate_os_settings_optin();
136 }
137
138 if ( $from < 2 ) {
139 openstation_migrate_unschedule_post_term_ai();
140 }
141
142 if ( $from < 3 ) {
143 openstation_migrate_delete_ai_keys();
144 }
145
146 if ( $from < 4 ) {
147 openstation_migrate_brand_defaults();
148 }
149
150 if ( $from < 5 ) {
151 openstation_migrate_flag_rebrand_notice( $from );
152 }
153 }
154
155 /**
156 * User meta marking someone as a Desktop Mode user from before the rebrand.
157 *
158 * Present and truthy => the shell offers this user the one-off rebrand
159 * announcement, once. Absent => they never used the plugin under its old
160 * name, so there is no rename to explain to them. Written only by
161 * migration 5, and only for users who were actually using Desktop Mode
162 * at the moment it ran.
163 *
164 * The VALUE keeps the pre-rebrand spelling for the reason every other
165 * stored key does — see {@see OPENSTATION_MIGRATION_OPTION}.
166 */
167 const OPENSTATION_REBRAND_NOTICE_META_KEY = 'desktop_mode_rebrand_notice';
168
169 /**
170 * Slug the rebrand announcement records in `desktop_mode_seen_intros`.
171 *
172 * A slug in the shared registry rather than a bespoke meta key, so the
173 * announcement is dismissed, reset and reasoned about exactly like the
174 * native-window intros beside it.
175 */
176 const OPENSTATION_REBRAND_INTRO_SLUG = 'openstation-rebrand';
177
178 /**
179 * Every user who carries proof of having used the shell on this site:
180 * `desktop_mode_mode` (the per-user opt-in, tested for EXISTENCE rather
181 * than for being `'1'`, since switching back to classic empties the
182 * value but leaves the row) or a saved `desktop_mode_os_settings`.
183 *
184 * @return int[] User IDs, unsorted and deduplicated.
185 */
186 function openstation_users_with_prior_desktop_use() {
187 return array_map(
188 'intval',
189 array_unique(
190 array_merge(
191 get_users(
192 array(
193 'fields' => 'ID',
194 'meta_key' => 'desktop_mode_mode', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- runs once per install; the key is indexed in usermeta and both callers are guarded to a single pass.
195 'meta_compare' => 'EXISTS',
196 )
197 ),
198 get_users(
199 array(
200 'fields' => 'ID',
201 'meta_key' => OPENSTATION_OS_SETTINGS_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- see above.
202 'meta_compare' => 'EXISTS',
203 )
204 )
205 )
206 )
207 );
208 }
209
210 /**
211 * Migration 5 — remember who was using Desktop Mode before the rebrand.
212 *
213 * Migration 4 moved the pre-brand *defaults* onto the brand ones. This
214 * one answers a different question: not "what should this desk look
215 * like" but "does this person need to be told why it changed". Someone
216 * who has been running Desktop Mode for months opens wp-admin one
217 * morning to a differently-named, differently-coloured shell; without a
218 * word of explanation that reads as a compromised site, not a release.
219 *
220 * Two gates. The install gate is a bare "has the rebrand already
221 * happened here", and the user gate does the actual work.
222 *
223 * **The install** must not already be past the rebrand: `$from < 4`. A
224 * `4` means migration 4 has run, which today means a checkout tracking
225 * trunk between the two release tags. Not a surprised user.
226 *
227 * Note what is deliberately NOT tested: whether `$from` is zero. It is
228 * tempting to read `0` as "fresh install, nothing to explain", and that
229 * reading is wrong in the one direction that matters. The migration
230 * runner itself only shipped in 0.9.1, so an install still on 0.9.0 or
231 * earlier that updates straight to the rebrand release has no stored
232 * version at all and arrives here with `$from === 0`, indistinguishable
233 * from a brand new site. Those are the installs that update rarely,
234 * which makes them the ones most likely to be blindsided by a rename,
235 * and gating on `$from > 0` would have silenced precisely them.
236 *
237 * **The user** has to have actually used it — see
238 * {@see openstation_users_with_prior_desktop_use} for what counts as
239 * proof. That separates a long-dormant install from a genuinely new one
240 * without needing to date the install at all, and it keeps the
241 * announcement away from an editor who joined an old site last week and
242 * enabled OpenStation this morning.
243 *
244 * What that gate does NOT do on its own is prove the evidence is old.
245 * On a new install it can be written between activation and the first
246 * `admin_init`, and then read back here as history. That window is
247 * closed by {@see openstation_run_migrations_on_activation}, which runs
248 * this migration before anything can write it.
249 *
250 * Deliberately NOT folded into migration 4, even though the two ship
251 * together: 4 has already run on trunk checkouts, and a migration that
252 * has run does not run again. Extending it would have silently skipped
253 * the flag exactly where it was easiest to believe it had been set.
254 *
255 * Flags are never cleared. Dismissal lives in the seen-intros registry,
256 * so one admin dismissing the announcement does not silence it for
257 * their editors, and "Reset what's-new dialogs" in OpenStation Preferences
258 * → Features brings it back with every other intro.
259 *
260 * @param int $from The highest migration version already applied.
261 * @return void
262 */
263 function openstation_migrate_flag_rebrand_notice( $from ) {
264 if ( (int) $from >= 4 ) {
265 return;
266 }
267
268 foreach ( openstation_users_with_prior_desktop_use() as $user_id ) {
269 update_user_meta( $user_id, OPENSTATION_REBRAND_NOTICE_META_KEY, 1 );
270 }
271 }
272
273 /**
274 * Whether the current user should be offered the rebrand announcement.
275 *
276 * Two gates: migration 5 flagged this user as one who was using Desktop
277 * Mode before the rename, and they have not already dismissed it. The
278 * seen-intros registry owns the second one, which is what makes the
279 * announcement behave like every other one-time dialog — including
280 * being brought back by "Reset what's-new dialogs".
281 *
282 * Only ever consulted while building the shell config, which is itself
283 * behind the `openstation_is_enabled()` / not-classic guard in
284 * `includes/render/assets.php`. So the announcement cannot reach the
285 * classic admin or a chromeless iframe: the bundle that would show it
286 * is not loaded there.
287 *
288 * @return bool
289 */
290 function openstation_should_show_rebrand_notice() {
291 $user_id = get_current_user_id();
292 if ( ! $user_id ) {
293 return false;
294 }
295
296 if ( ! get_user_meta( $user_id, OPENSTATION_REBRAND_NOTICE_META_KEY, true ) ) {
297 return false;
298 }
299
300 return ! openstation_has_seen_intro( $user_id, OPENSTATION_REBRAND_INTRO_SLUG );
301 }
302
303 /**
304 * Migration 4 — move the pre-brand defaults onto the OpenStation ones.
305 *
306 * The stored OS-settings snapshot outranks the shipped default, so
307 * changing `openstation_default_os_settings()` reaches new accounts and
308 * nobody else. Every existing desk would keep `wp-blue` on its focus
309 * rings, tab underlines, sort arrows and selection washes, and keep the
310 * graphite `dark` desk under the station's chrome — a half-applied
311 * rebrand, which reads as a bug rather than as a choice.
312 *
313 * **Only values still equal to the OLD default are touched.** A user who
314 * picked Indigo, or the Snow wallpaper, expressed a preference and keeps
315 * it. The one unavoidable cost is the user who deliberately chose
316 * WordPress Blue — indistinguishable from never having chosen at all,
317 * because it WAS the default — and for them it is one click in
318 * OS Settings → Appearance to set it back.
319 *
320 * Users with no stored settings are skipped entirely: they read the new
321 * defaults already.
322 *
323 * @return void
324 */
325 function openstation_migrate_brand_defaults() {
326 // The pre-brand => brand value map, keyed by OS-settings field.
327 // Deliberately not filterable: this runs once, against one release's
328 // stored defaults, and a third party rewriting which values get
329 // migrated would leave desks in a state no later migration accounts
330 // for.
331 $map = array(
332 'accent' => array(
333 'from' => 'wp-blue',
334 'to' => 'pulse',
335 ),
336 'wallpaper' => array(
337 'from' => 'dark',
338 'to' => 'galaxy',
339 ),
340 );
341
342 $user_ids = get_users(
343 array(
344 'fields' => 'ID',
345 'meta_key' => OPENSTATION_OS_SETTINGS_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one-time migration; the key is indexed in usermeta and the scan is guarded to run once.
346 'meta_compare' => 'EXISTS',
347 )
348 );
349
350 foreach ( $user_ids as $user_id ) {
351 $raw = get_user_meta( (int) $user_id, OPENSTATION_OS_SETTINGS_META_KEY, true );
352 if ( ! is_array( $raw ) ) {
353 continue;
354 }
355
356 $changed = false;
357 foreach ( $map as $key => $move ) {
358 if ( ! isset( $move['from'], $move['to'] ) ) {
359 continue;
360 }
361 // An absent key already resolves to the new default.
362 if ( isset( $raw[ $key ] ) && $move['from'] === $raw[ $key ] ) {
363 $raw[ $key ] = $move['to'];
364 $changed = true;
365 }
366 }
367
368 if ( $changed ) {
369 openstation_save_os_settings( (int) $user_id, $raw );
370 }
371 }
372 }
373
374 /**
375 * Migration 1 — reset the native list windows to opt-in.
376 *
377 * The native Posts/Pages/Users/Plugins/Comments windows used to default
378 * ON (opt-out). The shell persists the whole OS-settings object on every
379 * change, so most active users already have these flags stored as `true`
380 * and would keep the native UI even after the default flips. This clears
381 * the five flags from every user who has the meta, leaving the rest of
382 * their settings (wallpaper, accent, dock order, …) untouched. On the
383 * next read the cleared keys fall back to the new `false` default, so the
384 * whole install lands on opt-in and users re-enable each window from
385 * OS Settings → Features → Beta features.
386 *
387 * Only users who actually have the meta are queried — fresh accounts and
388 * users who never touched OS Settings are skipped entirely.
389 *
390 * @return void
391 */
392 function openstation_migrate_os_settings_optin() {
393 $flags = array(
394 'nativePostsEnabled',
395 'nativePagesEnabled',
396 'nativeUsersEnabled',
397 'nativePluginsEnabled',
398 'nativeCommentsEnabled',
399 );
400
401 $user_ids = get_users(
402 array(
403 'fields' => 'ID',
404 'meta_key' => OPENSTATION_OS_SETTINGS_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one-time migration; the key is indexed in usermeta and the scan is guarded to run once.
405 'meta_compare' => 'EXISTS',
406 )
407 );
408
409 foreach ( $user_ids as $user_id ) {
410 $raw = get_user_meta( (int) $user_id, OPENSTATION_OS_SETTINGS_META_KEY, true );
411 if ( ! is_array( $raw ) ) {
412 continue;
413 }
414
415 $changed = false;
416 foreach ( $flags as $flag ) {
417 if ( array_key_exists( $flag, $raw ) ) {
418 unset( $raw[ $flag ] );
419 $changed = true;
420 }
421 }
422
423 if ( ! $changed ) {
424 continue;
425 }
426
427 // Re-save through the canonical sanitizer so the cleared flags are
428 // backfilled with the new `false` default and the rest of the
429 // settings array is normalized exactly as a client write would be.
430 openstation_save_os_settings( (int) $user_id, $raw );
431 }
432 }
433
434 /**
435 * Migration 2 — unschedule leftover post/term AI analysis jobs.
436 *
437 * Post and taxonomy-term analysis was removed: the copilot now only
438 * analyzes comments (for the spam score), and the AI assistant finds
439 * content with native WordPress keyword search. Their cron callbacks no
440 * longer exist, so any single-events still queued from a prior version
441 * would simply no-op — but we clear them so the cron array stays tidy and
442 * `wp cron event list` doesn't show orphaned hooks.
443 *
444 * Existing `_desktop_mode_ai_analysis` meta on posts/terms is left in place
445 * (hidden, harmless, and cheap to ignore).
446 *
447 * @return void
448 */
449 function openstation_migrate_unschedule_post_term_ai() {
450 wp_unschedule_hook( 'desktop_mode_ai_analyze_post' );
451 wp_unschedule_hook( 'desktop_mode_ai_analyze_term' );
452 }
453
454 /**
455 * Migration 3 — delete self-managed AI credentials.
456 *
457 * WordPress 7.0 owns provider credentials (Settings → Connectors), so the
458 * copilot no longer stores keys of its own. Remove the platform key option and
459 * strip the now-unused key / provider / model / transport fields from every
460 * user's stored OS settings so no secret is left behind. The only `ai` field
461 * that remains is `enabled` (the per-user assistant toggle), backfilled from
462 * defaults on next read.
463 *
464 * @return void
465 */
466 function openstation_migrate_delete_ai_keys() {
467 // Platform-wide key option (formerly `desktop_mode_ai_platform`).
468 delete_option( 'desktop_mode_ai_platform' );
469
470 $user_ids = get_users(
471 array(
472 'fields' => 'ID',
473 'meta_key' => OPENSTATION_OS_SETTINGS_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one-time migration; guarded to run once.
474 'meta_compare' => 'EXISTS',
475 )
476 );
477
478 foreach ( $user_ids as $user_id ) {
479 $raw = get_user_meta( (int) $user_id, OPENSTATION_OS_SETTINGS_META_KEY, true );
480 if ( ! is_array( $raw ) || ! isset( $raw['ai'] ) || ! is_array( $raw['ai'] ) ) {
481 continue;
482 }
483
484 // Strip every legacy AI field: the self-managed credentials/transport,
485 // plus the `provider` / `model` preferences — provider + model selection
486 // is now delegated entirely to the Core AI Client.
487 $changed = false;
488 foreach ( array( 'apiKey', 'apiKeys', 'transport', 'provider', 'model' ) as $stale ) {
489 if ( array_key_exists( $stale, $raw['ai'] ) ) {
490 unset( $raw['ai'][ $stale ] );
491 $changed = true;
492 }
493 }
494
495 if ( ! $changed ) {
496 continue;
497 }
498
499 openstation_save_os_settings( (int) $user_id, $raw );
500 }
501 }
502