| @@ -15,8 +15,10 @@ | ||
| 15 | 15 | declare(strict_types=1); |
| 16 | 16 | |
| 17 | 17 | namespace XSpeed\Modules\Heartbeat; |
| 18 | 18 | |
| 19 | +defined( 'ABSPATH' ) || exit; | |
| 20 | + | |
| 19 | 21 | use XSpeed\Module; |
| 20 | 22 | |
| 21 | 23 | final class HeartbeatModule extends Module { |
| 22 | 24 | |
| @@ -32,11 +34,11 @@ | ||
| 32 | 34 | private const CONTEXTS = array( 'dashboard', 'editor', 'frontend' ); |
| 33 | 35 | |
| 34 | 36 | public function ui_metadata(): array { |
| 35 | 37 | return array( |
| 36 | - 'label' => 'Heartbeat', | |
| 38 | + 'label' => __( 'Heartbeat', 'xspeed' ), | |
| 37 | 39 | 'icon' => 'Activity', |
| 38 | - 'description' => 'Control the WordPress Heartbeat API per context.', | |
| 40 | + 'description' => __( 'Control the WordPress Heartbeat API per context.', 'xspeed' ), | |
| 39 | 41 | ); |
| 40 | 42 | } |
| 41 | 43 | |
| 42 | 44 | /** |
| @@ -58,10 +60,10 @@ | ||
| 58 | 60 | 'type' => 'enum', |
| 59 | 61 | 'default' => self::BEHAVIOR_THROTTLE, |
| 60 | 62 | 'options' => $behavior_options, |
| 61 | 63 | 'option_labels' => $behavior_option_labels, |
| 62 | - 'label' => 'Dashboard', | |
| 63 | - 'description' => 'Heartbeat behavior on /wp-admin/ screens (autosave, notifications).', | |
| 64 | + 'label' => __( 'Dashboard', 'xspeed' ), | |
| 65 | + 'description' => __( 'Heartbeat behavior on /wp-admin/ screens (autosave, notifications).', 'xspeed' ), | |
| 64 | 66 | ), |
| 65 | 67 | 'behavior_editor' => array( |
| 66 | 68 | 'type' => 'enum', |
| 67 | 69 | 'default' => self::BEHAVIOR_THROTTLE, |
| @@ -66,10 +68,10 @@ | ||
| 66 | 68 | 'type' => 'enum', |
| 67 | 69 | 'default' => self::BEHAVIOR_THROTTLE, |
| 68 | 70 | 'options' => $behavior_options, |
| 69 | 71 | 'option_labels' => $behavior_option_labels, |
| 70 | - 'label' => 'Editor', | |
| 71 | - 'description' => 'Heartbeat in the post / block editor. Disable only if you do not need autosave or co-edit locks.', | |
| 72 | + 'label' => __( 'Editor', 'xspeed' ), | |
| 73 | + 'description' => __( 'Heartbeat in the post / block editor. Disable only if you do not need autosave or co-edit locks.', 'xspeed' ), | |
| 72 | 74 | ), |
| 73 | 75 | 'behavior_frontend' => array( |
| 74 | 76 | 'type' => 'enum', |
| 75 | 77 | 'default' => self::BEHAVIOR_DISABLE, |
| @@ -74,10 +76,10 @@ | ||
| 74 | 76 | 'type' => 'enum', |
| 75 | 77 | 'default' => self::BEHAVIOR_DISABLE, |
| 76 | 78 | 'options' => $behavior_options, |
| 77 | 79 | 'option_labels' => $behavior_option_labels, |
| 78 | - 'label' => 'Frontend', | |
| 79 | - 'description' => 'Heartbeat on the public site. Recommended off — most themes never need it and it costs admin-ajax requests per visitor.', | |
| 80 | + 'label' => __( 'Frontend', 'xspeed' ), | |
| 81 | + 'description' => __( 'Controls the Heartbeat API on the public site. Only takes effect when a plugin or theme actually loads heartbeat on the frontend (e.g. WooCommerce cart fragments, membership/notification plugins) — a default WordPress site loads none there, so this has no visible effect on such sites. Recommended: Disable, to stop the admin-ajax polling those plugins add.', 'xspeed' ), | |
| 80 | 82 | ), |
| 81 | 83 | 'frequency' => array( |
| 82 | 84 | 'type' => 'int', |
| 83 | 85 | 'default' => 60, |
| @@ -82,10 +84,10 @@ | ||
| 82 | 84 | 'type' => 'int', |
| 83 | 85 | 'default' => 60, |
| 84 | 86 | 'min' => 15, |
| 85 | 87 | 'max' => 300, |
| 86 | - 'label' => 'Throttle Frequency', | |
| 87 | - 'description' => 'Interval in seconds for contexts set to Throttle. 60 is a sane default; lower = faster sync but more requests.', | |
| 88 | + 'label' => __( 'Throttle Frequency', 'xspeed' ), | |
| 89 | + 'description' => __( 'Interval in seconds for contexts set to Throttle. 60 is a sane default; lower = faster sync but more requests.', 'xspeed' ), | |
| 88 | 90 | ), |
| 89 | 91 | ); |
| 90 | 92 | } |
| 91 | 93 | |
| @@ -97,8 +99,9 @@ | ||
| 97 | 99 | array( |
| 98 | 100 | 'name' => 'xspeed heartbeat', |
| 99 | 101 | 'callback' => array( $this, 'cli_handler' ), |
| 100 | 102 | 'shortdesc' => 'Inspect or modify xSpeed heartbeat settings.', |
| 103 | + 'ai_hint' => 'Inspect or change WordPress Heartbeat (admin-ajax polling) frequency. Use for high admin-ajax.php CPU load, or hosting warnings about too many background requests.', | |
| 101 | 104 | 'synopsis' => array( |
| 102 | 105 | array( |
| 103 | 106 | 'type' => 'positional', |
| 104 | 107 | 'name' => 'action', |
| @@ -104,19 +107,25 @@ | ||
| 104 | 107 | 'name' => 'action', |
| 105 | 108 | 'options' => array( 'show', 'set' ), |
| 106 | 109 | 'optional' => false, |
| 107 | 110 | ), |
| 111 | + // Use `--for` (not `--context`): WP-CLI silently swallows a | |
| 112 | + // `--context` assoc arg before it reaches the command, so | |
| 113 | + // `set --context= --behavior=` always lost the context and | |
| 114 | + // errored "Nothing to set". `--for` reads naturally | |
| 115 | + // (set --for=editor --behavior=disable) and is collision- | |
| 116 | + // free. No `options` constraint either — an options list | |
| 117 | + // also dropped args from $assoc; we validate in | |
| 118 | + // cli_handler() instead. (FBS-82153 Bug 2.) | |
| 108 | 119 | array( |
| 109 | 120 | 'type' => 'assoc', |
| 110 | - 'name' => 'context', | |
| 121 | + 'name' => 'for', | |
| 111 | 122 | 'optional' => true, |
| 112 | - 'options' => self::CONTEXTS, | |
| 113 | 123 | ), |
| 114 | 124 | array( |
| 115 | 125 | 'type' => 'assoc', |
| 116 | 126 | 'name' => 'behavior', |
| 117 | 127 | 'optional' => true, |
| 118 | - 'options' => array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE ), | |
| 119 | 128 | ), |
| 120 | 129 | array( |
| 121 | 130 | 'type' => 'assoc', |
| 122 | 131 | 'name' => 'frequency', |
| @@ -127,12 +136,23 @@ | ||
| 127 | 136 | ); |
| 128 | 137 | } |
| 129 | 138 | |
| 130 | 139 | public function boot(): void { |
| 131 | - // `init` is early enough to register our filters before Heartbeat | |
| 132 | - // itself enqueues. Stay low priority to defer to plugins that ran | |
| 133 | - // at plugins_loaded. | |
| 134 | - add_action( 'init', array( $this, 'apply_settings' ), 5 ); | |
| 140 | + // Context resolution depends on WHEN we can classify the request: | |
| 141 | + // - Frontend: known at `init` (is_admin() is reliable there), and we | |
| 142 | + // must act before wp_enqueue_scripts (priority 10) registers the | |
| 143 | + // heartbeat script. | |
| 144 | + // - Admin: dashboard-vs-editor needs get_current_screen(), which is | |
| 145 | + // NULL at `init` and only populated from the `current_screen` | |
| 146 | + // action onward. Resolving context at init always returned | |
| 147 | + // "dashboard" on editor screens, so behavior_editor was dead. | |
| 148 | + // (FBS-82153 Bug 1.) Hook admin on `current_screen` instead, which | |
| 149 | + // fires before admin_enqueue_scripts so we can still dequeue. | |
| 150 | + if ( is_admin() ) { | |
| 151 | + add_action( 'current_screen', array( $this, 'apply_settings' ) ); | |
| 152 | + } else { | |
| 153 | + add_action( 'init', array( $this, 'apply_settings' ), 5 ); | |
| 154 | + } | |
| 135 | 155 | } |
| 136 | 156 | |
| 137 | 157 | /** |
| 138 | 158 | * Hook handler — translates our settings into Heartbeat behavior. Runs |
| @@ -203,17 +223,35 @@ | ||
| 203 | 223 | return; |
| 204 | 224 | } |
| 205 | 225 | |
| 206 | 226 | if ( 'set' === $action ) { |
| 207 | - $patch = array(); | |
| 208 | - if ( isset( $assoc['context'], $assoc['behavior'] ) ) { | |
| 209 | - $patch[ 'behavior_' . $assoc['context'] ] = $assoc['behavior']; | |
| 227 | + $patch = array(); | |
| 228 | + $behaviors = array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE ); | |
| 229 | + | |
| 230 | + $has_context = isset( $assoc['for'] ) && '' !== $assoc['for']; | |
| 231 | + $has_behavior = isset( $assoc['behavior'] ) && '' !== $assoc['behavior']; | |
| 232 | + | |
| 233 | + // --for and --behavior are a pair: both or neither. | |
| 234 | + if ( $has_context xor $has_behavior ) { | |
| 235 | + \WP_CLI::error( 'Pass --for and --behavior together.' ); | |
| 236 | + return; | |
| 210 | 237 | } |
| 238 | + if ( $has_context && $has_behavior ) { | |
| 239 | + if ( ! in_array( $assoc['for'], self::CONTEXTS, true ) ) { | |
| 240 | + \WP_CLI::error( 'Invalid --for. Use one of: ' . implode( ', ', self::CONTEXTS ) . '.' ); | |
| 241 | + return; | |
| 242 | + } | |
| 243 | + if ( ! in_array( $assoc['behavior'], $behaviors, true ) ) { | |
| 244 | + \WP_CLI::error( 'Invalid --behavior. Use one of: ' . implode( ', ', $behaviors ) . '.' ); | |
| 245 | + return; | |
| 246 | + } | |
| 247 | + $patch[ 'behavior_' . $assoc['for'] ] = $assoc['behavior']; | |
| 248 | + } | |
| 211 | 249 | if ( isset( $assoc['frequency'] ) ) { |
| 212 | 250 | $patch['frequency'] = (int) $assoc['frequency']; |
| 213 | 251 | } |
| 214 | 252 | if ( empty( $patch ) ) { |
| 215 | - \WP_CLI::error( 'Nothing to set. Provide --context= --behavior= and/or --frequency=' ); | |
| 253 | + \WP_CLI::error( 'Nothing to set. Provide --for= --behavior= and/or --frequency=' ); | |
| 216 | 254 | return; |
| 217 | 255 | } |
| 218 | 256 | $this->update_settings( $patch ); |
| 219 | 257 | \WP_CLI::success( 'Updated heartbeat settings.' ); |