PluginProbe
PlugVersions – Easily roll back to previous versions of your plugins. / 0.4.0
PlugVersions – Easily roll back to previous versions of your plugins. v0.4.0
0.3.1 0.4.0 0.3.0 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
plugversions / admin / pr-admin.php

pr-admin.php in PlugVersions – Easily roll back to previous versions of your plugins. 0.4.0, at admin/pr-admin.php

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