| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — Files Heartbeat sync (PHP). | |
| 3 | + * OpenStation — Files Heartbeat sync (PHP). | |
| 4 | 4 | * |
| 5 | 5 | * Piggybacks on the existing WordPress Heartbeat tick — the same |
| 6 | 6 | * channel `presence.php` uses — so connected clients see folder |
| 7 | 7 | * sharing changes and other users' placement edits inside one |
| @@ -6,13 +6,13 @@ | ||
| 6 | 6 | * channel `presence.php` uses — so connected clients see folder |
| 7 | 7 | * sharing changes and other users' placement edits inside one |
| 8 | 8 | * cross-feature poll instead of N parallel ones. |
| 9 | 9 | * |
| 10 | - * Wire format. Client sends `desktop_mode_files_subscribe` keyed | |
| 10 | + * Wire format. Client sends `openstation_files_subscribe` keyed | |
| 11 | 11 | * to three version markers: |
| 12 | 12 | * |
| 13 | 13 | * { |
| 14 | - * desktop_mode_files_subscribe: { | |
| 14 | + * openstation_files_subscribe: { | |
| 15 | 15 | * folderVersions: { '<folderId>': lastSeenUpdatedAtMs, ... }, |
| 16 | 16 | * placementsVersion: lastSeenUpdatedAtMs, |
| 17 | 17 | * sharesVersion: lastSeenInvitedAtMs |
| 18 | 18 | * } |
| @@ -19,9 +19,9 @@ | ||
| 19 | 19 | * } |
| 20 | 20 | * |
| 21 | 21 | * Server responds with deltas + tombstones: |
| 22 | 22 | * |
| 23 | - * desktop_mode_files: { | |
| 23 | + * openstation_files: { | |
| 24 | 24 | * placements: [ <RestPlacementShape> ], // upserts |
| 25 | 25 | * folders: [ <RestFolderShape> ], // upserts (incl. share-mode flips) |
| 26 | 26 | * removed: { |
| 27 | 27 | * placements: [ ids ], |
| @@ -33,38 +33,35 @@ | ||
| 33 | 33 | * serverTimeMs: int, |
| 34 | 34 | * truncated: bool |
| 35 | 35 | * } |
| 36 | 36 | * |
| 37 | - * Truncation kicks in when more than `desktop_mode_files_heartbeat_max_rows` | |
| 37 | + * Truncation kicks in when more than `openstation_files_heartbeat_max_rows` | |
| 38 | 38 | * (default 200) rows match — clients fall back to a full REST |
| 39 | 39 | * resync. The default cap is per-payload, not per-folder, so a |
| 40 | 40 | * massive shared folder doesn't starve other folders' deltas. |
| 41 | 41 | * |
| 42 | - * @package WPDesktopMode | |
| 43 | - * @since 0.9.0 | |
| 42 | + * @package OpenStation | |
| 44 | 43 | */ |
| 45 | 44 | |
| 46 | 45 | defined( 'ABSPATH' ) || exit; |
| 47 | 46 | |
| 48 | 47 | /** |
| 49 | - * @since 0.9.0 | |
| 50 | - * | |
| 51 | 48 | * @param array $response Pre-filtered response. |
| 52 | 49 | * @param array $data Client-sent payload. |
| 53 | 50 | * @return array |
| 54 | 51 | */ |
| 55 | -function desktop_mode_files_heartbeat_received( $response, $data ) { | |
| 52 | +function openstation_files_heartbeat_received( $response, $data ) { | |
| 56 | 53 | if ( ! is_array( $response ) ) { |
| 57 | 54 | $response = array(); |
| 58 | 55 | } |
| 59 | - if ( empty( $data['desktop_mode_files_subscribe'] ) || ! is_array( $data['desktop_mode_files_subscribe'] ) ) { | |
| 56 | + if ( empty( $data['openstation_files_subscribe'] ) || ! is_array( $data['openstation_files_subscribe'] ) ) { | |
| 60 | 57 | return $response; |
| 61 | 58 | } |
| 62 | - if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) { | |
| 59 | + if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) { | |
| 63 | 60 | return $response; |
| 64 | 61 | } |
| 65 | 62 | |
| 66 | - $sub = $data['desktop_mode_files_subscribe']; | |
| 63 | + $sub = $data['openstation_files_subscribe']; | |
| 67 | 64 | $folder_v = isset( $sub['folderVersions'] ) && is_array( $sub['folderVersions'] ) |
| 68 | 65 | ? $sub['folderVersions'] |
| 69 | 66 | : array(); |
| 70 | 67 | $plc_v = isset( $sub['placementsVersion'] ) ? (int) $sub['placementsVersion'] : 0; |
| @@ -79,15 +76,13 @@ | ||
| 79 | 76 | * Filter the per-payload row cap. Lower this on slow links |
| 80 | 77 | * to force REST fallback sooner; raise it for fast-LAN |
| 81 | 78 | * intranets where a fatter Heartbeat is fine. |
| 82 | 79 | * |
| 83 | - * @since 0.9.0 | |
| 84 | - * | |
| 85 | 80 | * @param int $cap Default 200. |
| 86 | 81 | */ |
| 87 | - $cap = max( 1, (int) apply_filters( 'desktop_mode_files_heartbeat_max_rows', 200 ) ); | |
| 82 | + $cap = max( 1, (int) apply_filters( 'openstation_files_heartbeat_max_rows', 200 ) ); | |
| 88 | 83 | |
| 89 | - $response['desktop_mode_files'] = desktop_mode_files_compute_heartbeat_delta( | |
| 84 | + $response['openstation_files'] = openstation_files_compute_heartbeat_delta( | |
| 90 | 85 | $user_id, |
| 91 | 86 | $folder_v, |
| 92 | 87 | $plc_v, |
| 93 | 88 | $cap, |
| @@ -94,15 +89,13 @@ | ||
| 94 | 89 | $shr_v |
| 95 | 90 | ); |
| 96 | 91 | return $response; |
| 97 | 92 | } |
| 98 | -add_filter( 'heartbeat_received', 'desktop_mode_files_heartbeat_received', 5, 2 ); | |
| 93 | +add_filter( 'heartbeat_received', 'openstation_files_heartbeat_received', 5, 2 ); | |
| 99 | 94 | |
| 100 | 95 | /** |
| 101 | 96 | * Compute the delta payload for a viewer. |
| 102 | 97 | * |
| 103 | - * @since 0.9.0 | |
| 104 | - * | |
| 105 | 98 | * @param int $user_id Viewer. |
| 106 | 99 | * @param array $folder_versions `{ folderId => lastSeenUpdatedAtMs }`. |
| 107 | 100 | * @param int $placements_version Last-seen `updated_at_ms` for placements. |
| 108 | 101 | * @param int $cap Row cap. |
| @@ -112,19 +105,19 @@ | ||
| 112 | 105 | * to invites the client hasn't seen |
| 113 | 106 | * yet. Defaults to `0` (deliver all). |
| 114 | 107 | * @return array |
| 115 | 108 | */ |
| 116 | -function desktop_mode_files_compute_heartbeat_delta( $user_id, $folder_versions, $placements_version, $cap, $shares_version = 0 ) { | |
| 109 | +function openstation_files_compute_heartbeat_delta( $user_id, $folder_versions, $placements_version, $cap, $shares_version = 0 ) { | |
| 117 | 110 | global $wpdb; |
| 118 | 111 | |
| 119 | - $tables = desktop_mode_files_table_names(); | |
| 112 | + $tables = openstation_files_table_names(); | |
| 120 | 113 | $truncated = false; |
| 121 | 114 | |
| 122 | 115 | // 1) Visible folders the viewer should know about. We send |
| 123 | - // the FULL row when its `updated_at_ms` exceeds whatever | |
| 124 | - // the client last saw (or the client doesn't know about | |
| 125 | - // it at all). | |
| 126 | - $visible = desktop_mode_files_get_visible_folders( $user_id ); | |
| 116 | + // the FULL row when its `updated_at_ms` exceeds whatever | |
| 117 | + // the client last saw (or the client doesn't know about | |
| 118 | + // it at all). | |
| 119 | + $visible = openstation_files_get_visible_folders( $user_id ); | |
| 127 | 120 | $folder_upserts = array(); |
| 128 | 121 | foreach ( $visible as $row ) { |
| 129 | 122 | $id = (int) $row['id']; |
| 130 | 123 | $client_ts = isset( $folder_versions[ (string) $id ] ) |
| @@ -130,9 +123,9 @@ | ||
| 130 | 123 | $client_ts = isset( $folder_versions[ (string) $id ] ) |
| 131 | 124 | ? (int) $folder_versions[ (string) $id ] |
| 132 | 125 | : 0; |
| 133 | 126 | if ( (int) $row['updated_at_ms'] > $client_ts ) { |
| 134 | - $folder_upserts[] = desktop_mode_files_shape_folder( $row ); | |
| 127 | + $folder_upserts[] = openstation_files_shape_folder( $row ); | |
| 135 | 128 | if ( count( $folder_upserts ) >= $cap ) { |
| 136 | 129 | $truncated = true; |
| 137 | 130 | break; |
| 138 | 131 | } |
| @@ -139,14 +132,17 @@ | ||
| 139 | 132 | } |
| 140 | 133 | } |
| 141 | 134 | |
| 142 | 135 | // 2) Placement upserts the viewer can see. We pull anything |
| 143 | - // written since `placements_version` whose owner is the | |
| 144 | - // viewer (their own desktop) OR which lives in a folder | |
| 145 | - // the viewer can see (shared content). | |
| 146 | - $visible_folder_ids = array_map( static function ( $f ) { | |
| 147 | - return (int) $f['id']; | |
| 148 | - }, $visible ); | |
| 136 | + // written since `placements_version` whose owner is the | |
| 137 | + // viewer (their own desktop) OR which lives in a folder | |
| 138 | + // the viewer can see (shared content). | |
| 139 | + $visible_folder_ids = array_map( | |
| 140 | + static function ( $f ) { | |
| 141 | + return (int) $f['id']; | |
| 142 | + }, | |
| 143 | + $visible | |
| 144 | + ); | |
| 149 | 145 | // Always include the desktop root (parent_id=0) for the viewer. |
| 150 | 146 | $placement_upserts = array(); |
| 151 | 147 | if ( ! $truncated ) { |
| 152 | 148 | // Owner-or-visible-folder filter, expressed as a SINGLE |
| @@ -200,16 +196,16 @@ | ||
| 200 | 196 | ARRAY_A |
| 201 | 197 | ); |
| 202 | 198 | } |
| 203 | 199 | foreach ( (array) $rows as $row ) { |
| 204 | - $row = desktop_mode_files_normalize_placement_row( $row ); | |
| 200 | + $row = openstation_files_normalize_placement_row( $row ); | |
| 205 | 201 | // Per-placement read gate: shared folder shouldn't |
| 206 | 202 | // surface a row the viewer's `can_read()` rejects. |
| 207 | - $file = desktop_mode_resolve_file( $row['file_type'], $row['file_ref'] ); | |
| 203 | + $file = openstation_resolve_file( $row['file_type'], $row['file_ref'] ); | |
| 208 | 204 | if ( $file && ! $file->can_read( $user_id ) ) { |
| 209 | 205 | continue; |
| 210 | 206 | } |
| 211 | - $placement_upserts[] = desktop_mode_files_shape_placement( $row ); | |
| 207 | + $placement_upserts[] = openstation_files_shape_placement( $row ); | |
| 212 | 208 | } |
| 213 | 209 | if ( count( $placement_upserts ) >= $cap ) { |
| 214 | 210 | $truncated = true; |
| 215 | 211 | } |
| @@ -215,9 +211,9 @@ | ||
| 215 | 211 | } |
| 216 | 212 | } |
| 217 | 213 | |
| 218 | 214 | // 3) Tombstones since the last placements_version — gives the |
| 219 | - // client the "this row is gone" signal. | |
| 215 | + // client the "this row is gone" signal. | |
| 220 | 216 | $tomb_rows = $wpdb->get_results( |
| 221 | 217 | $wpdb->prepare( |
| 222 | 218 | "SELECT kind, ref_id FROM {$tables['tombstones']} WHERE removed_at_ms > %d ORDER BY removed_at_ms ASC LIMIT %d", |
| 223 | 219 | $placements_version, |
| @@ -224,9 +220,12 @@ | ||
| 224 | 220 | $cap |
| 225 | 221 | ), |
| 226 | 222 | ARRAY_A |
| 227 | 223 | ); |
| 228 | - $removed = array( 'placements' => array(), 'folders' => array() ); | |
| 224 | + $removed = array( | |
| 225 | + 'placements' => array(), | |
| 226 | + 'folders' => array(), | |
| 227 | + ); | |
| 229 | 228 | foreach ( (array) $tomb_rows as $row ) { |
| 230 | 229 | if ( 'folder' === $row['kind'] ) { |
| 231 | 230 | $removed['folders'][] = (int) $row['ref_id']; |
| 232 | 231 | } else { |
| @@ -234,14 +233,14 @@ | ||
| 234 | 233 | } |
| 235 | 234 | } |
| 236 | 235 | |
| 237 | 236 | // 4) Soft-trash events. Tombstones only fire on hard delete, so |
| 238 | - // a trashed placement / folder would otherwise stay in the | |
| 239 | - // client store between F5s. Surface every row whose | |
| 240 | - // `trashed_at_ms` is fresher than the client's high-water | |
| 241 | - // mark as a `removed.*` entry. Restoring (clearing | |
| 242 | - // `trashed_at_ms`) bumps `updated_at_ms` and the row will | |
| 243 | - // flow back through `placements` / `folders` upserts above. | |
| 237 | + // a trashed placement / folder would otherwise stay in the | |
| 238 | + // client store between F5s. Surface every row whose | |
| 239 | + // `trashed_at_ms` is fresher than the client's high-water | |
| 240 | + // mark as a `removed.*` entry. Restoring (clearing | |
| 241 | + // `trashed_at_ms`) bumps `updated_at_ms` and the row will | |
| 242 | + // flow back through `placements` / `folders` upserts above. | |
| 244 | 243 | $trashed_placements = $wpdb->get_col( |
| 245 | 244 | $wpdb->prepare( |
| 246 | 245 | "SELECT id FROM {$tables['placements']} |
| 247 | 246 | WHERE trashed_at_ms IS NOT NULL |
| @@ -270,26 +269,26 @@ | ||
| 270 | 269 | $removed['folders'][] = (int) $id; |
| 271 | 270 | } |
| 272 | 271 | |
| 273 | 272 | // 5) Pending share invites for this viewer (across every folder |
| 274 | - // they're invited to). Owner-side share-status changes flow | |
| 275 | - // through the folder upserts above; this channel is for the | |
| 276 | - // recipient's "you've been invited" placeholder UI. | |
| 273 | + // they're invited to). Owner-side share-status changes flow | |
| 274 | + // through the folder upserts above; this channel is for the | |
| 275 | + // recipient's "you've been invited" placeholder UI. | |
| 277 | 276 | $shares = array(); |
| 278 | - $sharing_enabled = function_exists( 'desktop_mode_files_sharing_enabled_for' ) | |
| 279 | - ? desktop_mode_files_sharing_enabled_for( $user_id ) | |
| 277 | + $sharing_enabled = function_exists( 'openstation_files_sharing_enabled_for' ) | |
| 278 | + ? openstation_files_sharing_enabled_for( $user_id ) | |
| 280 | 279 | : true; |
| 281 | - if ( $sharing_enabled && function_exists( 'desktop_mode_files_get_pending_shares_for_user' ) ) { | |
| 282 | - $pending = desktop_mode_files_get_pending_shares_for_user( $user_id, $shares_version ); | |
| 280 | + if ( $sharing_enabled && function_exists( 'openstation_files_get_pending_shares_for_user' ) ) { | |
| 281 | + $pending = openstation_files_get_pending_shares_for_user( $user_id, $shares_version ); | |
| 283 | 282 | foreach ( $pending as $row ) { |
| 284 | - $shape = desktop_mode_files_shape_share( $row ); | |
| 285 | - $folder = desktop_mode_files_get_folder( $row['folder_id'] ); | |
| 283 | + $shape = openstation_files_shape_share( $row ); | |
| 284 | + $folder = openstation_files_get_folder( $row['folder_id'] ); | |
| 286 | 285 | if ( $folder ) { |
| 287 | - $shape['folderName'] = (string) $folder['name']; | |
| 288 | - $shape['ownerId'] = (int) $folder['owner_id']; | |
| 289 | - $owner_user = get_userdata( (int) $folder['owner_id'] ); | |
| 290 | - $shape['ownerName'] = $owner_user ? $owner_user->display_name : ''; | |
| 291 | - $shape['ownerAvatar'] = $owner_user ? get_avatar_url( $owner_user->ID, array( 'size' => 48 ) ) : ''; | |
| 286 | + $shape['folderName'] = (string) $folder['name']; | |
| 287 | + $shape['ownerId'] = (int) $folder['owner_id']; | |
| 288 | + $owner_user = get_userdata( (int) $folder['owner_id'] ); | |
| 289 | + $shape['ownerName'] = $owner_user ? $owner_user->display_name : ''; | |
| 290 | + $shape['ownerAvatar'] = $owner_user ? get_avatar_url( $owner_user->ID, array( 'size' => 48 ) ) : ''; | |
| 292 | 291 | } |
| 293 | 292 | $shares[] = $shape; |
| 294 | 293 | if ( count( $shares ) >= $cap ) { |
| 295 | 294 | $truncated = true; |
| @@ -296,8 +295,22 @@ | ||
| 296 | 295 | break; |
| 297 | 296 | } |
| 298 | 297 | } |
| 299 | 298 | } |
| 299 | + // Pending FILE-share invites ride the same channel. Shapes carry | |
| 300 | + // `targetType: 'file'` + `fileId` / `fileName` so the client | |
| 301 | + // invite banner can branch (folder shapes have no targetType and | |
| 302 | + // default to folder handling). | |
| 303 | + if ( $sharing_enabled && ! $truncated && function_exists( 'openstation_files_get_pending_file_shares_for_user' ) ) { | |
| 304 | + $pending_files = openstation_files_get_pending_file_shares_for_user( $user_id, $shares_version ); | |
| 305 | + foreach ( $pending_files as $row ) { | |
| 306 | + $shares[] = openstation_files_shape_file_share( $row ); | |
| 307 | + if ( count( $shares ) >= $cap ) { | |
| 308 | + $truncated = true; | |
| 309 | + break; | |
| 310 | + } | |
| 311 | + } | |
| 312 | + } | |
| 300 | 313 | |
| 301 | 314 | // Safety net: a row that is currently being delivered as an |
| 302 | 315 | // upsert (alive) must NOT also appear in `removed.*`. Otherwise |
| 303 | 316 | // the client applies upserts first, then removals, and the |
| @@ -309,17 +322,19 @@ | ||
| 309 | 322 | // restored but any tombstones written in error during the trash |
| 310 | 323 | // path stay in the table). Cleaning them up server-side prevents |
| 311 | 324 | // the same client-side glitch on every subsequent tick. |
| 312 | 325 | $upsert_placement_ids = array_map( |
| 313 | - static function ( $p ) { return (int) $p['id']; }, | |
| 326 | + static function ( $p ) { | |
| 327 | + return (int) $p['id']; }, | |
| 314 | 328 | $placement_upserts |
| 315 | 329 | ); |
| 316 | - $upsert_folder_ids = array_map( | |
| 317 | - static function ( $f ) { return (int) $f['id']; }, | |
| 330 | + $upsert_folder_ids = array_map( | |
| 331 | + static function ( $f ) { | |
| 332 | + return (int) $f['id']; }, | |
| 318 | 333 | $folder_upserts |
| 319 | 334 | ); |
| 320 | 335 | if ( ! empty( $upsert_placement_ids ) ) { |
| 321 | - $alive_placements = array_flip( $upsert_placement_ids ); | |
| 336 | + $alive_placements = array_flip( $upsert_placement_ids ); | |
| 322 | 337 | $removed['placements'] = array_values( |
| 323 | 338 | array_filter( |
| 324 | 339 | $removed['placements'], |
| 325 | 340 | static function ( $id ) use ( $alive_placements ) { |
| @@ -329,12 +344,12 @@ | ||
| 329 | 344 | ); |
| 330 | 345 | // Cleanup: drop any tombstones referring to placement ids |
| 331 | 346 | // that are demonstrably alive in this tick. Bounded by the |
| 332 | 347 | // upsert set so the work is per-tick, not table-wide. |
| 333 | - desktop_mode_files_purge_stale_tombstones( 'placement', $upsert_placement_ids ); | |
| 348 | + openstation_files_purge_stale_tombstones( 'placement', $upsert_placement_ids ); | |
| 334 | 349 | } |
| 335 | 350 | if ( ! empty( $upsert_folder_ids ) ) { |
| 336 | - $alive_folders = array_flip( $upsert_folder_ids ); | |
| 351 | + $alive_folders = array_flip( $upsert_folder_ids ); | |
| 337 | 352 | $removed['folders'] = array_values( |
| 338 | 353 | array_filter( |
| 339 | 354 | $removed['folders'], |
| 340 | 355 | static function ( $id ) use ( $alive_folders ) { |
| @@ -341,9 +356,9 @@ | ||
| 341 | 356 | return ! isset( $alive_folders[ (int) $id ] ); |
| 342 | 357 | } |
| 343 | 358 | ) |
| 344 | 359 | ); |
| 345 | - desktop_mode_files_purge_stale_tombstones( 'folder', $upsert_folder_ids ); | |
| 360 | + openstation_files_purge_stale_tombstones( 'folder', $upsert_folder_ids ); | |
| 346 | 361 | } |
| 347 | 362 | |
| 348 | 363 | return array( |
| 349 | 364 | 'placements' => $placement_upserts, |
| @@ -351,9 +366,9 @@ | ||
| 351 | 366 | 'removed' => $removed, |
| 352 | 367 | 'shares' => array( |
| 353 | 368 | 'pending' => $shares, |
| 354 | 369 | ), |
| 355 | - 'serverTimeMs' => desktop_mode_files_now_ms(), | |
| 370 | + 'serverTimeMs' => openstation_files_now_ms(), | |
| 356 | 371 | 'truncated' => $truncated, |
| 357 | 372 | ); |
| 358 | 373 | } |
| 359 | 374 | |
| @@ -363,19 +378,17 @@ | ||
| 363 | 378 | * `trashed_at_ms`). One-shot cleanup of stale rows written by |
| 364 | 379 | * earlier buggy code paths — once removed, the heartbeat no longer |
| 365 | 380 | * surfaces them every tick. |
| 366 | 381 | * |
| 367 | - * @since 0.8.5 | |
| 368 | - * | |
| 369 | 382 | * @param string $kind 'placement' | 'folder'. |
| 370 | 383 | * @param int[] $ids Ids known to be alive in the current tick. |
| 371 | 384 | */ |
| 372 | -function desktop_mode_files_purge_stale_tombstones( $kind, $ids ) { | |
| 385 | +function openstation_files_purge_stale_tombstones( $kind, $ids ) { | |
| 373 | 386 | if ( empty( $ids ) ) { |
| 374 | 387 | return; |
| 375 | 388 | } |
| 376 | 389 | global $wpdb; |
| 377 | - $tables = desktop_mode_files_table_names(); | |
| 390 | + $tables = openstation_files_table_names(); | |
| 378 | 391 | $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); |
| 379 | 392 | // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 380 | 393 | $wpdb->query( |
| 381 | 394 | $wpdb->prepare( |