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(
'
',
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'
);
?>
- Plugins get a "Prepare Update" row action right on the Plugins page.', 'downup' ), array( 'strong' => array() ) ); ?>
- Themes get the same link right here instead, since WordPress\'s Themes screen doesn\'t support custom row actions.', 'downup' ), array( 'strong' => array() ) ); ?>
- Enabled by default for everything — uncheck an item below to turn it off.', 'downup' ), array( 'strong' => array() ) ); ?>