Release 2.4.2: core 1.4.2 bugfix #11

Merged
wpdev merged 7 commits from hardening/2.4.2 into main 2026-09-28 18:51:40 +08:00
3 changed files with 160 additions and 25 deletions
Showing only changes of commit 86ac84e07c - Show all commits
+1 -1
View File
@@ -92,7 +92,7 @@
if ( data.success ) {
showStatus( statusEl, i18n.saved, false );
} else {
showStatus( statusEl, i18n.error, true );
showStatus( statusEl, ( data.data && data.data.message ) || i18n.error, true );
}
} )
.catch( () => {
+81 -24
View File
@@ -398,17 +398,7 @@ class TMDO_Admin {
// v2.5.4: Entity Bridge settings (v2.11.0: post added).
update_option( 'wpdo_hook_bus_enabled', isset( $_POST['wpdo_hook_bus_enabled'] ) ? '1' : '0', false );
if ( class_exists( 'TMDO_Mode_Manager' ) ) {
foreach ( array( 'user', 'post', 'term', 'comment' ) as $entity_type ) {
$new_mode = isset( $_POST[ 'wpdo_bridge_mode_' . $entity_type ] )
? sanitize_key( wp_unslash( (string) $_POST[ 'wpdo_bridge_mode_' . $entity_type ] ) )
: '';
if ( TMDO_Mode_Manager::is_valid_mode( $new_mode ) ) {
TMDO_Mode_Manager::set( $entity_type, $new_mode );
}
}
TMDO_Mode_Manager::reset_cache();
}
self::remember_bridge_mode_refusals( self::apply_bridge_modes( self::posted_bridge_modes() ) );
if ( class_exists( 'TMDO_Hook_Bus_Bridge' ) ) {
TMDO_Hook_Bus_Bridge::reset_cache();
}
@@ -3144,6 +3134,9 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
if ( isset( $_GET['wpdo_msg'] ) && 'settings_saved' === sanitize_key( wp_unslash( (string) $_GET['wpdo_msg'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( '設定已儲存。', '2meet-data-optimizer' ) . '</p></div>';
}
foreach ( self::pull_bridge_mode_refusals() as $refused ) {
echo '<div class="notice notice-error"><p>' . esc_html__( '模式未變更:', '2meet-data-optimizer' ) . esc_html( $refused ) . '</p></div>';
}
$enabled = class_exists( 'TMDO_Email_Notifier' ) && TMDO_Email_Notifier::is_enabled();
$email = class_exists( 'TMDO_Email_Notifier' ) ? TMDO_Email_Notifier::recipient() : '';
$throttle = class_exists( 'TMDO_Email_Notifier' ) ? TMDO_Email_Notifier::throttle_hours() : 24;
@@ -3743,22 +3736,33 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
}
}
private static function save_section_entity_bridge(): void {
/**
* Save the Entity Bridge section (Hook Bus switch and bridge modes).
*
* @return string[] Refused changes, "<entity>:<reason>".
*/
private static function save_section_entity_bridge(): array {
update_option( 'wpdo_hook_bus_enabled', isset( $_POST['wpdo_hook_bus_enabled'] ) ? '1' : '0', false );
if ( class_exists( 'TMDO_Mode_Manager' ) ) {
foreach ( array( 'user', 'post', 'term', 'comment' ) as $entity_type ) {
$new_mode = isset( $_POST[ 'wpdo_bridge_mode_' . $entity_type ] )
? sanitize_key( wp_unslash( (string) $_POST[ 'wpdo_bridge_mode_' . $entity_type ] ) )
: '';
if ( TMDO_Mode_Manager::is_valid_mode( $new_mode ) ) {
TMDO_Mode_Manager::set( $entity_type, $new_mode );
}
}
TMDO_Mode_Manager::reset_cache();
}
$refused = self::apply_bridge_modes( self::posted_bridge_modes() );
if ( class_exists( 'TMDO_Hook_Bus_Bridge' ) ) {
TMDO_Hook_Bus_Bridge::reset_cache();
}
return $refused;
}
/**
* Bridge modes posted by the settings form, keyed by entity type ('' when absent).
*
* @return array<string, string>
*/
private static function posted_bridge_modes(): array {
$posted = array();
foreach ( array( 'user', 'post', 'term', 'comment' ) as $entity_type ) {
$posted[ $entity_type ] = isset( $_POST[ 'wpdo_bridge_mode_' . $entity_type ] )
? sanitize_key( wp_unslash( (string) $_POST[ 'wpdo_bridge_mode_' . $entity_type ] ) )
: '';
}
return $posted;
}
private static function save_section_hp_transient(): void {
@@ -3786,6 +3790,56 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
}
// phpcs:enable WordPress.Security.NonceVerification.Missing,Squiz.Commenting.FunctionComment.Missing,Squiz.Commenting.FunctionComment.WrongStyle
/**
* Apply posted bridge modes through TMDO_Mode_Manager::set(). Empty or
* unknown modes are skipped; refused changes (unsafe jump, Promotion Guard)
* are returned instead of being dropped silently (2.4.2 PR-135).
*
* @param array<string, string> $posted Mode per entity type.
* @return string[] Refused changes, "<entity>:<reason>".
*/
public static function apply_bridge_modes( array $posted ): array {
if ( ! class_exists( 'TMDO_Mode_Manager' ) ) {
return array();
}
$refused = array();
foreach ( $posted as $entity_type => $new_mode ) {
if ( ! TMDO_Mode_Manager::is_valid_mode( (string) $new_mode ) ) {
continue;
}
$result = TMDO_Mode_Manager::set( (string) $entity_type, (string) $new_mode );
if ( is_wp_error( $result ) ) {
$refused[] = $entity_type . ':' . $result->get_error_message();
}
}
TMDO_Mode_Manager::reset_cache();
return $refused;
}
/**
* Keep refused bridge-mode changes for the page the settings form redirects to.
*
* @param string[] $refused Refused changes.
* @return void
*/
public static function remember_bridge_mode_refusals( array $refused ): void {
if ( $refused ) {
set_transient( 'wpdo_mode_refused_' . get_current_user_id(), $refused, 5 * MINUTE_IN_SECONDS );
}
}
/**
* Refused bridge-mode changes remembered for this user, once.
*
* @return string[]
*/
public static function pull_bridge_mode_refusals(): array {
$key = 'wpdo_mode_refused_' . get_current_user_id();
$refused = get_transient( $key );
delete_transient( $key );
return is_array( $refused ) ? $refused : array();
}
/**
* AJAX: save a single settings section without a full page reload.
*/
@@ -3807,7 +3861,10 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
if ( ! isset( $handlers[ $section ] ) ) {
wp_send_json_error( 'invalid_section', 400 );
}
self::{$handlers[ $section ]}();
$refused = self::{$handlers[ $section ]}();
if ( is_array( $refused ) && $refused ) {
wp_send_json_error( array( 'message' => __( '部分模式未變更:', '2meet-data-optimizer' ) . implode( ';', $refused ) ) );
}
wp_send_json_success( array( 'message' => __( '設定已儲存。', '2meet-data-optimizer' ) ) );
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* The admin settings form saves bridge modes through TMDO_Mode_Manager::set().
* A refused change (unsafe jump, Promotion Guard) used to be dropped silently:
* the page said "saved" while the mode stayed put (2.4.2 PR-135, manual F-2).
*/
class AdminBridgeModeSaveTest extends TestCase {
public static function setUpBeforeClass(): void {
require_once TMDO_PATH . 'admin/class-tmdo-admin.php';
}
protected function setUp(): void {
ini_set( 'error_log', '/dev/null' );
$GLOBALS['_wp_options'] = array();
$GLOBALS['_wp_filter_callbacks'] = array();
$GLOBALS['_wp_current_user_can'] = array( 'manage_options' => true );
$GLOBALS['_wp_current_user_id'] = 7;
TMDO_Mode_Manager::reset_cache();
}
private function modes( array $modes ): void {
$GLOBALS['_wp_options']['wpdo_bridge_modes'] = $modes;
TMDO_Mode_Manager::reset_cache();
}
public function test_an_accepted_change_is_applied_without_refusals(): void {
$this->modes( array( 'user' => 'disabled' ) );
$refused = TMDO_Admin::apply_bridge_modes( array( 'user' => 'dual_write' ) );
$this->assertSame( array(), $refused );
$this->assertSame( 'dual_write', TMDO_Mode_Manager::get( 'user' ) );
}
public function test_an_unsafe_jump_is_reported_and_the_mode_kept(): void {
$this->modes( array( 'post' => 'disabled' ) );
$refused = TMDO_Admin::apply_bridge_modes( array( 'post' => 'shadow_read' ) );
$this->assertCount( 1, $refused );
$this->assertStringStartsWith( 'post', $refused[0] );
$this->assertSame( 'disabled', TMDO_Mode_Manager::get( 'post' ) );
}
public function test_empty_or_unknown_modes_are_ignored(): void {
$this->modes( array( 'term' => 'disabled' ) );
$this->assertSame( array(), TMDO_Admin::apply_bridge_modes( array( 'post' => '', 'term' => 'bogus' ) ) );
$this->assertSame( 'disabled', TMDO_Mode_Manager::get( 'term' ) );
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function test_a_promotion_refused_by_the_guard_is_reported(): void {
eval( 'namespace HivePress; class Core {}' );
$this->modes( array( 'user' => 'shadow_read' ) );
$refused = TMDO_Admin::apply_bridge_modes( array( 'user' => 'aeav_only' ) );
$this->assertCount( 1, $refused );
$this->assertSame( 'shadow_read', TMDO_Mode_Manager::get( 'user' ) );
}
/** The full-page form redirects after saving: refusals wait in a per-user transient, shown once. */
public function test_refusals_are_kept_for_the_next_page_view_only(): void {
TMDO_Admin::remember_bridge_mode_refusals( array( 'post:refused' ) );
$this->assertSame( array( 'post:refused' ), TMDO_Admin::pull_bridge_mode_refusals() );
$this->assertSame( array(), TMDO_Admin::pull_bridge_mode_refusals() );
}
}