PluginProbe
WP-Stateless – Google Cloud Storage / trunk
WP-Stateless – Google Cloud Storage vtrunk
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / lib / classes / class-migrator.php

class-migrator.php in WP-Stateless – Google Cloud Storage trunk, at lib/classes/class-migrator.php

419 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Migrations manager
4 *
5 * @since 4.0.0
6 */
7
8 namespace wpCloud\StatelessMedia;
9
10 use wpCloud\StatelessMedia\Batch\BatchTaskManager;
11
12 class Migrator {
13 use Singleton;
14
15 const MIGRATIONS_KEY = 'sm_migrations';
16 const MIGRATIONS_NOTIFY_KEY = 'sm_migrations_notify';
17 const MIGRATIONS_NOTIFY_DISMISSED_KEY = 'dismissed_notice_migrations-finished';
18
19 const NOTIFY_REQUIRE = 'require';
20 const NOTIFY_FINISHED = 'finished';
21
22 const STATUS_PENDING = 'pending';
23 const STATUS_RUNNING = 'running';
24 const STATUS_PAUSED = 'paused';
25 const STATUS_SKIPPED = 'skipped';
26 const STATUS_FINISHED = 'finished';
27 const STATUS_FAILED = 'failed';
28
29 /**
30 * Path to migrations directory
31 *
32 * @var string
33 */
34 private $path;
35
36 protected function __construct() {
37 $this->path = ud_get_stateless_media()->path('static/migrations', 'dir');
38
39 $this->_init_hooks();
40 }
41
42 /**
43 * Initializes the needed hooks
44 */
45 private function _init_hooks() {
46 add_action( 'init', [$this, 'show_messages'] );
47 add_action( 'wp_stateless_batch_task_started', [$this, 'migration_started'], 10, 2 );
48 add_action( 'wp_stateless_batch_task_failed', [$this, 'migration_failed'], 10, 3 );
49 add_action( 'wp_stateless_batch_task_finished', [$this, 'migration_finished'], 10, 2 );
50 add_filter( 'wp_stateless_batch_action_start', [$this, 'start_migration'], 10, 2);
51 add_action( 'wp_stateless_notice_dismissed', [$this, 'notice_dismissed'], 10, 1 );
52 add_filter( 'wp_stateless_get_migrations', [$this, 'get_migrations']);
53 }
54
55 /**
56 * Get migration ID from file name
57 *
58 * @param string $file
59 * @return string
60 */
61 private function _file_to_id($file) {
62 return pathinfo($file, PATHINFO_FILENAME);
63 }
64
65 /**
66 * Get migration ID from class name
67 *
68 * @param string $class
69 * @return string
70 */
71 private function _class_to_id($class) {
72 return str_replace('Migration_', '', $class);
73 }
74
75 /**
76 * Get migration class name from ID
77 *
78 * @param string $id
79 * @return string
80 */
81 private function _id_to_class($id) {
82 return "\Migration_$id";
83 }
84
85 /**
86 * Get migration file name from ID
87 *
88 * @param string $id
89 * @return string
90 */
91 private function _id_to_file($id) {
92 return "$this->path/$id.php";
93 }
94
95 /**
96 * Compares the list of files in the migrations directory with the list of finished migrations
97 * Finds the oldest migration that has not been run yet
98 *
99 * @return array
100 */
101 private function _get_migration_ids() {
102 if ( !is_dir($this->path) ) {
103 return [];
104 }
105
106 $ids = [];
107 $files = scandir($this->path, SCANDIR_SORT_ASCENDING);
108
109 foreach ($files as $file) {
110 $extension = pathinfo($file, PATHINFO_EXTENSION);
111
112 if ( $extension !== 'php' ) {
113 continue;
114 }
115
116 $ids[] = $this->_file_to_id($file);
117 }
118
119 return $ids;
120 }
121
122 /**
123 * Returns the migration object
124 *
125 * @param string $id
126 * @return wpCloud\StatelessMedia\Batch\Migration
127 * @throws \Exception
128 */
129 private function _get_object($id) {
130 $class = $this->_id_to_class($id);
131
132 if ( !class_exists($class) ) {
133 require_once $this->_id_to_file($id);
134 }
135
136 $object = new $class();
137
138 if ( !is_a($object, '\wpCloud\StatelessMedia\Batch\Migration') ) {
139 throw new \Exception("$class is not a valid migration");
140 }
141
142 $object->init_state();
143
144 return $object;
145 }
146
147 /**
148 * Checks if any migrations required and sets or removes global flag
149 *
150 * @param array $migrations|null
151 */
152 private function _check_required_migrations($migrations = null) {
153 if ( empty($migrations) ) {
154 $migrations = apply_filters('wp_stateless_get_migrations', []);
155 }
156
157 $require_migrations = false;
158
159 foreach ($migrations as $id => $migration) {
160 if ( !in_array( $migration['status'], [self::STATUS_FINISHED, self::STATUS_SKIPPED] ) ) {
161 $require_migrations = true;
162 break;
163 }
164 }
165
166 if ( $require_migrations ) {
167 update_option(self::MIGRATIONS_NOTIFY_KEY, self::NOTIFY_REQUIRE);
168 delete_option(self::MIGRATIONS_NOTIFY_DISMISSED_KEY);
169 } else {
170 $notify = get_option(self::MIGRATIONS_NOTIFY_KEY, false);
171
172 empty($notify) ? delete_option(self::MIGRATIONS_NOTIFY_KEY) : update_option(self::MIGRATIONS_NOTIFY_KEY, self::NOTIFY_FINISHED);
173 }
174 }
175
176 /**
177 * Dismisses the migration notice
178 *
179 * @param string $option_name
180 */
181 public function notice_dismissed($option_name) {
182 delete_option(self::MIGRATIONS_NOTIFY_KEY);
183 }
184
185 /**
186 * Generates an updated list of migrations.
187 * Checks which migrations should run.
188 * Sets global options to display the requirement to run migrations.
189 *
190 * Is called by Bootstrap object during version upgrade on 'plugins_loaded' hook.
191 */
192 public function migrate() {
193 // Rebuild the migrations list and state according to the new version
194 $ids = $this->_get_migration_ids();
195
196 $migrations = apply_filters('wp_stateless_get_migrations', []);
197 $existing = array_keys($migrations);
198
199 foreach ($ids as $id) {
200 if ( in_array($id, $existing) ) {
201 continue;
202 }
203
204 try {
205 $object = $this->_get_object($id);
206 $skip = !$object->should_run();
207
208 $migrations[$id] = [
209 'description' => $object->get_description(),
210 'started' => '',
211 'finished' => '',
212 'status' => $object->should_run() ? self::STATUS_PENDING : self::STATUS_SKIPPED,
213 'message' => '',
214 ];
215
216 } catch (\Throwable $e) {
217 Helper::log("Unable to initialize migration $id: " . $e->getMessage());
218 }
219 }
220
221 krsort($migrations);
222
223 update_option(self::MIGRATIONS_KEY, $migrations);
224
225 // Check if we need to run any migrations
226 $this->_check_required_migrations($migrations);
227 }
228
229 /**
230 * Outputs the message that migrations are required
231 */
232 public function show_messages() {
233 if ( is_network_admin() ) {
234 return;
235 }
236
237 $is_running = BatchTaskManager::instance()->is_processing() || BatchTaskManager::instance()->is_paused();
238 $notify = get_option(self::MIGRATIONS_NOTIFY_KEY, false);
239
240 if ( $notify ) {
241 ud_get_stateless_media()->errors->add([
242 'title' => __('WP-Stateless: Data Optimization Required', ud_get_stateless_media()->domain),
243 'message' => __('WP-Stateless has been updated! Your WP-Stateless data must now be optimized. <strong>Please backup your database before proceeding with the optimization.</strong>', ud_get_stateless_media()->domain),
244 'button' => __('Optimize Data', ud_get_stateless_media()->domain),
245 'button_link' => admin_url('upload.php?page=stateless-settings&tab=stless_status_tab#migration-action'),
246 'key' => 'migrations-required',
247 'dismiss' => false,
248 'classes' => ($notify == self::NOTIFY_REQUIRE) && !$is_running ? '' : 'hidden',
249 ], 'warning');
250
251 ud_get_stateless_media()->errors->add([
252 'title' => __('WP-Stateless: Data Optimization in Progress', ud_get_stateless_media()->domain),
253 'message' => __('A background process is optimizing your WP-Stateless data. <strong>Please do not upload, change, or delete your media while this update is underway.</strong>', ud_get_stateless_media()->domain),
254 'button' => __('View Progress', ud_get_stateless_media()->domain),
255 'button_link' => admin_url('upload.php?page=stateless-settings&tab=stless_status_tab#migration-action'),
256 'key' => 'migrations-running',
257 'dismiss' => false,
258 'classes' => $is_running ? '' : 'hidden',
259 'capability' => 'upload_files',
260 'button_capability' => 'manage_options',
261 ], 'warning');
262
263 ud_get_stateless_media()->errors->add([
264 'title' => __('WP-Stateless: Data Optimization Complete', ud_get_stateless_media()->domain),
265 'message' => __('Your WP-Stateless data has been optimized. You can now continue using your media as usual.', ud_get_stateless_media()->domain),
266 'key' => 'migrations-finished',
267 'classes' => ($notify == self::NOTIFY_FINISHED) && !$is_running ? '' : 'hidden',
268 ], 'warning');
269 }
270 }
271
272 /**
273 * Mark migration as started
274 *
275 * @param string $class
276 * @param string $file
277 */
278 public function migration_started($class, $file) {
279 $migrations = apply_filters('wp_stateless_get_migrations', []);
280 $id = $this->_file_to_id($file);
281
282 if ( array_key_exists($id, $migrations) ) {
283 $migrations[$id]['status'] = self::STATUS_RUNNING;
284 $migrations[$id]['started'] = time();
285 $migrations[$id]['finished'] = '';
286
287 update_option(self::MIGRATIONS_KEY, $migrations);
288 }
289 }
290
291 /**
292 * Mark migration as failed and check other migrations
293 *
294 * @param string $class
295 * @param string $file
296 * @param string $message
297 */
298 public function migration_failed($class, $file, $message) {
299 $migrations = apply_filters('wp_stateless_get_migrations', []);
300 $id = $this->_file_to_id($file);
301
302 if ( array_key_exists($id, $migrations) ) {
303 $migrations[$id]['status'] = self::STATUS_FAILED;
304 $migrations[$id]['message'] = $message;
305
306 update_option(self::MIGRATIONS_KEY, $migrations);
307 $this->_check_required_migrations($migrations);
308 }
309 }
310
311 /**
312 * Mark migration as completed and check other migrations
313 *
314 * @param string $class
315 */
316 public function migration_finished($class, $state) {
317 $migrations = apply_filters('wp_stateless_get_migrations', []);
318 $id = $this->_class_to_id($class);
319
320 if ( array_key_exists($id, $migrations) ) {
321 $migrations[$id]['status'] = self::STATUS_FINISHED;
322 $migrations[$id]['finished'] = time();
323
324 update_option(self::MIGRATIONS_KEY, $migrations);
325 $this->_check_required_migrations($migrations);
326 }
327
328 // When started from the UI, run next migration if needed
329 if ( !empty($state['queue']) && is_array($state['queue']) ) {
330 $index = array_search($id, $state['queue']);
331 $next_index = false;
332
333 if ( $index !== false && isset($state['queue'][$index + 1]) ) {
334 $next_index = $state['queue'][$index + 1];
335 }
336
337 if ( $next_index === false ) {
338 return;
339 }
340
341 $params = [
342 'is_migration' => true,
343 'id' => $next_index,
344 'email' => $state['email'],
345 'queue' => implode(':', $state['queue']),
346 'action' => 'start',
347 ];
348
349 apply_filters("wp_stateless_batch_action_start", [], $params);
350 }
351 }
352
353 /**
354 * Run migration
355 *
356 * @param array $state
357 * @param array $params
358 * @return array
359 * @throws \Exception
360 */
361 public function start_migration($state, $params) {
362 // Possibly not migration action
363 if ( empty($params['is_migration']) || empty($params['id']) || !$params['is_migration'] ) {
364 return $state;
365 }
366
367 $id = $params['id'];
368 $migrations = apply_filters('wp_stateless_get_migrations', []);
369
370 // Unknown migration?
371 if ( !array_key_exists($id, $migrations) ) {
372 return $state;
373 }
374
375 $class = $this->_id_to_class($id);
376 $file = $this->_id_to_file($id);
377
378 // Still possibly not migration action
379 if ( !file_exists($file) ) {
380 return $state;
381 }
382
383 if ( $migrations[$id]['status'] !== self::STATUS_PENDING && !isset($params['force']) ) {
384 Helper::log("Migration $id is already started or finished. Status: " . $migrations[$id]['status']);
385
386 return $state;
387 }
388
389 if ( BatchTaskManager::instance()->is_running() ) {
390 Helper::log('Another batch task is already running');
391
392 return $state;
393 }
394
395 $email = $params['email'] ?? '';
396 $queue = isset($params['queue']) ? explode(':', $params['queue']) : [];
397
398 BatchTaskManager::instance()->start_task($class, $file, $email, $queue);
399
400 return apply_filters('wp_stateless_batch_state', $state, []);
401 }
402
403 /**
404 * Get the list of migrations
405 *
406 * @param array $migrations
407 * @return array
408 */
409 public function get_migrations($migrations) {
410 // We need to omit the cache and get the data directly from the db
411 global $wpdb;
412
413 $sql = $wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = '%s' LIMIT 1", self::MIGRATIONS_KEY);
414 $migrations = $wpdb->get_var($sql);
415
416 return empty($migrations) ? [] : maybe_unserialize($migrations);
417 }
418 }
419