self::PAGE_SLUG ), admin_url( 'tools.php' ) ); } /** * Enabled by default for every plugin. A plugin only loses the action * link once an admin explicitly unchecks it on the settings screen. */ public static function is_enabled( $plugin_basename ) { $disabled = get_option( self::OPTION_DISABLED_PLUGINS, array() ); return ! in_array( $plugin_basename, (array) $disabled, true ); } /** * Same opt-out model as is_enabled(), for themes. */ public static function is_theme_enabled( $stylesheet ) { $disabled = get_option( self::OPTION_DISABLED_THEMES, array() ); return ! in_array( $stylesheet, (array) $disabled, true ); } public static function add_plugin_row_action( $actions, $plugin_file ) { if ( FUFD_PLUGIN_BASENAME === $plugin_file || ! current_user_can( FUFD_CAPABILITY ) ) { return $actions; } if ( ! self::is_enabled( $plugin_file ) ) { return $actions; } $url = wp_nonce_url( add_query_arg( array( 'fufd_action' => 'prepare_downgrade', 'fufd_plugin' => $plugin_file, ), admin_url( 'plugins.php' ) ), 'fufd_prepare_downgrade_' . $plugin_file ); $confirm_message = __( 'Fake-downgrade this plugin\'s version so an update shows as available?', 'downup' ); $actions['fufd-prepare-update'] = '' . esc_html__( 'Prepare Update', 'downup' ) . ''; return $actions; } /** * Builds the "Prepare Update" link markup for one theme. * * Unlike plugins, WordPress's Themes screen has no equivalent of the * plugin_action_links filter — its grid/overlay is rendered from * Backbone templates that only ever reference a few hardcoded action * keys (customize/activate/delete/autoupdate), so a custom action can't * be injected there. This link lives on our own settings page instead; * the resulting "update available" notice still shows natively on * Appearance > Themes once a theme has been prepared. */ private static function theme_prepare_link( $stylesheet ) { $url = wp_nonce_url( add_query_arg( array( 'fufd_action' => 'prepare_theme_downgrade', 'fufd_theme' => $stylesheet, ), admin_url( 'themes.php' ) ), 'fufd_prepare_theme_downgrade_' . $stylesheet ); $confirm_message = __( 'Fake-downgrade this theme\'s version so an update shows as available?', 'downup' ); return '' . esc_html__( 'Prepare Update', 'downup' ) . ''; } /** * Read-only: displays a message based on a query var our own code sets * after an already-nonce-verified action completed and redirected here. * No state changes, so there's nothing for a nonce to protect. */ public static function render_notices() { if ( empty( $_GET['fufd_notice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- see docblock above. return; } $notice = sanitize_key( wp_unslash( $_GET['fufd_notice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- see docblock above. $messages = array( 'settings_saved' => array( 'success', __( 'Settings saved.', 'downup' ) ), 'prepared' => array( 'success', __( 'Version header updated. Now click "Update Now" to finish.', 'downup' ) ), 'error' => array( 'error', isset( $_GET['fufd_message'] ) ? sanitize_text_field( wp_unslash( $_GET['fufd_message'] ) ) : __( 'Something went wrong.', 'downup' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- see docblock above. ); if ( ! isset( $messages[ $notice ] ) ) { return; } list( $type, $text ) = $messages[ $notice ]; printf( '

%2$s

', esc_attr( $type ), esc_html( $text ) ); } public static function handle_save_settings() { if ( ! current_user_can( FUFD_CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'downup' ) ); } check_admin_referer( 'fufd_save_settings' ); $all_plugins = array_keys( self::get_installed_plugins() ); $checked_plugins = isset( $_POST['fufd_enabled_plugins'] ) && is_array( $_POST['fufd_enabled_plugins'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['fufd_enabled_plugins'] ) ) : array(); update_option( self::OPTION_DISABLED_PLUGINS, array_values( array_diff( $all_plugins, $checked_plugins ) ) ); if ( current_user_can( FUFD_THEME_CAPABILITY ) ) { $all_themes = array_keys( self::get_installed_themes() ); $checked_themes = isset( $_POST['fufd_enabled_themes'] ) && is_array( $_POST['fufd_enabled_themes'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['fufd_enabled_themes'] ) ) : array(); update_option( self::OPTION_DISABLED_THEMES, array_values( array_diff( $all_themes, $checked_themes ) ) ); } self::redirect_with_notice( self::settings_page_url(), 'settings_saved' ); } /** * Dispatcher only: reads the query var purely to decide which handler to * call. Each handler verifies its own nonce via check_admin_referer() * before taking any action, so no state change happens on this read. */ public static function maybe_handle_prepare() { if ( empty( $_GET['fufd_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- see docblock above. return; } if ( 'prepare_downgrade' === $_GET['fufd_action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- see docblock above. self::handle_prepare_plugin(); } elseif ( 'prepare_theme_downgrade' === $_GET['fufd_action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- see docblock above. self::handle_prepare_theme(); } } private static function handle_prepare_plugin() { if ( empty( $_GET['fufd_plugin'] ) ) { return; } $plugin = sanitize_text_field( wp_unslash( $_GET['fufd_plugin'] ) ); if ( ! current_user_can( FUFD_CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'downup' ) ); } check_admin_referer( 'fufd_prepare_downgrade_' . $plugin ); if ( ! self::is_enabled( $plugin ) ) { self::redirect_with_notice( admin_url( 'plugins.php' ), 'error', __( 'This plugin is disabled in DownUp settings.', 'downup' ) ); } $result = FUFD_Version_Manager::prepare_fake_downgrade( $plugin ); if ( is_wp_error( $result ) ) { self::redirect_with_notice( admin_url( 'plugins.php' ), 'error', $result->get_error_message() ); } self::redirect_with_notice( admin_url( 'plugins.php' ), 'prepared' ); } private static function handle_prepare_theme() { if ( empty( $_GET['fufd_theme'] ) ) { return; } $theme = sanitize_text_field( wp_unslash( $_GET['fufd_theme'] ) ); if ( ! current_user_can( FUFD_THEME_CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'downup' ) ); } check_admin_referer( 'fufd_prepare_theme_downgrade_' . $theme ); if ( ! self::is_theme_enabled( $theme ) ) { self::redirect_with_notice( admin_url( 'themes.php' ), 'error', __( 'This theme is disabled in DownUp settings.', 'downup' ) ); } $result = FUFD_Theme_Version_Manager::prepare_fake_downgrade( $theme ); if ( is_wp_error( $result ) ) { self::redirect_with_notice( admin_url( 'themes.php' ), 'error', $result->get_error_message() ); } self::redirect_with_notice( admin_url( 'themes.php' ), 'prepared' ); } private static function redirect_with_notice( $base_url, $notice, $message = '' ) { $args = array( 'fufd_notice' => $notice ); if ( $message ) { $args['fufd_message'] = rawurlencode( $message ); } wp_safe_redirect( add_query_arg( $args, $base_url ) ); exit; } private static function get_installed_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return get_plugins(); } private static function get_installed_themes() { return wp_get_themes(); } public static function render_page() { if ( ! current_user_can( FUFD_CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'downup' ) ); } $plugins = self::get_installed_plugins(); $can_themes = current_user_can( FUFD_THEME_CAPABILITY ); $themes = $can_themes ? self::get_installed_themes() : array(); ?>

, 2: example fake version in */ esc_html__( 'Marks a plugin\'s or theme\'s version as a pre-release of itself (%1$s becomes %2$s), so WordPress offers its normal "update available" notice. It never downloads or installs anything itself — you still finish with a normal "Update Now" click.', 'downup' ), '1.4.2', '1.4.2-dev' ); ?>

$data ) { if ( FUFD_PLUGIN_BASENAME === $file ) { continue; } if ( ! self::is_enabled( $file ) ) { $all_plugins_enabled = false; break; } } ?> $data ) : ?>
/>
/>

$theme ) { if ( ! self::is_theme_enabled( $stylesheet ) ) { $all_themes_enabled = false; break; } } ?> $theme ) : ?>
/>
/> get( 'Name' ) ); ?> get( 'Version' ) ); ?>