PluginProbe ʕ •ᴥ•ʔ
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF / trunk
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF vtrunk
2.0.5 trunk 1.3.7 1.4.0 1.4.1 1.4.2 1.4.6 1.5.0 1.5.3 1.5.6 1.5.8 1.6.5 1.6.6 1.6.9 1.7.0 1.7.4 1.8.1 1.8.2 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4
robin-image-optimizer / libs / factory / core / includes / class-factory-migrations.php
robin-image-optimizer / libs / factory / core / includes Last commit date
activation 5 months ago assets-managment 5 months ago components 5 months ago entities 5 months ago premium 5 months ago updates 5 months ago class-check-compatibility.php 6 months ago class-factory-migrations.php 5 months ago class-factory-notices.php 5 months ago class-factory-options.php 4 months ago class-factory-plugin-abstract.php 5 months ago class-factory-plugin-base.php 5 months ago class-factory-requests.php 5 months ago class-factory-requirements.php 5 months ago functions.php 5 months ago index.php 6 months ago
class-factory-migrations.php
541 lines
1 <?php
2
3 namespace WBCR\Factory_600;
4
5 use Exception;
6 use Wbcr_Factory600_Plugin;
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Often when updating plugins, you need to make some changes to the database.
14 * This class automatically checks for plugin migrations and executes them when
15 * updating.
16 *
17 * The class has a debug mode, to enable the debug mode add constants to your plugin:
18 * define ('FACTORY_MIGRATIONS_DEBUG', true) - enables/disables debugging mode
19 * define ('FACTORY_MIGRATIONS_FORCE_OLD_VERSION', '1.1.9') - sets previous version
20 * for the plugin, if constant isn't set, then the previous version is taken from
21 * the database.
22 *
23 * todo: get_option and get_site_option are used because some caching plugins caching options, which causes problems
24 *
25 * @since 4.1.1
26 */
27 class Migrations {
28
29 protected $plugin;
30
31 /**
32 * Migrations constructor.
33 *
34 * @param Wbcr_Factory600_Plugin $plugin
35 *
36 * @throws Exception
37 */
38 public function __construct( Wbcr_Factory600_Plugin $plugin ) {
39
40 $this->plugin = $plugin;
41 $plugin_name = $plugin->getPluginName();
42
43 if ( ! file_exists( $this->plugin->get_paths()->migrations ) ) {
44 throw new Exception( 'Starting with version 4.1.1 of the Core for Factory framework module, you must create a "migrations" folder in the root of your plugin to store the migration of the plugin.' );
45 }
46
47 if ( is_admin() ) {
48 add_action( 'admin_init', [ $this, 'check_migrations' ] );
49
50 add_action( "wbcr/factory/plugin_{$plugin_name}_activated", [ $this, 'activation_hook' ] );
51 add_action( 'wbcr/factory/admin_notices', [ $this, 'debug_bar_notice' ], 10, 2 );
52 add_action( 'wbcr/factory/admin_notices', [ $this, 'migration_error_notice' ], 10, 2 );
53 }
54 }
55
56 /**
57 * @since 4.1.1
58 * @return mixed|void
59 */
60 public function get_plugin_activated_time() {
61 if ( $this->plugin->isNetworkActive() ) {
62 return get_site_option( $this->plugin->getOptionName( 'plugin_activated' ), 0 );
63 }
64
65 return get_option( $this->plugin->getOptionName( 'plugin_activated' ), 0 );
66 }
67
68 /**
69 * Check if migration is necessary for plugin and if there are errors from previous migrations.
70 * In debug mode, migrations are not performed automatically.
71 */
72 public function check_migrations() {
73 if ( $this->is_migration_error() && isset( $_GET['wbcr_factory_fix_migration_error'] ) ) {
74 $this->fix_migration_error();
75 wp_safe_redirect( esc_url_raw( remove_query_arg( 'wbcr_factory_fix_migration_error' ) ) );
76 die();
77 }
78
79 if ( $this->is_debug() && isset( $_GET['wbcr_factory_test_migration'] ) ) {
80 $this->make_migration();
81 wp_safe_redirect( esc_url_raw( remove_query_arg( 'wbcr_factory_test_migration' ) ) );
82 die();
83 }
84
85 if ( $this->need_migration() && ! $this->is_debug() ) {
86 $this->make_migration();
87 }
88 }
89
90 /**
91 * Notification displays the errors of outstanding migrations to fix errors
92 * you need to follow the instructions in the notification and click
93 * "I fixed, confirm migration".
94 *
95 * What is it for. Migrations are performed in background and on some sites,
96 * due to php errors or for some other reason, migration may be
97 * interrupted, because of what plugin will work incorrectly, you may lose settings.
98 *
99 * When creating new migrations, developer will add error handlers,
100 * and framework will intercept them safely for user and display them
101 * in this notice.
102 *
103 * @param array $notices
104 * @param static $plugin_name
105 *
106 * @return array
107 */
108 public function migration_error_notice( $notices, $plugin_name ) {
109
110 if ( $this->plugin->getPluginName() !== $plugin_name ) {
111 return $notices;
112 }
113
114 if ( ! $this->is_migration_error() || ! current_user_can( 'update_plugins' ) ) {
115 return $notices;
116 }
117
118 if ( $this->plugin->isNetworkActive() ) {
119 $migration_error_text = get_site_option( $this->plugin->getOptionName( 'plugin_migration_error' ), '' );
120 } else {
121 $migration_error_text = get_option( $this->plugin->getOptionName( 'plugin_migration_error' ), '' );
122 }
123
124 $fix_migration_error_url = esc_url( add_query_arg( 'wbcr_factory_fix_migration_error', 1 ) );
125
126 $notice_text = $migration_error_text;
127 $notice_text .= "<br><br><a href='{$fix_migration_error_url}' class='button button-default'>" . __( 'I fixed, confirm migration', 'robin-image-optimizer' ) . '</a>';
128
129 $notices[] = [
130 'id' => 'migration_debug_bar',
131 'type' => 'error',
132 'dismissible' => false,
133 'dismiss_expires' => 0,
134 'text' => '<p><b>' . $this->plugin->getPluginTitle() . ' ' . __( 'migration error', 'robin-image-optimizer' ) . '</b><br>' . $notice_text . '</p>',
135 ];
136
137 return $notices;
138 }
139
140 /**
141 * Debug panel, display some information from the database. Also allows
142 * perform manual migrations to test new migrations.
143 *
144 * @param array $notices
145 * @param string $plugin_name
146 *
147 * @return array
148 */
149 public function debug_bar_notice( $notices, $plugin_name ) {
150
151 if ( $this->plugin->getPluginName() !== $plugin_name ) {
152 return $notices;
153 }
154 if ( ! $this->is_debug() || ! current_user_can( 'update_plugins' ) ) {
155 return $notices;
156 }
157
158 $migrate_url = esc_url( add_query_arg( 'wbcr_factory_test_migration', 1 ) );
159
160 $notice_text = __( 'Plugin activated:', 'robin-image-optimizer' ) . ' ' . date( 'Y-m-d H:i:s', $this->get_plugin_activated_time() ) . '<br>';
161
162 $notice_text .= __( 'Old plugin version (debug):', 'robin-image-optimizer' ) . ' ' . $this->get_old_plugin_version() . '<br>';
163 $notice_text .= __( 'Current plugin version:', 'robin-image-optimizer' ) . ' ' . $this->get_current_plugin_version() . '<br>';
164 $notice_text .= __( 'Need migration:', 'robin-image-optimizer' ) . ' ' . ( $this->need_migration() ? 'true' : 'false' ) . '<br><br>';
165 $notice_text .= "<a href='{$migrate_url}' class='button button-default'>" . __( 'Migrate now', 'robin-image-optimizer' ) . '</a><br>';
166
167 $notices[] = [
168 'id' => 'migration_debug_bar',
169 'type' => 'warning',
170 'dismissible' => false,
171 'dismiss_expires' => 0,
172 'text' => '<p><b style="color:red;">' . $this->plugin->getPluginTitle() . ' ' . __( 'migrations DEBUG bar', 'robin-image-optimizer' ) . '</b><br>' . $notice_text . '</p>',
173 ];
174
175 return $notices;
176 }
177
178 /**
179 * Runs when plugin is activated. Checks if you need to migrate
180 * and if necessary it does it. Also adds a option when the plugin
181 * was activated for the first time.
182 */
183 public function activation_hook() {
184 /*
185 if ( $this->need_migration() && ! $this->is_debug() ) {
186 $this->make_migration();
187 }*/
188
189 // just time to know when the plugin was activated the first time
190 $activated = $this->get_plugin_activated_time();
191
192 if ( ! $activated ) {
193 if ( $this->plugin->isNetworkActive() ) {
194 update_site_option( $this->plugin->getOptionName( 'plugin_activated' ), time() );
195 update_site_option( $this->plugin->getOptionName( 'plugin_version' ), $this->get_current_plugin_version() );
196 } else {
197 update_option( $this->plugin->getOptionName( 'plugin_activated' ), time() );
198 update_option( $this->plugin->getOptionName( 'plugin_version' ), $this->get_current_plugin_version() );
199 }
200 }
201 }
202
203 /**
204 * Checks if debug mode of migrations from version x.x.x to x.x.y is enabled.
205 *
206 * @return bool
207 */
208 protected function is_debug() {
209 return defined( 'FACTORY_MIGRATIONS_DEBUG' ) && FACTORY_MIGRATIONS_DEBUG;
210 }
211
212 /**
213 * Gets previous version of plugin that plugin had before updating to the new version.
214 *
215 * @return string|null
216 */
217 protected function get_old_plugin_version() {
218
219 if ( $this->is_debug() && defined( 'FACTORY_MIGRATIONS_FORCE_OLD_VERSION' ) ) {
220 return FACTORY_MIGRATIONS_FORCE_OLD_VERSION;
221 }
222
223 if ( $this->plugin->isNetworkActive() ) {
224 $plugin_version = get_site_option( $this->plugin->getOptionName( 'plugin_version' ), null );
225 } else {
226 $plugin_version = get_option( $this->plugin->getOptionName( 'plugin_version' ), null );
227 }
228
229 if ( ! empty( $plugin_version ) ) {
230 return $plugin_version;
231 }
232
233 // TODO: Remove after few releases
234 // This block for compatibility code with old version of framework < 4.1.1
235 // -------------------------------------------
236 if ( $this->plugin->isNetworkActive() ) {
237 $plugin_versions = get_site_option( 'factory_plugin_versions', [] );
238 } else {
239 $plugin_versions = get_option( 'factory_plugin_versions', [] );
240 }
241
242 $plugin_version = isset( $plugin_versions[ $this->plugin->getPluginName() ] ) ? $plugin_versions[ $this->plugin->getPluginName() ] : null;
243
244 if ( ! empty( $plugin_version ) ) {
245 $plugin_version = str_replace( [ 'free-', 'premium-', 'offline-' ], '', $plugin_version );
246 }
247
248 // -------------------------------------------
249
250 return $plugin_version;
251 }
252
253 /**
254 * Gets the current version of plugin.
255 *
256 * @return string
257 */
258 protected function get_current_plugin_version() {
259 return $this->plugin->getPluginVersion();
260 }
261
262 /**
263 * Do I need migration for plugin? If previous migration was with a error, then
264 * method will always return false to prevent looping.
265 *
266 * @return mixed
267 */
268 protected function need_migration() {
269 if ( $this->is_migration_error() ) {
270 return false;
271 }
272
273 return version_compare( $this->get_old_plugin_version(), $this->get_current_plugin_version(), '<' );
274 }
275
276 /**
277 * Are there errors from previous migrations?
278 *
279 * @return bool
280 */
281 protected function is_migration_error() {
282 if ( $this->plugin->isNetworkActive() ) {
283 $error = get_site_option( $this->plugin->getOptionName( 'plugin_migration_error' ), false );
284 } else {
285 $error = get_option( $this->plugin->getOptionName( 'plugin_migration_error' ), false );
286 }
287
288 return $error !== false;
289 }
290
291 /**
292 * Remove an option in database, thereby fix errors of the previous migration.
293 */
294 protected function fix_migration_error() {
295 if ( $this->plugin->isNetworkActive() ) {
296 delete_site_option( $this->plugin->getOptionName( 'plugin_migration_error' ) );
297
298 return;
299 }
300
301 delete_option( $this->plugin->getOptionName( 'plugin_migration_error' ) );
302 }
303
304 /**
305 * Migrates the plugin from version x.x.x to x.x.y. Automatically searches for files
306 * migrations to the plugin's root directory and executes them. Default files
307 * migrations are stored in wp-content/plugins/plugin-name/migrations and have names
308 * 0x0x0x.php, which corresponds to the version x.x.x. Method executes those migration files
309 * versions of which are between the previous version of plugin and current one.
310 */
311 protected function make_migration() {
312
313 if ( ! current_user_can( 'update_plugins' ) ) {
314 return;
315 }
316
317 $old_plugin_version = $this->get_old_plugin_version();
318 $new_plugin_version = $this->get_current_plugin_version();
319
320 if ( empty( $old_plugin_version ) ) {
321 $this->update_plugin_version_in_db();
322 }
323
324 // converts versions like 0.0.0 to 000000
325 $old_number = $this->get_version_number( $old_plugin_version );
326 $new_number = $this->get_version_number( $new_plugin_version );
327
328 try {
329
330 $update_files = $this->plugin->get_paths()->migrations;
331 $files = $this->find_files( $update_files );
332
333 if ( empty( $files ) ) {
334 $this->update_plugin_version_in_db();
335
336 return;
337 }
338
339 // finds updates that has intermediate version
340 foreach ( (array) $files as $item ) {
341 if ( ! preg_match( '/^\d+$/', $item['name'] ) ) {
342 continue;
343 }
344
345 $item_number = intval( $item['name'] );
346
347 if ( $item_number > $old_number && $item_number <= $new_number ) {
348 $classes = $this->get_classes( $item['path'] );
349
350 if ( count( $classes ) == 0 ) {
351 continue;
352 }
353
354 foreach ( $classes as $path => $class_data ) {
355 include_once $path;
356 $update_class = $class_data['name'];
357
358 $update = new $update_class( $this->plugin );
359 $update->install();
360 }
361 }
362 }
363
364 $this->update_plugin_version_in_db();
365 } catch ( Exception $e ) {
366 if ( $this->plugin->isNetworkActive() ) {
367 update_site_option( $this->plugin->getOptionName( 'plugin_migration_error' ), $e->getMessage() );
368
369 return;
370 }
371 update_option( $this->plugin->getOptionName( 'plugin_migration_error' ), $e->getMessage() );
372 }
373 }
374
375 /**
376 * Updates version of plugin in database. So that we can track which
377 * previous version of plugin was at the user, before he updated
378 * plugin.
379 */
380 protected function update_plugin_version_in_db() {
381
382 // TODO: Delete after few releases
383 // This block for compatibility code with the old version of framework.
384 // Cleans up old data, after the transition to new version of framework.
385 // -------------------------------------------
386 if ( $this->plugin->isNetworkActive() ) {
387 $plugin_versions = get_site_option( 'factory_plugin_versions', [] );
388 } else {
389 $plugin_versions = get_option( 'factory_plugin_versions', [] );
390 }
391
392 if ( isset( $plugin_versions[ $this->plugin->getPluginName() ] ) ) {
393 unset( $plugin_versions[ $this->plugin->getPluginName() ] );
394 }
395
396 if ( $this->plugin->isNetworkActive() ) {
397 if ( empty( $plugin_versions ) ) {
398 delete_site_option( 'factory_plugin_versions' );
399 }
400 update_site_option( 'factory_plugin_versions', $plugin_versions );
401 update_site_option( $this->plugin->getOptionName( 'plugin_version' ), $this->get_current_plugin_version() );
402
403 return;
404 }
405
406 if ( empty( $plugin_versions ) ) {
407 delete_option( 'factory_plugin_versions' );
408 }
409
410 update_option( 'factory_plugin_versions', $plugin_versions );
411 update_option( $this->plugin->getOptionName( 'plugin_version' ), $this->get_current_plugin_version() );
412 }
413
414 /**
415 * Converts string representation of the version to the numeric.
416 *
417 * @since 1.0.0
418 *
419 * @param string $version A string version to convert.
420 *
421 * @return integer
422 */
423 protected function get_version_number( $version ) {
424 preg_match( '/(\d+)\.(\d+)\.(\d+)/', $version, $matches );
425 if ( count( $matches ) == 0 ) {
426 return false;
427 }
428
429 $number = '';
430 $number .= ( strlen( $matches[1] ) == 1 ) ? '0' . $matches[1] : $matches[1];
431 $number .= ( strlen( $matches[2] ) == 1 ) ? '0' . $matches[2] : $matches[2];
432 $number .= ( strlen( $matches[3] ) == 1 ) ? '0' . $matches[3] : $matches[3];
433
434 return intval( $number );
435 }
436
437 /**
438 * Returns a list of files at a given path.
439 *
440 * @param string $path path for search
441 */
442 private function find_files( $path ) {
443 return $this->find_file_or_folders( $path, true );
444 }
445
446 /**
447 * Returns a list of folders at a given path.
448 *
449 * @param string $path path for search
450 */
451 /*
452 private function find_folders( $path ) {
453 return $this->find_file_or_folders( $path, false );
454 }*/
455
456 /**
457 * Returns a list of files or folders at a given path.
458 *
459 * @param string $path path for search
460 * @param bool $files files or folders?
461 */
462 private function find_file_or_folders( $path, $areFiles = true ) {
463 if ( ! is_dir( $path ) ) {
464 return [];
465 }
466
467 $entries = scandir( $path );
468 if ( empty( $entries ) ) {
469 return [];
470 }
471
472 $files = [];
473 foreach ( $entries as $entryName ) {
474 if ( $entryName == '.' || $entryName == '..' ) {
475 continue;
476 }
477
478 $filename = $path . '/' . $entryName;
479 if ( ( $areFiles && is_file( $filename ) ) || ( ! $areFiles && is_dir( $filename ) ) ) {
480 $files[] = [
481 'path' => str_replace( '\\', '/', $filename ),
482 'name' => $areFiles ? str_replace( '.php', '', $entryName ) : $entryName,
483 ];
484 }
485 }
486
487 return $files;
488 }
489
490 /**
491 * Gets php classes defined in a specified file.
492 *
493 * @param string $path
494 *
495 * @throws Exception
496 */
497 private function get_classes( $path ) {
498
499 $phpCode = file_get_contents( $path );
500
501 $classes = [];
502
503 if ( ! function_exists( 'token_get_all' ) ) {
504 throw new Exception(
505 // translators: %s is the function name.
506 sprintf( __( 'There is no PHP Tokenizer extension installed on your server, you cannot use the %s function.', 'robin-image-optimizer' ), 'token_get_all' )
507 );
508 }
509
510 $tokens = token_get_all( $phpCode );
511
512 $count = count( $tokens );
513 for ( $i = 2; $i < $count; $i++ ) {
514 if ( is_array( $tokens ) && $tokens[ $i - 2 ][0] == T_CLASS && $tokens[ $i - 1 ][0] == T_WHITESPACE && $tokens[ $i ][0] == T_STRING ) {
515
516 $extends = null;
517 if ( $tokens[ $i + 2 ][0] == T_EXTENDS && $tokens[ $i + 4 ][0] == T_STRING ) {
518 $extends = $tokens[ $i + 4 ][1];
519 }
520
521 $class_name = $tokens[ $i ][1];
522 $classes[ $path ] = [
523 'name' => $class_name,
524 'extends' => $extends,
525 ];
526 }
527 }
528
529 /**
530 * result example:
531 *
532 * $classes['/plugin/items/filename.php'] = array(
533 * 'name' => 'PluginNameItem',
534 * 'extendes' => 'PluginNameItemBase'
535 * )
536 */
537
538 return $classes;
539 }
540 }
541