PluginProbe
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder / 3.6.0
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder v3.6.0
3.6.0 3.5.0 trunk 1.0.10 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.6.1 1.0.7 1.0.8 1.0.9 1.1 1.1.1 1.2 1.3 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5 1.5.1 All 110 releases
buttonizer-multifunctional-button / app / Migration / MigrationManager.php

MigrationManager.php in Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder 3.6.0, at app/Migration/MigrationManager.php

293 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * SOFTWARE LICENSE INFORMATION
4 *
5 * Copyright (c) 2017 Buttonizer, all rights reserved.
6 *
7 * This file is part of Buttonizer
8 *
9 * For detailed information regarding to the licensing of
10 * this software, please review the license.txt or visit:
11 * https://buttonizer.pro/license/
12 */
13
14 namespace Buttonizer\Migration;
15
16 # No script kiddies
17 defined('ABSPATH') or die('No script kiddies please!');
18
19 /**
20 * Runs the migration of acquired plugins into Buttonizer.
21 *
22 * Order matters and is the whole safety story:
23 *
24 * detect → asked? → back up → adopt the connection → verify → deactivate → record
25 *
26 * Nothing destructive happens before the backup exists, and the source plugin
27 * is never deactivated until Buttonizer is verifiably able to serve its users.
28 * And none of it starts on Buttonizer's own initiative: an installed source is
29 * only absorbed after its own notice wrote down that the user asked for it.
30 */
31 class MigrationManager
32 {
33 /** The source plugin's own system is now served from modules/. */
34 const RESULT_LEGACY_MODULE = 'legacy_module_embedded';
35
36 /** Nothing was done: this build carries no module for that system. */
37 const RESULT_MODULE_MISSING = 'module_missing';
38
39 /**
40 * Register the migration hooks.
41 *
42 * Runs on admin_init rather than on activation: it covers the plugin being
43 * activated (the redirect right after activation is an admin request), the
44 * source plugin showing up later, and a failed run being retried — without
45 * deactivating plugins from inside another plugin's activation.
46 */
47 public static function boot(): void
48 {
49 if (!is_admin()) {
50 return;
51 }
52
53 // Deactivating a plugin in the middle of someone else's AJAX or cron
54 // request is a good way to break unrelated features. Wait for a real
55 // admin page load.
56 if (wp_doing_ajax() || wp_doing_cron()) {
57 return;
58 }
59
60 add_action('admin_init', [AdminNotice::class, 'handleDismiss'], 1);
61 add_action('admin_init', [self::class, 'run'], 5);
62 add_action('admin_init', [self::class, 'redirectSourcePages'], 6);
63 add_action('admin_notices', [AdminNotice::class, 'render']);
64 }
65
66 /**
67 * Evaluate every registered source plugin and migrate what can be migrated.
68 */
69 public static function run(): void
70 {
71 foreach (SourcePlugins::all() as $source) {
72 $state = Detector::detect($source);
73
74 if ($state === Detector::STATE_ALREADY_MIGRATED) {
75 continue;
76 }
77
78 $known = MigrationState::get($source->id());
79
80 // Only write when something actually changed — run() is on every
81 // admin request, and an option write per page load is not free.
82 if (($known['detected_state'] ?? null) !== $state && !($state === Detector::STATE_NOT_PRESENT && empty($known))) {
83 MigrationState::set($source->id(), [
84 'detected_state' => $state,
85 'checked_at' => (new \DateTime('now'))->format(\DateTime::ATOM),
86 ]);
87 }
88
89 if (!in_array($state, [
90 Detector::STATE_CLOUD_CONNECTED,
91 Detector::STATE_CLOUD_DISCONNECTED,
92 Detector::STATE_LEGACY,
93 ], true)) {
94 continue;
95 }
96
97 // Nobody asked. The source plugin writes this before activating
98 // Buttonizer; a build that predates the migration never does, and
99 // neither does a user who installed Buttonizer next to it by hand.
100 // Those sites keep both plugins, exactly as they had them.
101 //
102 // The one thing still worth doing unasked is picking data back up
103 // that Buttonizer had absorbed before and whose plugin is now gone:
104 // the backup is the proof it was ours to serve. Leftovers of a
105 // plugin that was simply deleted are not.
106 if (empty($known['requested']) && ($source->isInstalled() || !Backup::exists($source))) {
107 continue;
108 }
109
110 // A user who cannot manage plugins cannot complete the handover.
111 // Adopting the connection without being able to deactivate the
112 // source would leave both plugins running against the same site.
113 if (!Handoff::currentUserCanManagePlugins()) {
114 continue;
115 }
116
117 self::migrate($source, $state);
118 }
119 }
120
121 /**
122 * Migrate a single source plugin.
123 *
124 * @param SourcePlugin $source Source plugin descriptor.
125 * @param string $state Detected state, as returned by Detector.
126 */
127 private static function migrate(SourcePlugin $source, string $state): void
128 {
129 // Resolve the plugin once, deeply, so the backup and the handoff agree
130 // on which files they are talking about
131 $source->resolveBaseName(true);
132
133 // A legacy install is served by the plugin's own code, so Buttonizer
134 // may only step in once it carries that code itself. Without the
135 // module, deactivating would take the user's buttons off the frontend.
136 if ($state === Detector::STATE_LEGACY && !$source->hasModule()) {
137 MigrationState::set($source->id(), [
138 'status' => MigrationState::STATUS_SKIPPED,
139 'result' => self::RESULT_MODULE_MISSING,
140 ]);
141
142 return;
143 }
144
145 // 1. Everything from here on is reversible
146 Backup::create($source);
147
148 // 2. Take over the connection the user already has. A source that was
149 // never connected has no token, no site and nothing rendering on the
150 // frontend, so there is nothing to adopt and nothing to lose — it still
151 // gets handed over, otherwise the user is left with two active plugins
152 // after being invited to move.
153 $result = $state === Detector::STATE_LEGACY
154 ? self::RESULT_LEGACY_MODULE
155 : ConnectionAdopter::RESULT_NOTHING_TO_ADOPT;
156
157 if ($state === Detector::STATE_LEGACY) {
158 // Remembered so the module knows what a later connection means: a
159 // site that was already on the cloud keeps both, a site that was
160 // not has just decided to move.
161 MigrationState::set($source->id(), [
162 'target_connected' => ConnectionAdopter::isTargetConnected(),
163 ]);
164 }
165
166 if ($state === Detector::STATE_CLOUD_CONNECTED) {
167 $adoption = ConnectionAdopter::adopt($source);
168
169 if (!$adoption['adopted']) {
170 MigrationState::set($source->id(), [
171 'status' => MigrationState::STATUS_FAILED,
172 'result' => $adoption['result'],
173 ]);
174
175 return;
176 }
177
178 // 3. Never hand over on an unverified connection
179 if (!ConnectionAdopter::isTargetConnected()) {
180 MigrationState::set($source->id(), [
181 'status' => MigrationState::STATUS_FAILED,
182 'result' => ConnectionAdopter::RESULT_FAILED,
183 ]);
184
185 return;
186 }
187
188 $result = $adoption['result'];
189 }
190
191 // 4. Step aside
192 $handoff = Handoff::deactivateSource($source);
193
194 if (!$handoff['deactivated'] && $handoff['result'] === Handoff::RESULT_NO_PERMISSION) {
195 MigrationState::set($source->id(), [
196 'status' => MigrationState::STATUS_PENDING,
197 'result' => $handoff['result'],
198 ]);
199
200 return;
201 }
202
203 // 5. Done
204 MigrationState::markMigrated($source->id(), $result);
205
206 // Nothing was actually handed off: the source plugin's files are gone
207 // (deleted, not just deactivated), so this is legacy data being picked
208 // back up, not a migration happening right now. The green notice
209 // announces an event, and none occurred on this request.
210 if (!$source->isInstalled()) {
211 MigrationState::set($source->id(), ['notice_dismissed' => true]);
212 }
213
214 // 6. Start again on a clean request
215 self::land($source, $result);
216 }
217
218 /**
219 * Send the user to where they belong now.
220 *
221 * Everything above happened halfway through a request whose decisions were
222 * already taken: the source plugin registered its menu before it was
223 * deactivated and its entry is still on the screen, and Buttonizer decided
224 * not to serve the embedded module because the plugin was still running
225 * when it loaded. The page being rendered is a state that existed for one
226 * request and is wrong in both directions.
227 *
228 * Rather than patch each symptom, throw the request away. One redirect and
229 * everything is decided from the new truth.
230 *
231 * @param SourcePlugin $source Source plugin descriptor.
232 * @param string $result Migration result.
233 */
234 private static function land(SourcePlugin $source, string $result): void
235 {
236 // Endpoints that are answering something else, and redirects that were
237 // never meant for a person to see
238 if (in_array($GLOBALS['pagenow'] ?? '', ['admin-post.php', 'admin-ajax.php'], true)) {
239 return;
240 }
241
242 // A site whose old system Buttonizer now serves belongs on its own
243 // screens, not on a signup page for a product it has not asked for.
244 $page = $result === self::RESULT_LEGACY_MODULE && $source->modulePageSlug()
245 ? $source->modulePageSlug()
246 : \Buttonizer\Core\PluginConfig::pageSlug();
247
248 wp_safe_redirect(admin_url('admin.php?page=' . $page));
249 exit;
250 }
251
252 /**
253 * Send old admin pages to Buttonizer.
254 */
255 public static function redirectSourcePages(): void
256 {
257 foreach (SourcePlugins::all() as $source) {
258 if (!MigrationState::isMigrated($source->id())) {
259 continue;
260 }
261
262 Handoff::redirectSourcePage($source);
263 }
264 }
265
266 /**
267 * Undo a migration: restore the snapshot and forget the bookkeeping.
268 *
269 * Not wired to any UI — this is the escape hatch for testing and for
270 * support, callable from WP-CLI or a snippet.
271 *
272 * @param string $moduleId Module identifier.
273 *
274 * @return bool Whether a backup was found and restored.
275 */
276 public static function rollback(string $moduleId): bool
277 {
278 $source = SourcePlugins::get($moduleId);
279
280 if (!$source) {
281 return false;
282 }
283
284 if (!Backup::restore($source)) {
285 return false;
286 }
287
288 MigrationState::reset($moduleId);
289
290 return true;
291 }
292 }
293