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
← All changes | admin/pr-admin.php +343 -115 0.0.6.RC-1 → 0.4.0 View file →
@@ -6,8 +6,9 @@
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.
10 11 require_once PLUGIN_REVISIONS_PLUGIN_DIR . '/admin/pr-ajax-admin.php';
11 12 }
12 13
13 14 register_activation_hook( __FILE__, function() {
@@ -20,101 +21,8 @@
20 21 update_site_option( 'plugin_revisions',array( 'time' => time() ) );
21 22 }
22 23 } );
23 24
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 - $zip_dirname = 'pr-'.$key.'-'.sanitize_option( 'upload_path',$version ).'-ver-'.dirname( $plugin );
49 - $destination = str_replace( dirname( $plugin ), $zip_dirname, sanitize_option( 'upload_path',plugin_dir_path( $path )) );
50 - if( !is_dir( $destination ) ){
51 - global $wp_filesystem;
52 - if( empty( $wp_filesystem ) ){
53 - require_once ( ABSPATH .'/wp-admin/includes/file.php' );
54 - WP_Filesystem();
55 - }
56 - $wp_filesystem->mkdir( $destination );
57 - }
58 - if( !empty( $wp_filesystem ) && $wp_filesystem->is_dir( $destination ) ){
59 - $result = copy_dir( plugin_dir_path( $path ),$destination );
60 - $zip_dirname .= '.zip';
61 - if( eos_pv_create_zip_pclzip( $destination, $zip_dirname ) ) {
62 - // If zipped archive created delete the unzipped directory.
63 - $wp_filesystem->delete( $destination, true );
64 - }
65 - eos_plugin_revisions_remove_versions( apply_filters( 'max_plugin_revisions',4 ),$plugin_name );
66 - }
67 - }
68 - }
69 - }
70 - }
71 - }
72 - return $options;
73 -},10,4 );
74 -
75 -add_filter( 'all_plugins',function( $plugins ){
76 - /**
77 - * Remove plugin revisions from plugins table in the page wp-admin/plugins.php
78 - * Add link to restore previous unzipped versions for backward compatibility
79 - *
80 - * @since 0.0.1
81 - */
82 - $key = eos_plugin_revision_key();
83 - if( $key ){
84 - foreach( $plugins as $plugin => $arr ){
85 - if( false !== strpos( $plugin,'pr-'.$key.'-' ) ){
86 - unset( $plugins[$plugin] );
87 - $parent_plugin = str_replace( 'pr-'.$key.'-','',$plugin );
88 - $parent_pluginArr = explode( '-ver-',$parent_plugin );
89 - $parent_plugin = isset( $parent_pluginArr[1] ) ? $parent_pluginArr[1] : $parent_plugin;
90 - add_filter( 'plugin_action_links_'.$parent_plugin,function( $actions, $plugin_file, $plugin_data, $context ){
91 - if( !current_user_can( 'activate_plugin' ) ) return $actions;
92 - $key = eos_plugin_revision_key();
93 - $all_dirs = scandir( dirname( PLUGIN_REVISIONS_PLUGIN_DIR ) );
94 - if( $all_dirs && !empty( $all_dirs ) ){
95 - $new_action_links = '';
96 - $plugin = dirname( $plugin_file );
97 - foreach( $all_dirs as $dir ){
98 - if( false !== strpos( $dir,'pr-'.$key.'-' ) && substr( $dir,-strlen( $plugin ),strlen( $plugin ) ) === $plugin ){
99 - $ver = str_replace( 'pr-'.$key.'-','',$dir );
100 - $verArr = explode( '-ver-',$ver );
101 - $ver = $verArr[0];
102 - $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> ';
103 - }
104 - }
105 - if( '' !== $new_action_links ){
106 - $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>';
107 - }
108 - }
109 - return $actions;
110 - },10,4 );
111 - }
112 - }
113 - }
114 - return $plugins;
115 -} );
116 -
117 25 add_action( 'admin_head',function(){
118 26 /**
119 27 * Add style to properly show the revisions on the page of plugins
120 28 *
@@ -138,9 +46,10 @@
138 46 min-width:200px;
139 47 min-width:max-content;
140 48 background:#fff;
141 49 margin-top:15px;
142 - padding:10px 10px
50 + padding:10px 10px;
51 + z-index:9
143 52 }
144 53 .plugin-revision-wrp:hover .plugin-revisions-vers a{
145 54 display:block;
146 55 border-bottom:1px dashed;
@@ -157,8 +66,9 @@
157 66
158 67 add_action( 'admin_footer',function(){
159 68 /**
160 69 * Add JS to restore a revision via Ajax
70 + * This script is added to the plugins.php and plugin-editor.php pages.
161 71 *
162 72 * @since 0.0.1
163 73 */
164 74 global $pagenow;
@@ -164,8 +74,30 @@
164 74 global $pagenow;
165 75 if( $pagenow && 'plugins.php' === sanitize_text_field( $pagenow ) ){
166 76 wp_nonce_field( 'plugin_reviews_restore_version','plugin_reviews_restore_version' );
167 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>
168 100 <script id="plugin-revisions-js">
169 101 function eos_plugin_revisions(){
170 102 var as = document.getElementsByClassName('plugin-revision-action'),n=0,req = new XMLHttpRequest(),fd=new FormData(),nonce=document.getElementById('plugin_reviews_restore_version').value;
171 103 for(n;n<as.length;++n){
@@ -170,8 +102,12 @@
170 102 var as = document.getElementsByClassName('plugin-revision-action'),n=0,req = new XMLHttpRequest(),fd=new FormData(),nonce=document.getElementById('plugin_reviews_restore_version').value;
171 103 for(n;n<as.length;++n){
172 104 as[n].addEventListener('click',function(e){
173 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 + }
174 110 this.style.backgroundPosition = 'center center';
175 111 for(var k=0;k<as.length;++k){
176 112 as[k].style.pointerEvents = 'none';
177 113 }
@@ -220,8 +156,9 @@
220 156 } );
221 157
222 158 /**
223 159 * Remove all plugin revisions
160 + * This function removes all revisions of a specific plugin or all plugins if no plugin name is provided.
224 161 *
225 162 * @since 0.0.1
226 163 */
227 164 function eos_plugin_revisions_remove_versions( $N = false,$plugin_name = false ){
@@ -226,8 +163,10 @@
226 163 */
227 164 function eos_plugin_revisions_remove_versions( $N = false,$plugin_name = false ){
228 165 $key = eos_plugin_revision_key();
229 166 $all_dirs = eos_plugin_revisions_scandir( dirname( PLUGIN_REVISIONS_PLUGIN_DIR ) );
167 + $prefix = 'pr-'.$key.'-';
168 + $suffix = '-ver-'.$plugin_name.'.zip';
230 169 global $wp_filesystem;
231 170 if( empty( $wp_filesystem ) ){
232 171 require_once ABSPATH . 'wp-admin/includes/file.php';
233 172 WP_Filesystem();
@@ -233,9 +172,9 @@
233 172 WP_Filesystem();
234 173 }
235 174 if( $all_dirs && !empty( $all_dirs ) ){
236 175 foreach( $all_dirs as $all_dir ){
237 - if( substr( $all_dir,-strlen( $plugin_name ),strlen( $plugin_name ) ) !== $plugin_name ){
176 + if( 0 !== strpos( $all_dir,$prefix ) || substr( $all_dir,-strlen( $suffix ) ) !== $suffix ){
238 177 unset( $all_dirs[array_search( $all_dir,$all_dirs )] );
239 178 }
240 179 }
241 180 $n = 0;
@@ -273,8 +212,9 @@
273 212 }
274 213
275 214 /**
276 215 * Revisions scandir
216 + * This function scans the plugin revisions directory and returns an array of files sorted by modification time.
277 217 *
278 218 * @since 0.0.1
279 219 */
280 220 function eos_plugin_revisions_scandir( $dir ) {
@@ -291,32 +231,320 @@
291 231
292 232 add_action( 'admin_init', 'eos_pv_restore_revision_links' );
293 233 /**
294 234 * Add links to restore previous revisions from zipped plugins.
235 + * This function initializes the restoring link functionality for plugin revisions.
295 236 *
296 237 * @since 0.0.6
297 238 */
298 239 function eos_pv_restore_revision_links() {
299 - $key = eos_plugin_revision_key();
300 - if( $key ){
301 - $all_plugin_files = scandir( WP_PLUGIN_DIR );
302 - if( $all_plugin_files && is_array( $all_plugin_files ) && ! empty( $all_plugin_files ) ) {
303 - require_once PLUGIN_REVISIONS_PLUGIN_DIR . '/admin/classes/class-plugversions-restoring-link.php';
304 - foreach( $all_plugin_files as $plugin ) {
305 - if( 'zip' === pathinfo( $plugin, PATHINFO_EXTENSION ) ) {
306 - if( false !== strpos( $plugin, 'pr-' . $key .'-' ) && false !== strpos( $plugin, '-ver-' ) ) {
307 - $arr = explode( 'ver-', $plugin );
308 - $arr = explode( '-', $arr[0] );
309 - if( 'pr' === $arr[0] && isset( $arr[1] ) && $key === $arr[1] && isset( $arr[2] ) ) {
310 - $version = $arr[2];
311 - $plugin_name = str_replace( array( 'pr-' . $key . '-' . $version . '-ver-', '.zip' ), array( '', '' ), $plugin );
312 - if( is_dir( WP_PLUGIN_DIR . '/' . $plugin_name ) ) {
313 - $link = new PlugVersions_Restoring_Link( $key, $version, $plugin, $plugin_name );
314 - }
315 - }
316 - }
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
317 404 }
318 - }
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);
319 415 }
320 - }
416 +
417 + // Cleanup
418 + $wp_filesystem->delete($unzip_dir, true);
419 + $wp_filesystem->delete(ABSPATH . '.maintenance');
420 +
421 + return true;
321 422 }
322 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 +} );