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