| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — generic content-change realtime layer. | |
| 3 | + * OpenStation — generic content-change realtime layer. | |
| 4 | 4 | * |
| 5 | 5 | * Records create/update/trash mutations of posts, pages, CPTs, |
| 6 | 6 | * comments, and WooCommerce orders into a per-request changelog and |
| 7 | 7 | * relays them to the parent shell as cross-window broadcasts |
| 8 | - * (`desktop-mode.<type>.changed`, payload `{ source, action, ids }`), | |
| 8 | + * (`os.<type>.changed`, payload `{ source, action, ids }`), | |
| 9 | 9 | * so every window listing that content type can refresh without an F5. |
| 10 | 10 | * |
| 11 | 11 | * Three delivery paths, mirroring the Recycle Bin realtime layer |
| 12 | 12 | * (`includes/recycle-bin/realtime.php`) which delegates its own |
| @@ -13,9 +13,9 @@ | ||
| 13 | 13 | * changelog into this module: |
| 14 | 14 | * |
| 15 | 15 | * 1. **Chromeless footer — fast path.** At `admin_footer` on a |
| 16 | 16 | * chromeless render, one inline script postMessages a |
| 17 | - * `desktop-mode-broadcast` envelope per `[type][action]` entry | |
| 17 | + * `os-broadcast` envelope per `[type][action]` entry | |
| 18 | 18 | * to the parent shell. Because the dominant admin mutation flow |
| 19 | 19 | * is form-POST → 302 → GET (the mutating request renders no |
| 20 | 20 | * footer), the changelog is buffered across the redirect in a |
| 21 | 21 | * short-TTL per-user transient and flushed on the next |
| @@ -32,36 +32,36 @@ | ||
| 32 | 32 | * "entries newer than your seen ts" for opted-in shells. Covers |
| 33 | 33 | * Quick Edit, AJAX status flips, other browser tabs, REST and |
| 34 | 34 | * WP-CLI mutations within one tick (15–60 s). |
| 35 | 35 | * |
| 36 | - * @package WPDesktopMode | |
| 37 | - * @since 0.9.7 | |
| 36 | + * @package OpenStation | |
| 38 | 37 | */ |
| 39 | 38 | |
| 40 | 39 | defined( 'ABSPATH' ) || exit; |
| 41 | 40 | |
| 42 | -const DESKTOP_MODE_CONTENT_CHANGES_LOG_OPTION = '_desktop_mode_content_changes_log'; | |
| 41 | +/** | |
| 42 | + * The VALUE keeps its pre-rebrand spelling on purpose: it is a | |
| 43 | + * persisted or externally-visible identifier, so renaming it would | |
| 44 | + * orphan data already written by live installs (or break a live | |
| 45 | + * URL). The mismatch between this constant's name and its value is | |
| 46 | + * deliberate — it is NOT a half-finished rename. | |
| 47 | + */ | |
| 48 | +const OPENSTATION_CONTENT_CHANGES_LOG_OPTION = '_desktop_mode_content_changes_log'; | |
| 43 | 49 | |
| 44 | 50 | /** |
| 45 | 51 | * Milliseconds of heartbeat-changelog history to retain. |
| 46 | - * | |
| 47 | - * @since 0.9.7 | |
| 48 | 52 | */ |
| 49 | -const DESKTOP_MODE_CONTENT_CHANGES_LOG_WINDOW_MS = 300000; | |
| 53 | +const OPENSTATION_CONTENT_CHANGES_LOG_WINDOW_MS = 300000; | |
| 50 | 54 | |
| 51 | 55 | /** |
| 52 | 56 | * Maximum retained heartbeat-changelog entries. |
| 53 | - * | |
| 54 | - * @since 0.9.7 | |
| 55 | 57 | */ |
| 56 | -const DESKTOP_MODE_CONTENT_CHANGES_LOG_MAX = 100; | |
| 58 | +const OPENSTATION_CONTENT_CHANGES_LOG_MAX = 100; | |
| 57 | 59 | |
| 58 | 60 | /** |
| 59 | 61 | * TTL for the redirect-surviving per-user changelog buffer. |
| 60 | - * | |
| 61 | - * @since 0.9.7 | |
| 62 | 62 | */ |
| 63 | -const DESKTOP_MODE_CONTENT_CHANGES_BUFFER_TTL = 60; | |
| 63 | +const OPENSTATION_CONTENT_CHANGES_BUFFER_TTL = 60; | |
| 64 | 64 | |
| 65 | 65 | /** |
| 66 | 66 | * Per-request state shared by the recorder, the footer emitter, and |
| 67 | 67 | * the shutdown handler. |
| @@ -75,13 +75,11 @@ | ||
| 75 | 75 | * - `staged` — flat `{ type, action, id }` rows for the heartbeat log. |
| 76 | 76 | * - `flushed` — true once the footer emitter consumed `log`, so the |
| 77 | 77 | * shutdown handler doesn't buffer it a second time. |
| 78 | 78 | * |
| 79 | - * @since 0.9.7 | |
| 80 | - * | |
| 81 | 79 | * @return array Per-request state, by reference. |
| 82 | 80 | */ |
| 83 | -function &desktop_mode_content_changes_state() { | |
| 81 | +function &openstation_content_changes_state() { | |
| 84 | 82 | static $state = null; |
| 85 | 83 | if ( null === $state ) { |
| 86 | 84 | $state = array( |
| 87 | 85 | 'log' => array(), |
| @@ -95,13 +93,12 @@ | ||
| 95 | 93 | |
| 96 | 94 | /** |
| 97 | 95 | * Resets the per-request state. Test isolation only. |
| 98 | 96 | * |
| 99 | - * @since 0.9.7 | |
| 100 | 97 | * @internal |
| 101 | 98 | */ |
| 102 | -function desktop_mode_content_changes_reset() { | |
| 103 | - $state = &desktop_mode_content_changes_state(); | |
| 99 | +function openstation_content_changes_reset() { | |
| 100 | + $state = &openstation_content_changes_state(); | |
| 104 | 101 | $state = array( |
| 105 | 102 | 'log' => array(), |
| 106 | 103 | 'seen' => array(), |
| 107 | 104 | 'staged' => array(), |
| @@ -115,9 +112,9 @@ | ||
| 115 | 112 | * This is the public entry point for third-party plugins with their |
| 116 | 113 | * own storage (an HPOS-style custom table, a settings screen, …): |
| 117 | 114 | * call it from your mutation path and every open window listing your |
| 118 | 115 | * type refreshes, exactly like core content. Pair it with a |
| 119 | - * `desktop_mode_soft_reload_rules` filter entry if your list screen | |
| 116 | + * `openstation_soft_reload_rules` filter entry if your list screen | |
| 120 | 117 | * is not a standard `edit.php?post_type=<type>` page. |
| 121 | 118 | * |
| 122 | 119 | * Dedupe is first-writer-wins per `type:id` within the request: core |
| 123 | 120 | * fires the trash verbs (`wp_trash_post`, `untrash_post`) BEFORE the |
| @@ -125,19 +122,17 @@ | ||
| 125 | 122 | * specific verb is recorded and the follow-up `updated` for the same |
| 126 | 123 | * id is dropped. The same mechanism collapses WooCommerce legacy-mode |
| 127 | 124 | * double-fires (post hooks + `woocommerce_update_order`). |
| 128 | 125 | * |
| 129 | - * @since 0.9.7 | |
| 130 | - * | |
| 131 | 126 | * @param string $type Content type slug — a post type, `comment`, |
| 132 | 127 | * or `shop_order`. Becomes the broadcast topic |
| 133 | - * `desktop-mode.<type>.changed`. | |
| 128 | + * `os.<type>.changed`. | |
| 134 | 129 | * @param int $id Mutated object id. |
| 135 | 130 | * @param string $action One of 'created', 'updated', 'trashed', |
| 136 | 131 | * 'untrashed', 'deleted'. |
| 137 | 132 | * @return bool Whether the change was recorded. |
| 138 | 133 | */ |
| 139 | -function desktop_mode_content_changes_record( $type, $id, $action ) { | |
| 134 | +function openstation_content_changes_record( $type, $id, $action ) { | |
| 140 | 135 | $type = (string) $type; |
| 141 | 136 | $id = (int) $id; |
| 142 | 137 | $action = (string) $action; |
| 143 | 138 | |
| @@ -153,20 +148,18 @@ | ||
| 153 | 148 | * system entirely (footer broadcast AND heartbeat log) — e.g. a |
| 154 | 149 | * high-churn internal type whose list windows manage their own |
| 155 | 150 | * realtime. |
| 156 | 151 | * |
| 157 | - * @since 0.9.7 | |
| 158 | - * | |
| 159 | 152 | * @param bool $record Whether to record. Default true. |
| 160 | 153 | * @param string $type Content type slug. |
| 161 | 154 | * @param int $id Mutated object id. |
| 162 | 155 | * @param string $action Verb (created/updated/trashed/untrashed/deleted). |
| 163 | 156 | */ |
| 164 | - if ( ! apply_filters( 'desktop_mode_content_changes_should_record', true, $type, $id, $action ) ) { | |
| 157 | + if ( ! apply_filters( 'openstation_content_changes_should_record', true, $type, $id, $action ) ) { | |
| 165 | 158 | return false; |
| 166 | 159 | } |
| 167 | 160 | |
| 168 | - $state = &desktop_mode_content_changes_state(); | |
| 161 | + $state = &openstation_content_changes_state(); | |
| 169 | 162 | $key = $type . ':' . $id; |
| 170 | 163 | if ( isset( $state['seen'][ $key ] ) ) { |
| 171 | 164 | return false; |
| 172 | 165 | } |
| @@ -192,15 +185,13 @@ | ||
| 192 | 185 | * |
| 193 | 186 | * Subscribers can push their own real-time signal (websocket, |
| 194 | 187 | * SSE, …) without re-hooking every mutation path individually. |
| 195 | 188 | * |
| 196 | - * @since 0.9.7 | |
| 197 | - * | |
| 198 | 189 | * @param string $type Content type slug. |
| 199 | 190 | * @param int $id Mutated object id. |
| 200 | 191 | * @param string $action Verb (created/updated/trashed/untrashed/deleted). |
| 201 | 192 | */ |
| 202 | - do_action( 'desktop_mode_content_change_recorded', $type, $id, $action ); | |
| 193 | + do_action( 'openstation_content_change_recorded', $type, $id, $action ); | |
| 203 | 194 | |
| 204 | 195 | return true; |
| 205 | 196 | } |
| 206 | 197 | |
| @@ -206,14 +197,12 @@ | ||
| 206 | 197 | |
| 207 | 198 | /** |
| 208 | 199 | * Returns the per-request changelog: `[ type ][ action ] = int[] ids`. |
| 209 | 200 | * |
| 210 | - * @since 0.9.7 | |
| 211 | - * | |
| 212 | 201 | * @return array |
| 213 | 202 | */ |
| 214 | -function desktop_mode_content_changes_log() { | |
| 215 | - $state = &desktop_mode_content_changes_state(); | |
| 203 | +function openstation_content_changes_log() { | |
| 204 | + $state = &openstation_content_changes_state(); | |
| 216 | 205 | return $state['log']; |
| 217 | 206 | } |
| 218 | 207 | |
| 219 | 208 | /** |
| @@ -219,18 +208,16 @@ | ||
| 219 | 208 | /** |
| 220 | 209 | * Merges changelog `$b` into changelog `$a` (both |
| 221 | 210 | * `[ type ][ action ] = int[] ids`). |
| 222 | 211 | * |
| 223 | - * @since 0.9.7 | |
| 224 | - * | |
| 225 | 212 | * @param array $a Base changelog. |
| 226 | 213 | * @param array $b Changelog to merge in. |
| 227 | 214 | * @return array |
| 228 | 215 | */ |
| 229 | -function desktop_mode_content_changes_merge( $a, $b ) { | |
| 216 | +function openstation_content_changes_merge( $a, $b ) { | |
| 230 | 217 | foreach ( (array) $b as $type => $by_action ) { |
| 231 | 218 | foreach ( (array) $by_action as $action => $ids ) { |
| 232 | - $existing = isset( $a[ $type ][ $action ] ) ? (array) $a[ $type ][ $action ] : array(); | |
| 219 | + $existing = isset( $a[ $type ][ $action ] ) ? (array) $a[ $type ][ $action ] : array(); | |
| 233 | 220 | $a[ $type ][ $action ] = array_values( array_unique( array_merge( $existing, array_map( 'intval', (array) $ids ) ) ) ); |
| 234 | 221 | } |
| 235 | 222 | } |
| 236 | 223 | return $a; |
| @@ -238,15 +225,13 @@ | ||
| 238 | 225 | |
| 239 | 226 | /** |
| 240 | 227 | * Transient key of the redirect-surviving changelog buffer for a user. |
| 241 | 228 | * |
| 242 | - * @since 0.9.7 | |
| 243 | - * | |
| 244 | 229 | * @param int $user_id User id. |
| 245 | 230 | * @return string |
| 246 | 231 | */ |
| 247 | -function desktop_mode_content_changes_buffer_key( $user_id ) { | |
| 248 | - return 'desktop_mode_content_buf_' . (int) $user_id; | |
| 232 | +function openstation_content_changes_buffer_key( $user_id ) { | |
| 233 | + return 'openstation_content_buf_' . (int) $user_id; | |
| 249 | 234 | } |
| 250 | 235 | |
| 251 | 236 | /** |
| 252 | 237 | * `wp_after_insert_post` handler — posts, pages, and every `show_ui` |
| @@ -260,18 +245,16 @@ | ||
| 260 | 245 | * |
| 261 | 246 | * Post types without `show_ui` are skipped: they have no list screen |
| 262 | 247 | * to refresh, and internal types (notes, …) run their own realtime. |
| 263 | 248 | * Plugins that want one tracked anyway can call |
| 264 | - * `desktop_mode_content_changes_record()` from their own hooks. | |
| 249 | + * `openstation_content_changes_record()` from their own hooks. | |
| 265 | 250 | * |
| 266 | - * @since 0.9.7 | |
| 267 | - * | |
| 268 | 251 | * @param int $post_id Post id. |
| 269 | 252 | * @param WP_Post $post Saved post. |
| 270 | 253 | * @param bool $update Whether this is an update. |
| 271 | 254 | * @param WP_Post|null $post_before Pre-save post, null on creation. |
| 272 | 255 | */ |
| 273 | -function desktop_mode_content_changes_on_after_insert_post( $post_id, $post, $update, $post_before ) { | |
| 256 | +function openstation_content_changes_on_after_insert_post( $post_id, $post, $update, $post_before ) { | |
| 274 | 257 | if ( ! $post instanceof WP_Post ) { |
| 275 | 258 | return; |
| 276 | 259 | } |
| 277 | 260 | if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) { |
| @@ -291,9 +274,9 @@ | ||
| 291 | 274 | // The first real save of a new post arrives as an "update" of the |
| 292 | 275 | // auto-draft shell `post-new.php` created — report it as created. |
| 293 | 276 | $is_created = ! $update || ( $post_before instanceof WP_Post && 'auto-draft' === $post_before->post_status ); |
| 294 | 277 | |
| 295 | - desktop_mode_content_changes_record( $post->post_type, (int) $post_id, $is_created ? 'created' : 'updated' ); | |
| 278 | + openstation_content_changes_record( $post->post_type, (int) $post_id, $is_created ? 'created' : 'updated' ); | |
| 296 | 279 | } |
| 297 | 280 | |
| 298 | 281 | /** |
| 299 | 282 | * `transition_comment_status` handler. |
| @@ -302,15 +285,13 @@ | ||
| 302 | 285 | * `trashed_comment` / `untrashed_comment` hooks record those verbs, |
| 303 | 286 | * and they fire AFTER the transition — without the skip the dedupe |
| 304 | 287 | * set would keep this handler's less-specific `updated`. |
| 305 | 288 | * |
| 306 | - * @since 0.9.7 | |
| 307 | - * | |
| 308 | 289 | * @param string $new_status New comment status. |
| 309 | 290 | * @param string $old_status Old comment status. |
| 310 | 291 | * @param WP_Comment $comment Comment object. |
| 311 | 292 | */ |
| 312 | -function desktop_mode_content_changes_on_comment_transition( $new_status, $old_status, $comment ) { | |
| 293 | +function openstation_content_changes_on_comment_transition( $new_status, $old_status, $comment ) { | |
| 313 | 294 | if ( 'trash' === $new_status || 'trash' === $old_status ) { |
| 314 | 295 | return; |
| 315 | 296 | } |
| 316 | 297 | if ( ! $comment instanceof WP_Comment ) { |
| @@ -315,9 +296,9 @@ | ||
| 315 | 296 | } |
| 316 | 297 | if ( ! $comment instanceof WP_Comment ) { |
| 317 | 298 | return; |
| 318 | 299 | } |
| 319 | - desktop_mode_content_changes_record( 'comment', (int) $comment->comment_ID, 'updated' ); | |
| 300 | + openstation_content_changes_record( 'comment', (int) $comment->comment_ID, 'updated' ); | |
| 320 | 301 | } |
| 321 | 302 | |
| 322 | 303 | /** |
| 323 | 304 | * Wires the WooCommerce order hooks. No-op unless WooCommerce is |
| @@ -327,40 +308,56 @@ | ||
| 327 | 308 | * in custom tables and none of the post hooks fire. Under legacy |
| 328 | 309 | * (posts-table) storage the post hooks fire too; the recorder's |
| 329 | 310 | * per-request dedupe collapses the double-fire. The type is always |
| 330 | 311 | * recorded as `shop_order` so one broadcast topic |
| 331 | - * (`desktop-mode.shop_order.changed`) serves both storage modes. | |
| 312 | + * (`os.shop_order.changed`) serves both storage modes. | |
| 332 | 313 | * |
| 333 | - * @since 0.9.7 | |
| 334 | - * | |
| 335 | 314 | * @return bool Whether the hooks were registered. |
| 336 | 315 | */ |
| 337 | -function desktop_mode_content_changes_register_wc_hooks() { | |
| 316 | +function openstation_content_changes_register_wc_hooks() { | |
| 338 | 317 | if ( ! class_exists( 'WooCommerce' ) || ! function_exists( 'wc_get_order' ) ) { |
| 339 | 318 | return false; |
| 340 | 319 | } |
| 341 | 320 | |
| 342 | - add_action( 'woocommerce_new_order', function ( $order_id ) { | |
| 343 | - desktop_mode_content_changes_record( 'shop_order', (int) $order_id, 'created' ); | |
| 344 | - } ); | |
| 345 | - add_action( 'woocommerce_update_order', function ( $order_id ) { | |
| 346 | - desktop_mode_content_changes_record( 'shop_order', (int) $order_id, 'updated' ); | |
| 347 | - } ); | |
| 321 | + add_action( | |
| 322 | + 'woocommerce_new_order', | |
| 323 | + function ( $order_id ) { | |
| 324 | + openstation_content_changes_record( 'shop_order', (int) $order_id, 'created' ); | |
| 325 | + } | |
| 326 | + ); | |
| 327 | + add_action( | |
| 328 | + 'woocommerce_update_order', | |
| 329 | + function ( $order_id ) { | |
| 330 | + openstation_content_changes_record( 'shop_order', (int) $order_id, 'updated' ); | |
| 331 | + } | |
| 332 | + ); | |
| 348 | 333 | // Some AJAX status-flip paths reach `woocommerce_order_status_changed` |
| 349 | 334 | // without `woocommerce_update_order`; the dedupe set absorbs the |
| 350 | 335 | // overlap when both fire. |
| 351 | - add_action( 'woocommerce_order_status_changed', function ( $order_id ) { | |
| 352 | - desktop_mode_content_changes_record( 'shop_order', (int) $order_id, 'updated' ); | |
| 353 | - } ); | |
| 354 | - add_action( 'woocommerce_trash_order', function ( $order_id ) { | |
| 355 | - desktop_mode_content_changes_record( 'shop_order', (int) $order_id, 'trashed' ); | |
| 356 | - } ); | |
| 357 | - add_action( 'woocommerce_untrash_order', function ( $order_id ) { | |
| 358 | - desktop_mode_content_changes_record( 'shop_order', (int) $order_id, 'untrashed' ); | |
| 359 | - } ); | |
| 360 | - add_action( 'woocommerce_delete_order', function ( $order_id ) { | |
| 361 | - desktop_mode_content_changes_record( 'shop_order', (int) $order_id, 'deleted' ); | |
| 362 | - } ); | |
| 336 | + add_action( | |
| 337 | + 'woocommerce_order_status_changed', | |
| 338 | + function ( $order_id ) { | |
| 339 | + openstation_content_changes_record( 'shop_order', (int) $order_id, 'updated' ); | |
| 340 | + } | |
| 341 | + ); | |
| 342 | + add_action( | |
| 343 | + 'woocommerce_trash_order', | |
| 344 | + function ( $order_id ) { | |
| 345 | + openstation_content_changes_record( 'shop_order', (int) $order_id, 'trashed' ); | |
| 346 | + } | |
| 347 | + ); | |
| 348 | + add_action( | |
| 349 | + 'woocommerce_untrash_order', | |
| 350 | + function ( $order_id ) { | |
| 351 | + openstation_content_changes_record( 'shop_order', (int) $order_id, 'untrashed' ); | |
| 352 | + } | |
| 353 | + ); | |
| 354 | + add_action( | |
| 355 | + 'woocommerce_delete_order', | |
| 356 | + function ( $order_id ) { | |
| 357 | + openstation_content_changes_record( 'shop_order', (int) $order_id, 'deleted' ); | |
| 358 | + } | |
| 359 | + ); | |
| 363 | 360 | |
| 364 | 361 | return true; |
| 365 | 362 | } |
| 366 | 363 | |
| @@ -370,31 +367,29 @@ | ||
| 370 | 367 | * Merges the in-memory changelog with the redirect-surviving buffer |
| 371 | 368 | * (consumed on read), builds one broadcast envelope per |
| 372 | 369 | * `[ type ][ action ]`, and prints one inline script that postMessages |
| 373 | 370 | * each to the parent shell. The parent's broadcast receiver fans them |
| 374 | - * out as `desktop-mode.<type>.changed` — iframe list pages soft-reload | |
| 371 | + * out as `os.<type>.changed` — iframe list pages soft-reload | |
| 375 | 372 | * and native list windows refetch. |
| 376 | 373 | * |
| 377 | 374 | * Runs at `admin_footer` priority 100, same slot as the Recycle Bin's |
| 378 | 375 | * bin-specific ts signal. |
| 379 | - * | |
| 380 | - * @since 0.9.7 | |
| 381 | 376 | */ |
| 382 | -function desktop_mode_content_changes_emit_footer() { | |
| 383 | - if ( ! function_exists( 'desktop_mode_is_chromeless_request' ) || ! desktop_mode_is_chromeless_request() ) { | |
| 377 | +function openstation_content_changes_emit_footer() { | |
| 378 | + if ( ! function_exists( 'openstation_is_chromeless_request' ) || ! openstation_is_chromeless_request() ) { | |
| 384 | 379 | return; |
| 385 | 380 | } |
| 386 | 381 | |
| 387 | - $state = &desktop_mode_content_changes_state(); | |
| 382 | + $state = &openstation_content_changes_state(); | |
| 388 | 383 | $log = $state['log']; |
| 389 | 384 | |
| 390 | 385 | $user_id = get_current_user_id(); |
| 391 | 386 | if ( $user_id > 0 ) { |
| 392 | - $key = desktop_mode_content_changes_buffer_key( $user_id ); | |
| 387 | + $key = openstation_content_changes_buffer_key( $user_id ); | |
| 393 | 388 | $buffered = get_transient( $key ); |
| 394 | 389 | if ( is_array( $buffered ) && ! empty( $buffered ) ) { |
| 395 | 390 | delete_transient( $key ); |
| 396 | - $log = desktop_mode_content_changes_merge( $buffered, $log ); | |
| 391 | + $log = openstation_content_changes_merge( $buffered, $log ); | |
| 397 | 392 | } |
| 398 | 393 | } |
| 399 | 394 | |
| 400 | 395 | // The in-memory log is consumed regardless of whether anything is |
| @@ -411,15 +406,13 @@ | ||
| 411 | 406 | foreach ( $by_action as $action => $ids ) { |
| 412 | 407 | /** |
| 413 | 408 | * Filters the broadcast topic for a content-change type. |
| 414 | 409 | * |
| 415 | - * @since 0.9.7 | |
| 416 | - * | |
| 417 | - * @param string $topic Default `desktop-mode.<type>.changed`. | |
| 410 | + * @param string $topic Default `os.<type>.changed`. | |
| 418 | 411 | * @param string $type Content type slug. |
| 419 | 412 | * @param string $action Verb for this envelope. |
| 420 | 413 | */ |
| 421 | - $topic = (string) apply_filters( 'desktop_mode_content_change_topic', 'desktop-mode.' . $type . '.changed', $type, $action ); | |
| 414 | + $topic = (string) apply_filters( 'openstation_content_change_topic', 'os.' . $type . '.changed', $type, $action ); | |
| 422 | 415 | |
| 423 | 416 | $broadcasts[] = array( |
| 424 | 417 | 'topic' => $topic, |
| 425 | 418 | 'payload' => array( |
| @@ -437,13 +430,11 @@ | ||
| 437 | 430 | * |
| 438 | 431 | * Each entry is `array( 'topic' => string, 'payload' => array )`. |
| 439 | 432 | * Return an empty array to suppress the emit. |
| 440 | 433 | * |
| 441 | - * @since 0.9.7 | |
| 442 | - * | |
| 443 | 434 | * @param array $broadcasts Broadcast envelopes. |
| 444 | 435 | */ |
| 445 | - $broadcasts = (array) apply_filters( 'desktop_mode_content_changes_broadcasts', $broadcasts ); | |
| 436 | + $broadcasts = (array) apply_filters( 'openstation_content_changes_broadcasts', $broadcasts ); | |
| 446 | 437 | if ( empty( $broadcasts ) ) { |
| 447 | 438 | return; |
| 448 | 439 | } |
| 449 | 440 | |
| @@ -452,9 +443,9 @@ | ||
| 452 | 443 | return; |
| 453 | 444 | } |
| 454 | 445 | |
| 455 | 446 | ?> |
| 456 | - <script id="desktop-mode-content-changes-signal"> | |
| 447 | + <script id="os-content-changes-signal"> | |
| 457 | 448 | ( function () { |
| 458 | 449 | if ( window.parent === window ) { |
| 459 | 450 | return; |
| 460 | 451 | } |
| @@ -462,9 +453,9 @@ | ||
| 462 | 453 | var broadcasts = <?php echo $broadcasts_json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode output. ?>; |
| 463 | 454 | for ( var i = 0; i < broadcasts.length; i++ ) { |
| 464 | 455 | try { |
| 465 | 456 | window.parent.postMessage( { |
| 466 | - type: 'desktop-mode-broadcast', | |
| 457 | + type: 'os-broadcast', | |
| 467 | 458 | topic: broadcasts[ i ].topic, |
| 468 | 459 | payload: broadcasts[ i ].payload |
| 469 | 460 | }, origin ); |
| 470 | 461 | } catch ( _err ) { /* parent gone */ } |
| @@ -476,13 +467,11 @@ | ||
| 476 | 467 | /** |
| 477 | 468 | * Fires after the chromeless footer emitted the content-change |
| 478 | 469 | * broadcast envelopes. |
| 479 | 470 | * |
| 480 | - * @since 0.9.7 | |
| 481 | - * | |
| 482 | 471 | * @param array $broadcasts Emitted broadcast envelopes. |
| 483 | 472 | */ |
| 484 | - do_action( 'desktop_mode_content_changes_emitted', $broadcasts ); | |
| 473 | + do_action( 'openstation_content_changes_emitted', $broadcasts ); | |
| 485 | 474 | } |
| 486 | 475 | |
| 487 | 476 | /** |
| 488 | 477 | * `shutdown` handler — persists the heartbeat changelog and buffers |
| @@ -489,13 +478,11 @@ | ||
| 489 | 478 | * an unflushed in-request changelog across the coming redirect. |
| 490 | 479 | * |
| 491 | 480 | * One `update_option` per mutating request; requests that recorded |
| 492 | 481 | * nothing pay a static-array read and return. |
| 493 | - * | |
| 494 | - * @since 0.9.7 | |
| 495 | 482 | */ |
| 496 | -function desktop_mode_content_changes_on_shutdown() { | |
| 497 | - $state = &desktop_mode_content_changes_state(); | |
| 483 | +function openstation_content_changes_on_shutdown() { | |
| 484 | + $state = &openstation_content_changes_state(); | |
| 498 | 485 | |
| 499 | 486 | if ( ! empty( $state['staged'] ) ) { |
| 500 | 487 | $now = (int) round( microtime( true ) * 1000 ); |
| 501 | 488 | |
| @@ -504,9 +491,9 @@ | ||
| 504 | 491 | foreach ( $state['staged'] as $row ) { |
| 505 | 492 | $grouped[ $row['type'] . '|' . $row['action'] ][] = (int) $row['id']; |
| 506 | 493 | } |
| 507 | 494 | |
| 508 | - $log = get_option( DESKTOP_MODE_CONTENT_CHANGES_LOG_OPTION, array() ); | |
| 495 | + $log = get_option( OPENSTATION_CONTENT_CHANGES_LOG_OPTION, array() ); | |
| 509 | 496 | $entries = ( is_array( $log ) && isset( $log['entries'] ) && is_array( $log['entries'] ) ) ? $log['entries'] : array(); |
| 510 | 497 | |
| 511 | 498 | foreach ( $grouped as $group_key => $ids ) { |
| 512 | 499 | list( $type, $action ) = explode( '|', $group_key, 2 ); |
| @@ -519,18 +506,23 @@ | ||
| 519 | 506 | } |
| 520 | 507 | |
| 521 | 508 | // Prune: drop entries older than the retention window, cap the |
| 522 | 509 | // tail. The option stays small no matter how chatty the site. |
| 523 | - $cutoff = $now - DESKTOP_MODE_CONTENT_CHANGES_LOG_WINDOW_MS; | |
| 524 | - $entries = array_values( array_filter( $entries, function ( $entry ) use ( $cutoff ) { | |
| 525 | - return isset( $entry['ts'] ) && (int) $entry['ts'] >= $cutoff; | |
| 526 | - } ) ); | |
| 527 | - if ( count( $entries ) > DESKTOP_MODE_CONTENT_CHANGES_LOG_MAX ) { | |
| 528 | - $entries = array_slice( $entries, -DESKTOP_MODE_CONTENT_CHANGES_LOG_MAX ); | |
| 510 | + $cutoff = $now - OPENSTATION_CONTENT_CHANGES_LOG_WINDOW_MS; | |
| 511 | + $entries = array_values( | |
| 512 | + array_filter( | |
| 513 | + $entries, | |
| 514 | + function ( $entry ) use ( $cutoff ) { | |
| 515 | + return isset( $entry['ts'] ) && (int) $entry['ts'] >= $cutoff; | |
| 516 | + } | |
| 517 | + ) | |
| 518 | + ); | |
| 519 | + if ( count( $entries ) > OPENSTATION_CONTENT_CHANGES_LOG_MAX ) { | |
| 520 | + $entries = array_slice( $entries, -OPENSTATION_CONTENT_CHANGES_LOG_MAX ); | |
| 529 | 521 | } |
| 530 | 522 | |
| 531 | 523 | update_option( |
| 532 | - DESKTOP_MODE_CONTENT_CHANGES_LOG_OPTION, | |
| 524 | + OPENSTATION_CONTENT_CHANGES_LOG_OPTION, | |
| 533 | 525 | array( |
| 534 | 526 | 'ts' => $now, |
| 535 | 527 | 'entries' => $entries, |
| 536 | 528 | ), |
| @@ -545,12 +537,12 @@ | ||
| 545 | 537 | if ( $user_id <= 0 ) { |
| 546 | 538 | return; |
| 547 | 539 | } |
| 548 | 540 | |
| 549 | - $key = desktop_mode_content_changes_buffer_key( $user_id ); | |
| 541 | + $key = openstation_content_changes_buffer_key( $user_id ); | |
| 550 | 542 | $existing = get_transient( $key ); |
| 551 | - $merged = desktop_mode_content_changes_merge( is_array( $existing ) ? $existing : array(), $state['log'] ); | |
| 552 | - set_transient( $key, $merged, DESKTOP_MODE_CONTENT_CHANGES_BUFFER_TTL ); | |
| 543 | + $merged = openstation_content_changes_merge( is_array( $existing ) ? $existing : array(), $state['log'] ); | |
| 544 | + set_transient( $key, $merged, OPENSTATION_CONTENT_CHANGES_BUFFER_TTL ); | |
| 553 | 545 | } |
| 554 | 546 | |
| 555 | 547 | /** |
| 556 | 548 | * Heartbeat handler — answers "which content changed since you last |
| @@ -555,30 +547,28 @@ | ||
| 555 | 547 | /** |
| 556 | 548 | * Heartbeat handler — answers "which content changed since you last |
| 557 | 549 | * heard from me?". |
| 558 | 550 | * |
| 559 | - * Opt-in via the client-sent `desktop_mode_content_changes_seen_ts` | |
| 551 | + * Opt-in via the client-sent `openstation_content_changes_seen_ts` | |
| 560 | 552 | * key; requests without it early-return so non-desktop tabs pay zero |
| 561 | 553 | * per tick. The response carries the server high-water mark plus the |
| 562 | 554 | * entries newer than the client's seen ts; the shell re-broadcasts |
| 563 | - * each as `desktop-mode.<type>.changed`. | |
| 555 | + * each as `os.<type>.changed`. | |
| 564 | 556 | * |
| 565 | - * @since 0.9.7 | |
| 566 | - * | |
| 567 | 557 | * @param array $response Heartbeat response. |
| 568 | 558 | * @param array $data Client-sent payload. |
| 569 | 559 | * @return array |
| 570 | 560 | */ |
| 571 | -function desktop_mode_content_changes_heartbeat_received( $response, $data ) { | |
| 561 | +function openstation_content_changes_heartbeat_received( $response, $data ) { | |
| 572 | 562 | if ( ! is_array( $response ) ) { |
| 573 | 563 | $response = array(); |
| 574 | 564 | } |
| 575 | - if ( ! isset( $data['desktop_mode_content_changes_seen_ts'] ) ) { | |
| 565 | + if ( ! isset( $data['openstation_content_changes_seen_ts'] ) ) { | |
| 576 | 566 | return $response; |
| 577 | 567 | } |
| 578 | 568 | |
| 579 | - $seen = (int) $data['desktop_mode_content_changes_seen_ts']; | |
| 580 | - $log = get_option( DESKTOP_MODE_CONTENT_CHANGES_LOG_OPTION, array() ); | |
| 569 | + $seen = (int) $data['openstation_content_changes_seen_ts']; | |
| 570 | + $log = get_option( OPENSTATION_CONTENT_CHANGES_LOG_OPTION, array() ); | |
| 581 | 571 | |
| 582 | 572 | $ts = ( is_array( $log ) && isset( $log['ts'] ) ) ? (int) $log['ts'] : 0; |
| 583 | 573 | $entries = ( is_array( $log ) && isset( $log['entries'] ) && is_array( $log['entries'] ) ) ? $log['entries'] : array(); |
| 584 | 574 | |
| @@ -588,9 +578,9 @@ | ||
| 588 | 578 | $fresh[] = $entry; |
| 589 | 579 | } |
| 590 | 580 | } |
| 591 | 581 | |
| 592 | - $response['desktop_mode_content_changes'] = array( | |
| 582 | + $response['openstation_content_changes'] = array( | |
| 593 | 583 | 'ts' => $ts, |
| 594 | 584 | 'entries' => $fresh, |
| 595 | 585 | ); |
| 596 | 586 | |
| @@ -597,29 +587,133 @@ | ||
| 597 | 587 | return $response; |
| 598 | 588 | } |
| 599 | 589 | |
| 600 | 590 | /** |
| 591 | + * Converts a plugin file path to a stable positive integer suitable | |
| 592 | + * for use as the `$id` parameter of | |
| 593 | + * `openstation_content_changes_record()`. | |
| 594 | + * | |
| 595 | + * The record function requires a positive integer for its ID slot (used | |
| 596 | + * for per-request deduplication keyed as `type:id`). Plugin files are | |
| 597 | + * strings (`akismet/akismet.php`), so we derive a deterministic integer | |
| 598 | + * via `crc32`. Using the actual hash rather than a fixed value (e.g. 1) | |
| 599 | + * prevents every distinct plugin in a bulk operation from collapsing to | |
| 600 | + * the same dedup key. | |
| 601 | + * | |
| 602 | + * @param string $plugin_file Plugin file path (relative to plugins dir). | |
| 603 | + * @return int Positive integer ID. | |
| 604 | + */ | |
| 605 | +function openstation_content_changes_plugin_id( $plugin_file ) { | |
| 606 | + $hash = abs( crc32( (string) $plugin_file ) ); | |
| 607 | + return max( 1, $hash ); | |
| 608 | +} | |
| 609 | + | |
| 610 | +/** | |
| 611 | + * Wires plugin lifecycle hooks so installs, activations, deactivations, | |
| 612 | + * and deletions are recorded into the realtime changelog. | |
| 613 | + * | |
| 614 | + * Every open window listing plugins (native Plugins window Installed tab, | |
| 615 | + * classic `plugins.php`) will then refresh via the | |
| 616 | + * `os.plugin.changed` broadcast — the same mechanism | |
| 617 | + * posts/pages use for `os.post.changed`. | |
| 618 | + */ | |
| 619 | +function openstation_content_changes_register_plugin_hooks() { | |
| 620 | + add_action( | |
| 621 | + 'activated_plugin', | |
| 622 | + function ( $plugin_file ) { | |
| 623 | + openstation_content_changes_record( | |
| 624 | + 'plugin', | |
| 625 | + openstation_content_changes_plugin_id( $plugin_file ), | |
| 626 | + 'activated' | |
| 627 | + ); | |
| 628 | + } | |
| 629 | + ); | |
| 630 | + | |
| 631 | + add_action( | |
| 632 | + 'deactivated_plugin', | |
| 633 | + function ( $plugin_file ) { | |
| 634 | + openstation_content_changes_record( | |
| 635 | + 'plugin', | |
| 636 | + openstation_content_changes_plugin_id( $plugin_file ), | |
| 637 | + 'deactivated' | |
| 638 | + ); | |
| 639 | + } | |
| 640 | + ); | |
| 641 | + | |
| 642 | + add_action( | |
| 643 | + 'deleted_plugin', | |
| 644 | + function ( $plugin_file, $deleted ) { | |
| 645 | + if ( $deleted ) { | |
| 646 | + openstation_content_changes_record( | |
| 647 | + 'plugin', | |
| 648 | + openstation_content_changes_plugin_id( $plugin_file ), | |
| 649 | + 'deleted' | |
| 650 | + ); | |
| 651 | + } | |
| 652 | + }, | |
| 653 | + 10, | |
| 654 | + 2 | |
| 655 | + ); | |
| 656 | + | |
| 657 | + // `upgrader_process_complete` covers installs from wp-admin/plugin-install.php | |
| 658 | + // (AJAX path, no page navigation) and bulk installs from update.php. | |
| 659 | + add_action( | |
| 660 | + 'upgrader_process_complete', | |
| 661 | + function ( $upgrader, $options ) { | |
| 662 | + if ( | |
| 663 | + ! isset( $options['type'], $options['action'] ) || | |
| 664 | + 'plugin' !== $options['type'] || | |
| 665 | + 'install' !== $options['action'] | |
| 666 | + ) { | |
| 667 | + return; | |
| 668 | + } | |
| 669 | + $plugins = ! empty( $options['plugins'] ) ? (array) $options['plugins'] : array(); | |
| 670 | + if ( empty( $plugins ) && is_callable( array( $upgrader, 'plugin_info' ) ) ) { | |
| 671 | + $info = $upgrader->plugin_info(); | |
| 672 | + if ( $info ) { | |
| 673 | + $plugins = array( $info ); | |
| 674 | + } | |
| 675 | + } | |
| 676 | + foreach ( $plugins as $plugin_file ) { | |
| 677 | + openstation_content_changes_record( | |
| 678 | + 'plugin', | |
| 679 | + openstation_content_changes_plugin_id( (string) $plugin_file ), | |
| 680 | + 'installed' | |
| 681 | + ); | |
| 682 | + } | |
| 683 | + }, | |
| 684 | + 10, | |
| 685 | + 2 | |
| 686 | + ); | |
| 687 | +} | |
| 688 | + | |
| 689 | +/** | |
| 601 | 690 | * Wires every content-change hook. |
| 602 | 691 | * |
| 603 | 692 | * One bootstrap so the wiring is auditable — |
| 604 | - * `grep desktop_mode_content_changes_record` finds every emitter. | |
| 605 | - * | |
| 606 | - * @since 0.9.7 | |
| 693 | + * `grep openstation_content_changes_record` finds every emitter. | |
| 607 | 694 | */ |
| 608 | -function desktop_mode_content_changes_register_hooks() { | |
| 609 | - add_action( 'wp_after_insert_post', 'desktop_mode_content_changes_on_after_insert_post', 10, 4 ); | |
| 695 | +function openstation_content_changes_register_hooks() { | |
| 696 | + add_action( 'wp_after_insert_post', 'openstation_content_changes_on_after_insert_post', 10, 4 ); | |
| 610 | 697 | |
| 611 | - add_action( 'wp_insert_comment', function ( $comment_id ) { | |
| 612 | - desktop_mode_content_changes_record( 'comment', (int) $comment_id, 'created' ); | |
| 613 | - } ); | |
| 614 | - add_action( 'edit_comment', function ( $comment_id ) { | |
| 615 | - desktop_mode_content_changes_record( 'comment', (int) $comment_id, 'updated' ); | |
| 616 | - } ); | |
| 617 | - add_action( 'transition_comment_status', 'desktop_mode_content_changes_on_comment_transition', 10, 3 ); | |
| 698 | + add_action( | |
| 699 | + 'wp_insert_comment', | |
| 700 | + function ( $comment_id ) { | |
| 701 | + openstation_content_changes_record( 'comment', (int) $comment_id, 'created' ); | |
| 702 | + } | |
| 703 | + ); | |
| 704 | + add_action( | |
| 705 | + 'edit_comment', | |
| 706 | + function ( $comment_id ) { | |
| 707 | + openstation_content_changes_record( 'comment', (int) $comment_id, 'updated' ); | |
| 708 | + } | |
| 709 | + ); | |
| 710 | + add_action( 'transition_comment_status', 'openstation_content_changes_on_comment_transition', 10, 3 ); | |
| 618 | 711 | |
| 619 | - desktop_mode_content_changes_register_wc_hooks(); | |
| 712 | + openstation_content_changes_register_wc_hooks(); | |
| 713 | + openstation_content_changes_register_plugin_hooks(); | |
| 620 | 714 | |
| 621 | - add_action( 'admin_footer', 'desktop_mode_content_changes_emit_footer', 100 ); | |
| 622 | - add_action( 'shutdown', 'desktop_mode_content_changes_on_shutdown' ); | |
| 623 | - add_filter( 'heartbeat_received', 'desktop_mode_content_changes_heartbeat_received', 10, 2 ); | |
| 715 | + add_action( 'admin_footer', 'openstation_content_changes_emit_footer', 100 ); | |
| 716 | + add_action( 'shutdown', 'openstation_content_changes_on_shutdown' ); | |
| 717 | + add_filter( 'heartbeat_received', 'openstation_content_changes_heartbeat_received', 10, 2 ); | |
| 624 | 718 | } |
| 625 | -add_action( 'init', 'desktop_mode_content_changes_register_hooks', 5 ); | |
| 719 | +add_action( 'init', 'openstation_content_changes_register_hooks', 5 ); | |