PluginProbe
PlugVersions – Easily roll back to previous versions of your plugins. / 0.0.4
PlugVersions – Easily roll back to previous versions of your plugins. v0.0.4
0.0.6.RC-1 0.0.7 0.0.8 0.1.0 0.2.0 0.2.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6
← All changes | admin/pr-admin.php +87 -349 trunk0.0.4 View file →
@@ -6,9 +6,8 @@
6 6 */
7 7 defined( 'PLUGIN_REVISIONS_PLUGIN_DIR' ) || exit; // Exit if not accessed from Plugversions.
8 8
9 9 if( wp_doing_ajax() ){
10 - //* Include the Ajax admin file.
11 10 require_once PLUGIN_REVISIONS_PLUGIN_DIR . '/admin/pr-ajax-admin.php';
12 11 }
13 12
14 13 register_activation_hook( __FILE__, function() {
@@ -21,8 +20,94 @@
21 20 update_site_option( 'plugin_revisions',array( 'time' => time() ) );
22 21 }
23 22 } );
24 23
24 +add_filter( 'upgrader_package_options',function( $options ) {
25 + /**
26 + * Update revisions after plugin update
27 + *
28 + * @since 0.0.1
29 + */
30 + if( isset( $options['destination'] ) && isset( $options['hook_extra'] ) ){
31 + $hook_extra = $options['hook_extra'];
32 + if( isset( $hook_extra['plugin'] ) ){
33 + $plugin = $hook_extra['plugin'];
34 + $plugin_name = dirname( $plugin );
35 + $path = sanitize_option( 'upload_path',$options['destination'].'/'.$plugin );
36 + if( file_exists( $path ) ){
37 + $plugin_data = get_plugin_data( $path );
38 + $version = $plugin_data['Version'];
39 + if( !class_exists( 'WP_Upgrader' ) ){
40 + if( file_exists( ABSPATH.'wp-admin/includes/class-wp-upgrader.php' ) ){
41 + require_once ABSPATH.'/wp-admin/includes/class-wp-upgrader.php';
42 + }
43 + }
44 + if( class_exists( 'WP_Upgrader' ) ){
45 + $upgrader = new WP_Upgrader();
46 + $key = eos_plugin_revision_key();
47 + if( $key ){
48 + $destination = str_replace( dirname( $plugin ),'pr-'.$key.'-'.sanitize_option( 'upload_path',$version ).'-ver-'.dirname( $plugin ),sanitize_option( 'upload_path',plugin_dir_path( $path )) );
49 + if( !is_dir( $destination ) ){
50 + global $wp_filesystem;
51 + if( empty( $wp_filesystem ) ){
52 + require_once ( ABSPATH .'/wp-admin/includes/file.php' );
53 + WP_Filesystem();
54 + }
55 + $wp_filesystem->mkdir( $destination );
56 + }
57 + if( !empty( $wp_filesystem ) && $wp_filesystem->is_dir( $destination ) ){
58 + $result = copy_dir( plugin_dir_path( $path ),$destination );
59 + eos_plugin_revisions_remove_versions( apply_filters( 'max_plugin_revisions',4 ),$plugin_name );
60 + }
61 + }
62 + }
63 + }
64 + }
65 + }
66 + return $options;
67 +},10,4 );
68 +
69 +add_filter( 'all_plugins',function( $plugins ){
70 + /**
71 + * Remove plugin revisions from plugins table in the page wp-admin/plugins.php
72 + *
73 + * @since 0.0.1
74 + */
75 + $key = eos_plugin_revision_key();
76 + if( $key ){
77 + foreach( $plugins as $plugin => $arr ){
78 + if( false !== strpos( $plugin,'pr-'.$key.'-' ) ){
79 + unset( $plugins[$plugin] );
80 + $parent_plugin = str_replace( 'pr-'.$key.'-','',$plugin );
81 + $parent_pluginArr = explode( '-ver-',$parent_plugin );
82 + $parent_plugin = isset( $parent_pluginArr[1] ) ? $parent_pluginArr[1] : $parent_plugin;
83 + add_filter( 'plugin_action_links_'.$parent_plugin,function( $actions, $plugin_file, $plugin_data, $context ){
84 + if( !current_user_can( 'activate_plugin' ) ) return $actions;
85 + $key = eos_plugin_revision_key();
86 + $all_dirs = scandir( dirname( PLUGIN_REVISIONS_PLUGIN_DIR ) );
87 + if( $all_dirs && !empty( $all_dirs ) ){
88 + $new_action_links = '';
89 + $plugin = dirname( $plugin_file );
90 + foreach( $all_dirs as $dir ){
91 + if( false !== strpos( $dir,'pr-'.$key.'-' ) && substr( $dir,-strlen( $plugin ),strlen( $plugin ) ) === $plugin ){
92 + $ver = str_replace( 'pr-'.$key.'-','',$dir );
93 + $verArr = explode( '-ver-',$ver );
94 + $ver = $verArr[0];
95 + $new_action_links .= '<a class="plugin-revision-action" href="#" data-parent_plugin="'.esc_attr( $plugin_file ).'" data-dir="'.esc_attr( $dir ).'">'.sprintf( esc_html__( 'Replace with version: %s','plugversions' ),$ver ).'</a> ';
96 + }
97 + }
98 + if( '' !== $new_action_links ){
99 + $actions['versions'] = '<span class="plugin-revision-wrp"><a href="#">'.esc_html__( 'Revisions','plugin-revisioons' ).'</a><span class="plugin-revisions-vers">'.rtrim( $new_action_links,' ' ).'</span></span>';
100 + }
101 + }
102 + return $actions;
103 + },10,4 );
104 + }
105 + }
106 + }
107 + return $plugins;
108 +} );
109 +
25 110 add_action( 'admin_head',function(){
26 111 /**
27 112 * Add style to properly show the revisions on the page of plugins
28 113 *
@@ -46,10 +131,9 @@
46 131 min-width:200px;
47 132 min-width:max-content;
48 133 background:#fff;
49 134 margin-top:15px;
50 - padding:10px 10px;
51 - z-index:9
135 + padding:10px 10px
52 136 }
53 137 .plugin-revision-wrp:hover .plugin-revisions-vers a{
54 138 display:block;
55 139 border-bottom:1px dashed;
@@ -66,9 +150,8 @@
66 150
67 151 add_action( 'admin_footer',function(){
68 152 /**
69 153 * Add JS to restore a revision via Ajax
70 - * This script is added to the plugins.php and plugin-editor.php pages.
71 154 *
72 155 * @since 0.0.1
73 156 */
74 157 global $pagenow;
@@ -74,30 +157,8 @@
74 157 global $pagenow;
75 158 if( $pagenow && 'plugins.php' === sanitize_text_field( $pagenow ) ){
76 159 wp_nonce_field( 'plugin_reviews_restore_version','plugin_reviews_restore_version' );
77 160 ?>
78 - <style id="plugin-revisions-js-css" type="text/css">
79 - .plugin-revision-processing{position:relative;cursor:wait;}
80 - .plugin-revision-processing:after{
81 - content:' ';
82 - position:absolute;
83 - top:50%;
84 - left:50%;
85 - transform:translate(-50%,-50%);
86 - background-color:rgba(255,255,255,0.8);
87 - border-radius:50%;
88 - box-shadow:0 0 5px rgba(0,0,0,0.2);
89 - z-index:9999;
90 - background-size:16px 16px;
91 - background-image:url(<?php echo esc_url( PLUGIN_REVISIONS_PLUGIN_URL ); ?>/admin/assets/images/ajax-loader.gif );
92 - background-position:center center;
93 - background-repeat:no-repeat;
94 - width:16px;
95 - height:16px;
96 - display:block;
97 - opacity:1
98 - }
99 - </style>
100 161 <script id="plugin-revisions-js">
101 162 function eos_plugin_revisions(){
102 163 var as = document.getElementsByClassName('plugin-revision-action'),n=0,req = new XMLHttpRequest(),fd=new FormData(),nonce=document.getElementById('plugin_reviews_restore_version').value;
103 164 for(n;n<as.length;++n){
@@ -102,9 +163,8 @@
102 163 var as = document.getElementsByClassName('plugin-revision-action'),n=0,req = new XMLHttpRequest(),fd=new FormData(),nonce=document.getElementById('plugin_reviews_restore_version').value;
103 164 for(n;n<as.length;++n){
104 165 as[n].addEventListener('click',function(e){
105 166 e.preventDefault();
106 - this.closest('td').classList.add('plugin-revision-processing');
107 167 this.style.backgroundPosition = 'center center';
108 168 for(var k=0;k<as.length;++k){
109 169 as[k].style.pointerEvents = 'none';
110 170 }
@@ -153,9 +213,8 @@
153 213 } );
154 214
155 215 /**
156 216 * Remove all plugin revisions
157 - * This function removes all revisions of a specific plugin or all plugins if no plugin name is provided.
158 217 *
159 218 * @since 0.0.1
160 219 */
161 220 function eos_plugin_revisions_remove_versions( $N = false,$plugin_name = false ){
@@ -207,9 +266,8 @@
207 266 }
208 267
209 268 /**
210 269 * Revisions scandir
211 - * This function scans the plugin revisions directory and returns an array of files sorted by modification time.
212 270 *
213 271 * @since 0.0.1
214 272 */
215 273 function eos_plugin_revisions_scandir( $dir ) {
@@ -222,324 +280,4 @@
222 280 asort( $files );
223 281 $files = array_keys( $files );
224 282 return ( $files ) ? $files : false;
225 283 }
226 -
227 -add_action( 'admin_init', 'eos_pv_restore_revision_links' );
228 -/**
229 - * Add links to restore previous revisions from zipped plugins.
230 - * This function initializes the restoring link functionality for plugin revisions.
231 - *
232 - * @since 0.0.6
233 - */
234 -function eos_pv_restore_revision_links() {
235 - require_once PLUGIN_REVISIONS_PLUGIN_DIR . '/admin/classes/class-plugversions-restoring-link.php';
236 - $link = new PlugVersions_Restoring_Link();
237 -}
238 -
239 -
240 -/**
241 - * Backup the plugin before update.
242 - * This filter allows you to catch all possible update scenarios, including bulk and manual replacements, as well as Ajax updates.
243 - *
244 - * @param string $source Plugin source path.
245 - * @param string $remote_source Plugin remote source path.
246 - * @param object $upgrader Upgrader object.
247 - * @param array $hook_extra Hook extra data.
248 - * @return string
249 - *
250 - * @author Vincenzo Casu.
251 - */
252 -function eos_plugin_unversal_backup( $source, $remote_source, $upgrader, $hook_extra ) {
253 - // Load get_plugin_data() if not exists.
254 - if ( ! function_exists( 'get_plugin_data' ) ) {
255 - require_once ABSPATH . 'wp-admin/includes/plugin.php';
256 - }
257 -
258 - // Get plugins to backup.
259 - $plugins_to_backup = array();
260 -
261 - if ( ! empty( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) {
262 - // Bulk update (hook_extra['plugins']).
263 - $plugins_to_backup = $hook_extra['plugins'];
264 - } elseif ( ! empty( $hook_extra['plugin'] ) ) {
265 - // Single AJAX update (hook_extra['plugin']).
266 - $plugins_to_backup = array( $hook_extra['plugin'] );
267 - } elseif ( ! empty( $hook_extra['type'] ) && $hook_extra['type'] === 'plugin'
268 - && ! empty( $hook_extra['action'] )
269 - && in_array( $hook_extra['action'], array( 'update', 'install' ), true ) ) {
270 - // Fallback “replace from ZIP” or other plugin-based cases.
271 -
272 - // Get slug from source.
273 - $slug = basename( $source );
274 - $all = get_plugins( '/' . $slug );
275 -
276 - if ( ! empty( $all ) ) {
277 - $main_file = key( $all );
278 - $plugins_to_backup[] = $slug . '/' . $main_file;
279 - } else {
280 - // If no plugins found, return the source.
281 - return $source;
282 - }
283 - } else {
284 - // No plugins to backup.
285 - return $source;
286 - }
287 -
288 - // Preparing Filesystem & Upgrader.
289 - if ( ! class_exists( 'WP_Upgrader' ) ) {
290 - require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
291 - }
292 -
293 - global $wp_filesystem;
294 - if ( empty( $wp_filesystem ) ) {
295 - require_once ABSPATH . 'wp-admin/includes/file.php';
296 - WP_Filesystem();
297 - }
298 -
299 - $key = eos_plugin_revision_key();
300 - if ( ! $key ) {
301 - return $source;
302 - }
303 - foreach ( $plugins_to_backup as $plugin_file ) {
304 - $plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );
305 - $full_path = WP_PLUGIN_DIR . '/' . $plugin_file;
306 - if ( ! file_exists( $full_path ) ) {
307 - continue;
308 - }
309 -
310 - // Metadata & version.
311 - $plugin_data = get_plugin_data( $full_path );
312 - $version = $plugin_data['Version'];
313 - $slug = dirname( $plugin_file );
314 - $zip_dirname = 'pr-' . $key . '-' . sanitize_option( 'upload_path', $version ) . '-ver-' . $slug;
315 -
316 - // Base dir & Backup destination.
317 - $base_dir = plugin_dir_path( $full_path );
318 - $backup_dir = trailingslashit( str_replace( $slug, $zip_dirname, $base_dir ) );
319 - // Make Backup dir and move plugin files into it.
320 - if ( ! $wp_filesystem->is_dir( $backup_dir ) ) {
321 - $wp_filesystem->mkdir( $backup_dir );
322 - }
323 - if ( $wp_filesystem->is_dir( $backup_dir ) ) {
324 - copy_dir( $plugin_dir, $backup_dir );
325 - $zip_file = $zip_dirname . '.zip';
326 - // Create a zip file of the plugin.
327 -
328 - if ( eos_pv_create_zip_pclzip( $backup_dir, $zip_file ) ) {
329 - $wp_filesystem->delete( untrailingslashit( $backup_dir ), true );
330 - }
331 -
332 - // Remove old versions of the plugin.
333 - eos_plugin_revisions_remove_versions(
334 - apply_filters( 'max_plugin_revisions', 4 ),
335 - $slug
336 - );
337 - }
338 - }
339 -
340 - return $source;
341 -}
342 -add_filter( 'upgrader_source_selection', 'eos_plugin_unversal_backup', 10, 4 );
343 -
344 -/**
345 - * Rollback WordPress core to a specific version.
346 - * This function allows you to rollback the WordPress core to a specified version.
347 - * It downloads the specified version, unzips it, and replaces the current core files,
348 - * while preserving the wp-content directory and wp-config.php file.
349 - *
350 - * @param string $version The version of WordPress to rollback to. Default is '6.4.3'.
351 - * @return bool|WP_Error Returns true on success or WP_Error on failure.
352 - *
353 - * @since 1.0.0
354 - */
355 -function eos_pv_rollback_wordpress_core($version) {
356 - if ( ! current_user_can('update_core') ) {
357 - wp_die( esc_html__( 'You are not allowed to update the core.', 'plugversions' ) );
358 - }
359 -
360 - require_once ABSPATH . 'wp-admin/includes/file.php';
361 - require_once ABSPATH . 'wp-admin/includes/misc.php';
362 - require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
363 - require_once ABSPATH . 'wp-admin/includes/update.php';
364 -
365 - WP_Filesystem();
366 - global $wp_filesystem;
367 -
368 - // Put site in maintenance mode
369 - $maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
370 - $wp_filesystem->put_contents(ABSPATH . '.maintenance', $maintenance_string, FS_CHMOD_FILE);
371 -
372 - // Download WordPress version zip
373 - $zip_url = "https://wordpress.org/wordpress-{$version}.zip";
374 - $tmp_file = download_url($zip_url);
375 -
376 - if ( is_wp_error($tmp_file) ) {
377 - $wp_filesystem->delete(ABSPATH . '.maintenance');
378 - return new WP_Error('download_failed', 'Failed to download WordPress version.');
379 - }
380 -
381 - // Unzip into temp folder
382 - $unzip_dir = WP_TEMP_DIR . "wordpress-rollback-{$version}";
383 - $result = unzip_file($tmp_file, $unzip_dir);
384 - unlink($tmp_file); // Remove zip file
385 -
386 - if ( is_wp_error($result) ) {
387 - $wp_filesystem->delete(ABSPATH . '.maintenance');
388 - return $result;
389 - }
390 -
391 - // Copy files from the unzipped wordpress/ directory
392 - $source = trailingslashit($unzip_dir) . 'wordpress';
393 - $dest = ABSPATH;
394 -
395 - $items = $wp_filesystem->dirlist($source);
396 - foreach ($items as $item => $details) {
397 - if ($item === 'wp-content' || $item === 'wp-config.php') {
398 - continue; // Skip user files
399 - }
400 - $source_path = trailingslashit($source) . $item;
401 - $dest_path = trailingslashit($dest) . $item;
402 -
403 - // Remove existing file or folder
404 - if ($wp_filesystem->exists($dest_path)) {
405 - $wp_filesystem->delete($dest_path, true);
406 - }
407 -
408 - // Copy new one
409 - $wp_filesystem->copy($source_path, $dest_path, true, FS_CHMOD_FILE);
410 - }
411 -
412 - // Cleanup
413 - $wp_filesystem->delete($unzip_dir, true);
414 - $wp_filesystem->delete(ABSPATH . '.maintenance');
415 -
416 - return true;
417 -}
418 -
419 -add_action('admin_init', function() {
420 - /**
421 - * Handle rollback request for WordPress core.
422 - * This function checks if the user has permission to update the core,
423 - * verifies the nonce, and processes the rollback request.
424 - *
425 - * @since 1.0.0
426 - */
427 -
428 - if (isset($_GET['rollback_wp']) && current_user_can('update_core') && check_admin_referer('eos_pv_core_rollback_nonce')) {
429 - $version = sanitize_text_field($_GET['rollback_wp']);
430 - // Validate version format
431 - // Example: 6.4.3
432 - // Ensure it matches the expected format of major.minor.patch
433 - // This regex checks for three groups of digits separated by dots.
434 - if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) {
435 - wp_die(esc_html__('Invalid version format.', 'plugversions'));
436 - }
437 -
438 - // Perform the rollback
439 - $result = eos_pv_rollback_wordpress_core( $version );
440 - if (is_wp_error($result)) {
441 - echo 'Error: ' . $result->get_error_message();
442 - } else {
443 - esc_html_e( 'Rollback completed. Please manually check your site.', 'plugversions' );
444 - }
445 - exit;
446 - }
447 -});
448 -
449 -add_action('upgrader_process_complete', function ($upgrader, $hook_extra) {
450 - /**
451 - * Save the current WordPress core version after an update.
452 - * This function saves the current WordPress version to an option after a core update.
453 - * It prevents duplicates and limits the saved versions to the last 3.
454 - *
455 - * @param WP_Upgrader $upgrader The upgrader instance.
456 - * @param array $hook_extra Extra data passed to the hook.
457 - *
458 - * @since 1.0.0
459 - */
460 -
461 - // Check if this is a core update
462 - if ( ! is_a( $upgrader, 'Core_Upgrader' ) || ! isset( $hook_extra['type'], $hook_extra['action'] ) ) {
463 - return;
464 - }
465 -
466 - // Check if the action is an update
467 - // and the type is core.
468 - // This ensures that we only save versions after a core update.
469 - if (
470 - isset($hook_extra['type'], $hook_extra['action']) &&
471 - $hook_extra['type'] === 'core' &&
472 - $hook_extra['action'] === 'update'
473 - ) {
474 - $current_version = get_bloginfo('version');
475 - $option_name = 'eos_pv_core_versions';
476 -
477 - // Get existing saved versions
478 - $saved_versions = get_option($option_name, []);
479 -
480 - // Prevent duplicates
481 - if (!in_array($current_version, $saved_versions, true)) {
482 - array_unshift($saved_versions, $current_version); // Add to beginning
483 - }
484 -
485 - // Optional: limit to last 3 versions
486 - $saved_versions = array_slice($saved_versions, 0, 3);
487 -
488 - update_option($option_name, $saved_versions);
489 - }
490 -}, 10, 2);
491 -
492 -add_action('admin_notices', function () {
493 - return; // Disable admin notices for now, as the core rollback functionality is still not ready.
494 - /**
495 - * Display rollback notice on the update-core page.
496 - * This function displays a notice with a dropdown to select a previous WordPress core version
497 - * for rollback, if available.
498 - *
499 - * @since 1.0.0
500 - */
501 -
502 - // Only show on the update-core screen
503 -
504 - if (get_current_screen()->id !== 'update-core') return;
505 -
506 - $versions = get_option('eos_pv_core_versions', []); // Get saved versions
507 - // Ensure versions are unique and sorted
508 - $versions = array_unique($versions);
509 - sort($versions);
510 -
511 - // If no versions are available, do not display the notice
512 - if (empty($versions)) return;
513 -
514 - ?>
515 - <div class="notice notice-info">
516 - <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
517 - <h3><?php esc_html_e( 'Rollback WordPress Core', 'plugversions' ); ?></h3>
518 - <p><?php esc_html_e( 'Select a previously stored version to roll back to:', 'plugversions' ); ?></p>
519 - <select name="eos_pv_version_to_rollback">
520 - <?php foreach ($versions as $version): ?>
521 - <option value="<?php echo esc_attr($version); ?>">
522 - <?php echo esc_html($version); ?>
523 - </option>
524 - <?php endforeach; ?>
525 - </select>
526 - <?php wp_nonce_field('eos_pv_rollback_wp_core', 'eos_pv_nonce'); ?>
527 - <input type="hidden" name="action" value="eos_pv_rollback_core">
528 - <button type="submit" class="button button-primary"><?php esc_html_e( 'Rollback', 'plugversions' ); ?></button>
529 - </form>
530 - </div>
531 - <?php
532 -});
533 -
534 -add_filter( 'plugin_action_links_plugversions/plugversions.php', function( $links ) {
535 - /**
536 - * Add plugin action link for Annual Protection Plan.
537 - *
538 - * @param array $links Array of plugin action links
539 - * @return array Modified array of plugin action links
540 - */
541 - $plan_link = '<a href="https://shop.josemortellaro.com/downloads/plugin-update-rescue-annual-protection-plan/" style="color: #d63638; font-weight: bold;">' . esc_html__( 'Protection Plan', 'plugversions' ) . '</a>';
542 - // Add to the beginning of the links array
543 - array_unshift( $links, $plan_link );
544 - return $links;
545 -} );