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