Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
cb8f57a
fix: preserve selected page status and template across maintenance mo…
Alexia-Soare Jul 16, 2026
2c25e1a
fix: never overwrite user-owned pages when applying a maintenance tem…
Alexia-Soare Jul 17, 2026
941315a
fix: harden maintenance page bootstrap and template import
Alexia-Soare Jul 17, 2026
820e2b3
fix: never resurrect or re-trash a trashed maintenance page selection
Alexia-Soare Jul 17, 2026
f79bbc5
test: cover the full issue #523 workflow through the select-page handler
Alexia-Soare Jul 17, 2026
41961be
ci: run PHPUnit on PHP 7.4, required by the latest WordPress test suite
Alexia-Soare Jul 17, 2026
383dfda
test: drop the issue number from the test name and comments
Alexia-Soare Jul 20, 2026
e8a06d6
fix: restore abandoned selections, clean up trashed pages, record ato…
Alexia-Soare Jul 21, 2026
9b6fd08
Merge remote-tracking branch 'origin/development' into fix/523-preser…
Alexia-Soare Jul 28, 2026
0aede9e
fix: satisfy PHPStan on the page-state additions
Alexia-Soare Jul 28, 2026
dab296c
fix: never override user status changes and keep the state record atomic
Alexia-Soare Jul 28, 2026
97d3836
fix: keep a private status the user chooses while maintenance is active
Alexia-Soare Jul 29, 2026
68873ca
fix: keep the page status when overwriting a generated page with a te…
Alexia-Soare Jul 29, 2026
418148e
fix: route every selection change through one take-over lifecycle
Alexia-Soare Jul 29, 2026
66f5f3d
fix: hand the selected page back when the design tab is reset
Alexia-Soare Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
# the latest WordPress test suite requires PHP 7.4+
php-version: '7.4'
extensions: simplexml, mysql
tools: phpunit:7.5.20, phpunit-polyfills
tools: phpunit-polyfills
- name: Checkout source code
uses: actions/checkout@v2
- name: Install Subversion
Expand Down
96 changes: 83 additions & 13 deletions includes/classes/wp-maintenance-mode-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@ public function save_plugin_settings() {
// Other
$_POST['options']['design']['other_custom_css'] = sanitize_textarea_field( $_POST['options']['design']['other_custom_css'] );

// Selected page: the form is a second writer besides the AJAX
// select, so route it through the same take-over lifecycle
$_POST['options']['design']['page_id'] = isset( $_POST['options']['design']['page_id'] ) ? absint( $_POST['options']['design']['page_id'] ) : 0;
$this->switch_selected_page( $_POST['options']['design']['page_id'] );

// Delete cache when is activated
if ( ! empty( $this->plugin_settings['general']['status'] ) && $this->plugin_settings['general']['status'] === 1 ) {
wpmm_delete_cache();
Expand Down Expand Up @@ -660,6 +665,11 @@ public function reset_plugin_settings() {
throw new Exception( __( 'The tab slug must exist.', 'wp-maintenance-mode' ) );
}

// resetting design drops the selection, so hand the page back first
if ( 'design' === $tab ) {
$this->switch_selected_page( 0 );
}

// update options using the default values
$this->plugin_settings[ $tab ] = $this->plugin_default_settings[ $tab ];
update_option( 'wpmm_settings', $this->plugin_settings );
Expand All @@ -686,19 +696,48 @@ public function select_page() {
die( esc_html__( 'Security check.', 'wp-maintenance-mode' ) );
}

$this->plugin_settings['design']['page_id'] = $_POST['page_id'];
wp_update_post(
array(
'ID' => $this->plugin_settings['design']['page_id'],
'page_template' => 'templates/wpmm-page-template.php',
)
);
$page_id = isset( $_POST['page_id'] ) ? absint( $_POST['page_id'] ) : 0;

$this->switch_selected_page( $page_id );

update_option( 'wpmm_settings', $this->plugin_settings );

wp_send_json_success();
}

/**
* Switch the selected maintenance page, handing the previous page back
* and taking the new one over. Every path that changes the selection
* (AJAX select, settings form) must go through here.
*
* @since 2.6.23
* @param int $page_id The newly selected page ID; 0 clears the selection.
* @return void
*/
private function switch_selected_page( $page_id ) {
$previous_page_id = isset( $this->plugin_settings['design']['page_id'] ) ? absint( $this->plugin_settings['design']['page_id'] ) : 0;

if ( $previous_page_id && $previous_page_id !== $page_id ) {
// hand the previously selected page back before abandoning it,
// otherwise nothing would ever restore it
wpmm_restore_page_state( $previous_page_id );
}

$this->plugin_settings['design']['page_id'] = $page_id;

if ( $page_id ) {
Comment thread
Soare-Robert-Daniel marked this conversation as resolved.
// remember the page's original state before taking it over as a maintenance page
wpmm_record_page_state( $page_id );

wp_update_post(
array(
'ID' => $page_id,
'page_template' => 'templates/wpmm-page-template.php',
)
);
}
}

/**
* Insert the content from the template to the Maintenance Page
* If no Maintenance Page exists, create one.
Expand All @@ -721,9 +760,19 @@ public function insert_template() {
die( esc_html__( 'Security check.', 'wp-maintenance-mode' ) );
}

$template_slug = $_POST['template_slug'];
$category = $_POST['category'];
$template = json_decode( file_get_contents( WPMM_TEMPLATES_PATH . $category . '/' . $template_slug . '/blocks-export.json' ) );
$template_slug = isset( $_POST['template_slug'] ) ? basename( sanitize_text_field( $_POST['template_slug'] ) ) : '';
$category = isset( $_POST['category'] ) ? basename( sanitize_text_field( $_POST['category'] ) ) : '';
$template_file = WPMM_TEMPLATES_PATH . $category . '/' . $template_slug . '/blocks-export.json';

if ( '' === $template_slug || '' === $category || ! is_readable( $template_file ) ) {
wp_send_json_error( array( 'error' => 'Unknown template' ) );
}

$template = json_decode( file_get_contents( $template_file ) );

if ( empty( $template->content ) ) {
wp_send_json_error( array( 'error' => 'Unknown template' ) );
}

$blocks = str_replace( '\n', '', $template->content );

Expand All @@ -734,20 +783,41 @@ public function insert_template() {
'page_template' => 'templates/wpmm-page-template.php',
);

if ( isset( $this->plugin_settings['design']['page_id'] ) && get_post_status( $this->plugin_settings['design']['page_id'] ) && get_post_status( $this->plugin_settings['design']['page_id'] ) !== 'trash' ) {
$post_arr['ID'] = $this->plugin_settings['design']['page_id'];
$selected_page_id = isset( $this->plugin_settings['design']['page_id'] ) ? absint( $this->plugin_settings['design']['page_id'] ) : 0;
$page_exists = $selected_page_id && get_post_status( $selected_page_id ) && get_post_status( $selected_page_id ) !== 'trash';

if ( $page_exists && get_post_meta( $selected_page_id, '_wpmm_generated', true ) ) {
// only pages created by the plugin may be overwritten; the private
// status is for new pages only — overwriting must not unpublish a
// page maintenance mode already made public
unset( $post_arr['post_status'] );

$post_arr['ID'] = $selected_page_id;
$page_id = wp_update_post( $post_arr );
Comment thread
Alexia-Soare marked this conversation as resolved.
} else {
if ( $selected_page_id && get_post_status( $selected_page_id ) ) {
// hand the user's page back before switching to a generated one —
// trashed pages included, or their record and template are orphaned
wpmm_restore_page_state( $selected_page_id );
}

$post_arr['post_title'] = __( 'Maintenance Page', 'wp-maintenance-mode' );
$page_id = wp_insert_post( $post_arr );

if ( $page_id ) {
update_post_meta( $page_id, '_wpmm_generated', 1 );
}
}

if ( $page_id === 0 || $page_id instanceof WP_Error ) {
wp_send_json_error( array( 'error' => 'Could not get the page' ) );
}

$this->plugin_settings['design']['page_id'] = $page_id;
CSS_Handler::generate_css_file( $page_id );

if ( class_exists( 'ThemeIsle\GutenbergBlocks\CSS\CSS_Handler' ) ) {
CSS_Handler::generate_css_file( $page_id );
}

if ( 'wizard' === $_POST['source'] ) {
$this->plugin_settings['general']['status'] = 1;
Expand Down
48 changes: 34 additions & 14 deletions includes/classes/wp-maintenance-mode.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,33 @@ private function __construct() {
add_action( 'wp_ajax_wpmm_send_contact', array( $this, 'send_contact' ) );
add_action( 'otter_form_after_submit', array( $this, 'otter_add_subscriber' ) );

if ( isset( $this->plugin_settings['design']['page_id'] ) && get_option( 'wpmm_new_look' ) && get_post_status( $this->plugin_settings['design']['page_id'] ) === 'private' ) {
add_filter( 'wpo_purge_all_cache_on_update', '__return_true' );
if ( function_exists( 'wp_functionality_constants' ) ) {
wp_functionality_constants();
$maintenance_page_status = isset( $this->plugin_settings['design']['page_id'] ) ? get_post_status( $this->plugin_settings['design']['page_id'] ) : false;

if ( $maintenance_page_status && $maintenance_page_status !== 'trash' && get_option( 'wpmm_new_look' ) ) {
// remember the page's original state so it can be restored when maintenance mode is disabled
wpmm_record_page_state( (int) $this->plugin_settings['design']['page_id'] );
Comment thread
Soare-Robert-Daniel marked this conversation as resolved.

if ( get_post_meta( $this->plugin_settings['design']['page_id'], '_wp_page_template', true ) !== 'templates/wpmm-page-template.php' ) {
update_post_meta( $this->plugin_settings['design']['page_id'], '_wp_page_template', 'templates/wpmm-page-template.php' );
}

if ( get_post_status( $this->plugin_settings['design']['page_id'] ) === 'private' ) {
$original_state = get_post_meta( $this->plugin_settings['design']['page_id'], '_wpmm_original_state', true );

// only publish the page that was private when first taken over, and
// only once: a private status the user chose afterwards is theirs to keep
if ( is_array( $original_state ) && isset( $original_state['status'] ) && $original_state['status'] === 'private' && ! get_post_meta( $this->plugin_settings['design']['page_id'], '_wpmm_applied_status', true ) ) {
add_filter( 'wpo_purge_all_cache_on_update', '__return_true' );
if ( function_exists( 'wp_functionality_constants' ) ) {
wp_functionality_constants();
}
wp_publish_post( $this->plugin_settings['design']['page_id'] );

// mark the publish as plugin-made, so restore can tell it apart
// from a status the user chose deliberately
update_post_meta( $this->plugin_settings['design']['page_id'], '_wpmm_applied_status', 'publish' );
}
}
wp_publish_post( $this->plugin_settings['design']['page_id'] );
}

update_option( 'show_on_front', 'page' );
Expand Down Expand Up @@ -141,21 +162,14 @@ function ( $value ) {
add_action( 'wpmm_before_scripts', array( $this, 'add_bot_extras' ) );
add_action( 'wpmm_footer', array( $this, 'add_js_files' ) );
} else {
// make maintenance page private when maintenance mode is disabled
// restore the maintenance page to its original state when maintenance mode is disabled
add_action(
'init',
function () {
if ( ! isset( $this->plugin_settings['design']['page_id'] ) ) {
return;
}
if ( get_post_status( $this->plugin_settings['design']['page_id'] ) === 'publish' ) {
wp_update_post(
array(
'ID' => $this->plugin_settings['design']['page_id'],
'post_status' => 'private',
)
);
}
wpmm_restore_page_state( (int) $this->plugin_settings['design']['page_id'] );
}
);
}
Expand Down Expand Up @@ -670,6 +684,12 @@ public static function single_activate( $network_wide = false ) {
* @since 2.0.0
*/
public static function single_deactivate() {
// give the selected page back to the site in its original state
$settings = get_option( 'wpmm_settings' );
if ( ! empty( $settings['design']['page_id'] ) ) {
wpmm_restore_page_state( (int) $settings['design']['page_id'] );
}

wpmm_delete_cache();
}

Expand Down
85 changes: 85 additions & 0 deletions includes/functions/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,91 @@ function sanitize_hex_color( $color ) {
}
}

/**
* Record the current state of the selected maintenance page so it can be
* restored when maintenance mode is disabled or the plugin is deactivated.
*
* A previously recorded state is never overwritten: the page may already be
* carrying plugin-made changes, and recording those would poison the original
* state. The record is cleared when the page is restored.
*
* @since 2.6.23
* @param int $page_id page ID
* @return void
*/
function wpmm_record_page_state( $page_id ) {
$status = get_post_status( $page_id );

if ( ! $status ) {
return;
}

$template = get_post_meta( $page_id, '_wp_page_template', true );

// a selection carried over from an old release already wears the plugin's
// template; recording that would make the takeover permanent
if ( $template === 'templates/wpmm-page-template.php' ) {
$template = '';
}

// a single first-write-wins meta keeps the record atomic: a racing request
// can neither save the plugin-modified state nor observe half a record
add_post_meta(
$page_id,
'_wpmm_original_state',
array(
'status' => $status,
'template' => $template,
),
true
);
}

/**
* Restore the selected maintenance page to its recorded original state.
* Pages without a recorded state are left untouched.
*
* The status is only put back while the page still carries the status the
* plugin applied: any other status — trash included — was the user's own
* doing and expresses intent the record predates.
*
* @since 2.6.23
* @param int $page_id page ID
* @return void
*/
function wpmm_restore_page_state( $page_id ) {
$state = get_post_meta( $page_id, '_wpmm_original_state', true );

if ( empty( $state ) || ! is_array( $state ) || ! get_post( $page_id ) ) {
return;
}

$original_status = isset( $state['status'] ) ? $state['status'] : '';
$applied_status = get_post_meta( $page_id, '_wpmm_applied_status', true );
$current_status = get_post_status( $page_id );

if ( $original_status && $applied_status && $current_status === $applied_status && $current_status !== $original_status ) {
wp_update_post(
array(
'ID' => $page_id,
'post_status' => $original_status,
)
);
}

$original_template = isset( $state['template'] ) ? $state['template'] : '';

if ( empty( $original_template ) ) {
delete_post_meta( $page_id, '_wp_page_template' );
} else {
update_post_meta( $page_id, '_wp_page_template', $original_template );
}

// clear the record so the next take-over captures the page's state afresh
delete_post_meta( $page_id, '_wpmm_original_state' );
delete_post_meta( $page_id, '_wpmm_applied_status' );
}

/**
* Get option page URL.
*/
Expand Down
Loading
Loading