| @@ -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,12 @@ | ||
| 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' => __( 'Controls how often WordPress checks in with your server in the background.', 'xspeed' ), | |
| 41 | + 'group' => 'performance', | |
| 39 | 42 | ); |
| 40 | 43 | } |
| 41 | 44 | |
| 42 | 45 | /** |
| @@ -58,10 +61,10 @@ | ||
| 58 | 61 | 'type' => 'enum', |
| 59 | 62 | 'default' => self::BEHAVIOR_THROTTLE, |
| 60 | 63 | 'options' => $behavior_options, |
| 61 | 64 | 'option_labels' => $behavior_option_labels, |
| 62 | - 'label' => 'Dashboard', | |
| 63 | - 'description' => 'Heartbeat behavior on /wp-admin/ screens (autosave, notifications).', | |
| 65 | + 'label' => __( 'Dashboard', 'xspeed' ), | |
| 66 | + 'description' => __( 'Background checks on admin screens, used for notifications.', 'xspeed' ), | |
| 64 | 67 | ), |
| 65 | 68 | 'behavior_editor' => array( |
| 66 | 69 | 'type' => 'enum', |
| 67 | 70 | 'default' => self::BEHAVIOR_THROTTLE, |
| @@ -66,10 +69,10 @@ | ||
| 66 | 69 | 'type' => 'enum', |
| 67 | 70 | 'default' => self::BEHAVIOR_THROTTLE, |
| 68 | 71 | 'options' => $behavior_options, |
| 69 | 72 | '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.', | |
| 73 | + 'label' => __( 'Editor', 'xspeed' ), | |
| 74 | + 'description' => __( 'Background checks in the post editor. Disable only if you do not need autosave or the warning when someone else is editing.', 'xspeed' ), | |
| 72 | 75 | ), |
| 73 | 76 | 'behavior_frontend' => array( |
| 74 | 77 | 'type' => 'enum', |
| 75 | 78 | 'default' => self::BEHAVIOR_DISABLE, |
| @@ -74,10 +77,10 @@ | ||
| 74 | 77 | 'type' => 'enum', |
| 75 | 78 | 'default' => self::BEHAVIOR_DISABLE, |
| 76 | 79 | 'options' => $behavior_options, |
| 77 | 80 | '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.', | |
| 81 | + 'label' => __( 'Frontend', 'xspeed' ), | |
| 82 | + 'description' => __( 'Background checks on the public site, which only some plugins add. Disable is recommended, as it cuts extra requests to your server.', 'xspeed' ), | |
| 80 | 83 | ), |
| 81 | 84 | 'frequency' => array( |
| 82 | 85 | 'type' => 'int', |
| 83 | 86 | 'default' => 60, |
| @@ -82,10 +85,19 @@ | ||
| 82 | 85 | 'type' => 'int', |
| 83 | 86 | 'default' => 60, |
| 84 | 87 | 'min' => 15, |
| 85 | 88 | '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.', | |
| 89 | + 'label' => __( 'Throttle interval', 'xspeed' ), | |
| 90 | + 'unit' => 'seconds', | |
| 91 | + 'description' => __( 'Seconds between checks where you chose Throttle. 60 suits most sites; a lower number means more requests.', 'xspeed' ), | |
| 92 | + 'advanced' => true, | |
| 93 | + 'dependsOn' => array( | |
| 94 | + 'any' => array( | |
| 95 | + array( 'field' => 'behavior_dashboard', 'value' => self::BEHAVIOR_THROTTLE ), | |
| 96 | + array( 'field' => 'behavior_editor', 'value' => self::BEHAVIOR_THROTTLE ), | |
| 97 | + array( 'field' => 'behavior_frontend', 'value' => self::BEHAVIOR_THROTTLE ), | |
| 98 | + ), | |
| 99 | + ), | |
| 88 | 100 | ), |
| 89 | 101 | ); |
| 90 | 102 | } |
| 91 | 103 | |
| @@ -97,8 +109,9 @@ | ||
| 97 | 109 | array( |
| 98 | 110 | 'name' => 'xspeed heartbeat', |
| 99 | 111 | 'callback' => array( $this, 'cli_handler' ), |
| 100 | 112 | 'shortdesc' => 'Inspect or modify xSpeed heartbeat settings.', |
| 113 | + '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 | 114 | 'synopsis' => array( |
| 102 | 115 | array( |
| 103 | 116 | 'type' => 'positional', |
| 104 | 117 | 'name' => 'action', |
| @@ -104,19 +117,25 @@ | ||
| 104 | 117 | 'name' => 'action', |
| 105 | 118 | 'options' => array( 'show', 'set' ), |
| 106 | 119 | 'optional' => false, |
| 107 | 120 | ), |
| 121 | + // Use `--for` (not `--context`): WP-CLI silently swallows a | |
| 122 | + // `--context` assoc arg before it reaches the command, so | |
| 123 | + // `set --context= --behavior=` always lost the context and | |
| 124 | + // errored "Nothing to set". `--for` reads naturally | |
| 125 | + // (set --for=editor --behavior=disable) and is collision- | |
| 126 | + // free. No `options` constraint either — an options list | |
| 127 | + // also dropped args from $assoc; we validate in | |
| 128 | + // cli_handler() instead. (FBS-82153 Bug 2.) | |
| 108 | 129 | array( |
| 109 | 130 | 'type' => 'assoc', |
| 110 | - 'name' => 'context', | |
| 131 | + 'name' => 'for', | |
| 111 | 132 | 'optional' => true, |
| 112 | - 'options' => self::CONTEXTS, | |
| 113 | 133 | ), |
| 114 | 134 | array( |
| 115 | 135 | 'type' => 'assoc', |
| 116 | 136 | 'name' => 'behavior', |
| 117 | 137 | 'optional' => true, |
| 118 | - 'options' => array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE ), | |
| 119 | 138 | ), |
| 120 | 139 | array( |
| 121 | 140 | 'type' => 'assoc', |
| 122 | 141 | 'name' => 'frequency', |
| @@ -127,12 +146,23 @@ | ||
| 127 | 146 | ); |
| 128 | 147 | } |
| 129 | 148 | |
| 130 | 149 | 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 ); | |
| 150 | + // Context resolution depends on WHEN we can classify the request: | |
| 151 | + // - Frontend: known at `init` (is_admin() is reliable there), and we | |
| 152 | + // must act before wp_enqueue_scripts (priority 10) registers the | |
| 153 | + // heartbeat script. | |
| 154 | + // - Admin: dashboard-vs-editor needs get_current_screen(), which is | |
| 155 | + // NULL at `init` and only populated from the `current_screen` | |
| 156 | + // action onward. Resolving context at init always returned | |
| 157 | + // "dashboard" on editor screens, so behavior_editor was dead. | |
| 158 | + // (FBS-82153 Bug 1.) Hook admin on `current_screen` instead, which | |
| 159 | + // fires before admin_enqueue_scripts so we can still dequeue. | |
| 160 | + if ( is_admin() ) { | |
| 161 | + add_action( 'current_screen', array( $this, 'apply_settings' ) ); | |
| 162 | + } else { | |
| 163 | + add_action( 'init', array( $this, 'apply_settings' ), 5 ); | |
| 164 | + } | |
| 135 | 165 | } |
| 136 | 166 | |
| 137 | 167 | /** |
| 138 | 168 | * Hook handler — translates our settings into Heartbeat behavior. Runs |
| @@ -203,17 +233,35 @@ | ||
| 203 | 233 | return; |
| 204 | 234 | } |
| 205 | 235 | |
| 206 | 236 | if ( 'set' === $action ) { |
| 207 | - $patch = array(); | |
| 208 | - if ( isset( $assoc['context'], $assoc['behavior'] ) ) { | |
| 209 | - $patch[ 'behavior_' . $assoc['context'] ] = $assoc['behavior']; | |
| 237 | + $patch = array(); | |
| 238 | + $behaviors = array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE ); | |
| 239 | + | |
| 240 | + $has_context = isset( $assoc['for'] ) && '' !== $assoc['for']; | |
| 241 | + $has_behavior = isset( $assoc['behavior'] ) && '' !== $assoc['behavior']; | |
| 242 | + | |
| 243 | + // --for and --behavior are a pair: both or neither. | |
| 244 | + if ( $has_context xor $has_behavior ) { | |
| 245 | + \WP_CLI::error( 'Pass --for and --behavior together.' ); | |
| 246 | + return; | |
| 210 | 247 | } |
| 248 | + if ( $has_context && $has_behavior ) { | |
| 249 | + if ( ! in_array( $assoc['for'], self::CONTEXTS, true ) ) { | |
| 250 | + \WP_CLI::error( 'Invalid --for. Use one of: ' . implode( ', ', self::CONTEXTS ) . '.' ); | |
| 251 | + return; | |
| 252 | + } | |
| 253 | + if ( ! in_array( $assoc['behavior'], $behaviors, true ) ) { | |
| 254 | + \WP_CLI::error( 'Invalid --behavior. Use one of: ' . implode( ', ', $behaviors ) . '.' ); | |
| 255 | + return; | |
| 256 | + } | |
| 257 | + $patch[ 'behavior_' . $assoc['for'] ] = $assoc['behavior']; | |
| 258 | + } | |
| 211 | 259 | if ( isset( $assoc['frequency'] ) ) { |
| 212 | 260 | $patch['frequency'] = (int) $assoc['frequency']; |
| 213 | 261 | } |
| 214 | 262 | if ( empty( $patch ) ) { |
| 215 | - \WP_CLI::error( 'Nothing to set. Provide --context= --behavior= and/or --frequency=' ); | |
| 263 | + \WP_CLI::error( 'Nothing to set. Provide --for= --behavior= and/or --frequency=' ); | |
| 216 | 264 | return; |
| 217 | 265 | } |
| 218 | 266 | $this->update_settings( $patch ); |
| 219 | 267 | \WP_CLI::success( 'Updated heartbeat settings.' ); |