PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/desktop-files/store.php +153 -159 0.9.81.1.10 View file →
@@ -1,10 +1,10 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Files placement store.
3 + * OpenStation — Files placement store.
4 4 *
5 5 * CRUD primitives for the `_desktop_mode_file_placements` table.
6 - * Every read goes through `desktop_mode_files_query_args` so
6 + * Every read goes through `openstation_files_query_args` so
7 7 * plugins can scope what's visible (mirror of the recycle-bin's
8 8 * filter pattern). Every write fires before / after actions so
9 9 * other plugins can react and so Phase 6's Heartbeat sync has a
10 10 * single subscription point.
@@ -10,17 +10,17 @@
10 10 * single subscription point.
11 11 *
12 12 * Capability gate is per-call: callers pass the `$user_id` they
13 13 * intend to act for; the function consults
14 - * `desktop_mode_files_can_place` (filter) and the file's
15 - * `Desktop_Mode_File::can_read()` before writing.
14 + * `openstation_files_can_place` (filter) and the file's
15 + * `OpenStation_File::can_read()` before writing.
16 16 *
17 17 * Tombstones are written only for permanent removals (hard
18 18 * deletes); soft-trash and moves are surfaced to clients via
19 19 * `updated_at_ms` / `trashed_at_ms` in the Heartbeat delta — see
20 - * desktop_mode_files_write_tombstone() for the invariant.
20 + * openstation_files_write_tombstone() for the invariant.
21 21 *
22 - * @package WPDesktopMode
22 + * @package OpenStation
23 23 */
24 24
25 25 defined( 'ABSPATH' ) || exit;
26 26
@@ -34,9 +34,9 @@
34 34 * @param string $ref Entity reference.
35 35 * @param array $args Optional. `x`, `y`, `sort_order`, `meta`.
36 36 * @return int|WP_Error Placement id on success, `WP_Error` otherwise.
37 37 */
38 -function desktop_mode_files_place( $user_id, $parent_id, $type, $ref, $args = array() ) {
38 +function openstation_files_place( $user_id, $parent_id, $type, $ref, $args = array() ) {
39 39 global $wpdb;
40 40
41 41 $user_id = (int) $user_id;
42 42 $parent_id = (int) $parent_id;
@@ -43,13 +43,13 @@
43 43 $type = (string) $type;
44 44 $ref = (string) $ref;
45 45
46 46 if ( $user_id <= 0 ) {
47 - return new WP_Error( 'desktop_mode_files_invalid_user', __( 'A user id is required.', 'desktop-mode' ), array( 'status' => 400 ) );
47 + return new WP_Error( 'openstation_files_invalid_user', __( 'A user id is required.', 'desktop-mode' ), array( 'status' => 400 ) );
48 48 }
49 - $entry = desktop_mode_get_file_type( $type );
49 + $entry = openstation_get_file_type( $type );
50 50 if ( ! $entry ) {
51 - return new WP_Error( 'desktop_mode_files_unknown_type', __( 'Unknown file type.', 'desktop-mode' ), array( 'status' => 400 ) );
51 + return new WP_Error( 'openstation_files_unknown_type', __( 'Unknown file type.', 'desktop-mode' ), array( 'status' => 400 ) );
52 52 }
53 53
54 54 /**
55 55 * Gate placement creation. Defaults to allowing the user to
@@ -60,13 +60,13 @@
60 60 * @param int $user_id Owner.
61 61 * @param string $type File-type slug.
62 62 * @param string $ref Entity reference.
63 63 */
64 - $file = desktop_mode_resolve_file( $type, $ref );
64 + $file = openstation_resolve_file( $type, $ref );
65 65 $can = $file ? $file->can_read( $user_id ) : false;
66 - $can = (bool) apply_filters( 'desktop_mode_files_can_place', $can, $user_id, $type, $ref );
66 + $can = (bool) apply_filters( 'openstation_files_can_place', $can, $user_id, $type, $ref );
67 67 if ( ! $can ) {
68 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You are not allowed to place this file.', 'desktop-mode' ), array( 'status' => 403 ) );
68 + return new WP_Error( 'openstation_files_forbidden', __( 'You are not allowed to place this file.', 'desktop-mode' ), array( 'status' => 403 ) );
69 69 }
70 70
71 71 // Write-gate: placing INTO a non-owned folder requires the
72 72 // folder's `write` cap. Owner / desktop-root placements are
@@ -71,16 +71,16 @@
71 71 // Write-gate: placing INTO a non-owned folder requires the
72 72 // folder's `write` cap. Owner / desktop-root placements are
73 73 // always allowed.
74 74 if ( (int) $parent_id > 0 ) {
75 - $target_folder = desktop_mode_files_get_folder( (int) $parent_id );
75 + $target_folder = openstation_files_get_folder( (int) $parent_id );
76 76 if ( $target_folder && (int) $target_folder['owner_id'] !== $user_id ) {
77 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
78 - ? desktop_mode_folder_share_user_capability( (int) $parent_id, $user_id )
77 + $cap = function_exists( 'openstation_folder_share_user_capability' )
78 + ? openstation_folder_share_user_capability( (int) $parent_id, $user_id )
79 79 : 'none';
80 80 if ( 'write' !== $cap ) {
81 81 return new WP_Error(
82 - 'desktop_mode_files_no_write_in_shared_folder',
82 + 'openstation_files_no_write_in_shared_folder',
83 83 __( 'You only have read access to that folder.', 'desktop-mode' ),
84 84 array( 'status' => 403 )
85 85 );
86 86 }
@@ -96,10 +96,10 @@
96 96 'meta' => null,
97 97 )
98 98 );
99 99
100 - $tables = desktop_mode_files_table_names();
101 - $now = desktop_mode_files_now_ms();
100 + $tables = openstation_files_table_names();
101 + $now = openstation_files_now_ms();
102 102 $row = array(
103 103 'owner_id' => $user_id,
104 104 'updated_by' => $user_id,
105 105 'parent_id' => max( 0, $parent_id ),
@@ -118,21 +118,30 @@
118 118 // `<div class="wpdberror">…</div>` to the REST response body and
119 119 // break `await response.json()` on the client. `$wpdb->last_error`
120 120 // still holds the message, so genuine DB failures surface via the
121 121 // `WP_Error` we return when no existing row is found.
122 - $prev_suppress = $wpdb->suppress_errors( true );
123 - $ok = $wpdb->insert( $tables['placements'], $row, array( '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ) );
124 - $wpdb->suppress_errors( $prev_suppress );
122 + $insert = static function () use ( $tables, $row ) {
123 + global $wpdb;
124 + $prev_suppress = $wpdb->suppress_errors( true );
125 + $ok = $wpdb->insert( $tables['placements'], $row, array( '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ) );
126 + $wpdb->suppress_errors( $prev_suppress );
127 + return array( $ok, (int) $wpdb->insert_id );
128 + };
129 + $result = 'upload' === $type ? openstation_stored_files_place_insert( $ref, $insert ) : $insert();
130 + if ( is_wp_error( $result ) ) {
131 + return $result;
132 + }
133 + list( $ok, $id ) = $result;
125 134 if ( false === $ok ) {
126 135 // Disambiguate the two cases hidden behind a generic `false`:
127 - // (a) The `placement_unique` index collided
128 - // with an existing row for this (user, parent, type,
129 - // ref). The collider may be active (the orphan placer
130 - // won a race against this caller, or a stale duplicate
131 - // client request) or soft-trashed (the user removed a
132 - // link tile and is now recreating the same URL).
133 - // (b) Any other DB failure — connection, deadlock, bad
134 - // column. The error must surface to the caller as-is.
136 + // (a) The `placement_unique` index collided
137 + // with an existing row for this (user, parent, type,
138 + // ref). The collider may be active (the orphan placer
139 + // won a race against this caller, or a stale duplicate
140 + // client request) or soft-trashed (the user removed a
141 + // link tile and is now recreating the same URL).
142 + // (b) Any other DB failure — connection, deadlock, bad
143 + // column. The error must surface to the caller as-is.
135 144 // We treat (a) idempotently: restore if trashed, then apply
136 145 // the caller's coords / meta so the new placement lands
137 146 // where the user clicked. Reported as #167.
138 147 $existing = $wpdb->get_row(
@@ -150,15 +159,15 @@
150 159 ),
151 160 ARRAY_A
152 161 );
153 162 if ( ! $existing ) {
154 - return new WP_Error( 'desktop_mode_files_insert_failed', __( 'Failed to write placement.', 'desktop-mode' ), array( 'status' => 500 ) );
163 + return new WP_Error( 'openstation_files_insert_failed', __( 'Failed to write placement.', 'desktop-mode' ), array( 'status' => 500 ) );
155 164 }
156 165
157 166 $existing_id = (int) $existing['id'];
158 167
159 168 if ( ! empty( $existing['trashed_at_ms'] ) ) {
160 - $restore = desktop_mode_files_restore_placement( $user_id, $existing_id );
169 + $restore = openstation_files_restore_placement( $user_id, $existing_id );
161 170 if ( is_wp_error( $restore ) ) {
162 171 return $restore;
163 172 }
164 173 }
@@ -168,11 +177,11 @@
168 177 // stale tombstones for this id should be cleared so a fresh
169 178 // heartbeat tick can't surface "alive + removed" together.
170 179 // `restore_placement` already clears its own tombstones, so
171 180 // this is a no-op in the soft-trashed branch above.
172 - desktop_mode_files_clear_tombstones_for( 'placement', $existing_id );
181 + openstation_files_clear_tombstones_for( 'placement', $existing_id );
173 182
174 - $move = desktop_mode_files_move(
183 + $move = openstation_files_move(
175 184 $existing_id,
176 185 $user_id,
177 186 array(
178 187 'parent_id' => max( 0, $parent_id ),
@@ -187,10 +196,8 @@
187 196 }
188 197
189 198 return $existing_id;
190 199 }
191 - $id = (int) $wpdb->insert_id;
192 -
193 200 $row['id'] = $id;
194 201
195 202 /**
196 203 * Fires after a placement is created.
@@ -197,9 +204,9 @@
197 204 *
198 205 * @param int $id Placement id.
199 206 * @param array $row Inserted row.
200 207 */
201 - do_action( 'desktop_mode_file_placed', $id, $row );
208 + do_action( 'openstation_file_placed', $id, $row );
202 209
203 210 return $id;
204 211 }
205 212
@@ -214,20 +221,20 @@
214 221 * @param int $user_id Acting user (for capability gate).
215 222 * @param array $changes `parent_id`, `x`, `y`, `sort_order`, `meta`.
216 223 * @return true|WP_Error
217 224 */
218 -function desktop_mode_files_move( $placement_id, $user_id, $changes = array() ) {
225 +function openstation_files_move( $placement_id, $user_id, $changes = array() ) {
219 226 global $wpdb;
220 227
221 228 $placement_id = (int) $placement_id;
222 229 $user_id = (int) $user_id;
223 230 if ( $placement_id <= 0 || $user_id <= 0 ) {
224 - return new WP_Error( 'desktop_mode_files_bad_request', __( 'Invalid arguments.', 'desktop-mode' ), array( 'status' => 400 ) );
231 + return new WP_Error( 'openstation_files_bad_request', __( 'Invalid arguments.', 'desktop-mode' ), array( 'status' => 400 ) );
225 232 }
226 233
227 - $row = desktop_mode_files_get_placement( $placement_id );
234 + $row = openstation_files_get_placement( $placement_id );
228 235 if ( ! $row ) {
229 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) );
236 + return new WP_Error( 'openstation_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) );
230 237 }
231 238
232 239 // Owner-lock for `upload` placements: only the stored file's
233 240 // owner may move them — folder-share write capability does NOT
@@ -232,9 +239,9 @@
232 239 // Owner-lock for `upload` placements: only the stored file's
233 240 // owner may move them — folder-share write capability does NOT
234 241 // extend to uploaded files (recipients are read + download
235 242 // only; see stored-files-store.php).
236 - $upload_lock = desktop_mode_files_upload_owner_lock( $row, $user_id );
243 + $upload_lock = openstation_files_upload_owner_lock( $row, $user_id );
237 244 if ( is_wp_error( $upload_lock ) ) {
238 245 return $upload_lock;
239 246 }
240 247
@@ -244,17 +251,17 @@
244 251 // every icon in it, regardless of which user originally placed
245 252 // the row (shared-namespace semantics).
246 253 $is_row_owner = (int) $row['owner_id'] === $user_id;
247 254 if ( (int) $row['parent_id'] > 0 ) {
248 - $source_folder = desktop_mode_files_get_folder( (int) $row['parent_id'] );
255 + $source_folder = openstation_files_get_folder( (int) $row['parent_id'] );
249 256 if ( $source_folder ) {
250 257 $is_folder_owner = (int) $source_folder['owner_id'] === $user_id;
251 - $source_cap = function_exists( 'desktop_mode_folder_share_user_capability' )
252 - ? desktop_mode_folder_share_user_capability( (int) $row['parent_id'], $user_id )
258 + $source_cap = function_exists( 'openstation_folder_share_user_capability' )
259 + ? openstation_folder_share_user_capability( (int) $row['parent_id'], $user_id )
253 260 : 'none';
254 261 if ( ! $is_row_owner && ! $is_folder_owner && 'write' !== $source_cap ) {
255 262 return new WP_Error(
256 - 'desktop_mode_files_no_write_in_shared_folder',
263 + 'openstation_files_no_write_in_shared_folder',
257 264 __( 'You only have read access to this folder.', 'desktop-mode' ),
258 265 array( 'status' => 403 )
259 266 );
260 267 }
@@ -260,9 +267,9 @@
260 267 }
261 268 // Folder reader on their own row inside the folder — still no.
262 269 if ( $is_row_owner && ! $is_folder_owner && 'write' !== $source_cap ) {
263 270 return new WP_Error(
264 - 'desktop_mode_files_no_write_in_shared_folder',
271 + 'openstation_files_no_write_in_shared_folder',
265 272 __( 'You only have read access to this folder.', 'desktop-mode' ),
266 273 array( 'status' => 403 )
267 274 );
268 275 }
@@ -268,21 +275,21 @@
268 275 }
269 276 }
270 277 } elseif ( ! $is_row_owner ) {
271 278 // Row at root, viewer doesn't own it.
272 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot edit this placement.', 'desktop-mode' ), array( 'status' => 403 ) );
279 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot edit this placement.', 'desktop-mode' ), array( 'status' => 403 ) );
273 280 }
274 281 if ( isset( $changes['parent_id'] ) ) {
275 282 $target_parent = max( 0, (int) $changes['parent_id'] );
276 283 if ( $target_parent > 0 ) {
277 - $target = desktop_mode_files_get_folder( $target_parent );
284 + $target = openstation_files_get_folder( $target_parent );
278 285 if ( $target && (int) $target['owner_id'] !== $user_id ) {
279 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
280 - ? desktop_mode_folder_share_user_capability( $target_parent, $user_id )
286 + $cap = function_exists( 'openstation_folder_share_user_capability' )
287 + ? openstation_folder_share_user_capability( $target_parent, $user_id )
281 288 : 'none';
282 289 if ( 'write' !== $cap ) {
283 290 return new WP_Error(
284 - 'desktop_mode_files_no_write_in_shared_folder',
291 + 'openstation_files_no_write_in_shared_folder',
285 292 __( 'You only have read access to that folder.', 'desktop-mode' ),
286 293 array( 'status' => 403 )
287 294 );
288 295 }
@@ -299,9 +306,9 @@
299 306 if ( 'folder' === (string) $row['file_type'] && $target_parent > 0 ) {
300 307 $moving_folder_id = (int) $row['file_ref'];
301 308 if ( $moving_folder_id > 0 ) {
302 309 if (
303 - desktop_mode_files_would_create_folder_cycle(
310 + openstation_files_would_create_folder_cycle(
304 311 $user_id,
305 312 $moving_folder_id,
306 313 $target_parent
307 314 )
@@ -306,9 +313,9 @@
306 313 $target_parent
307 314 )
308 315 ) {
309 316 return new WP_Error(
310 - 'desktop_mode_files_folder_cycle',
317 + 'openstation_files_folder_cycle',
311 318 __( 'A folder cannot be placed inside itself or one of its descendants.', 'desktop-mode' ),
312 319 array( 'status' => 409 )
313 320 );
314 321 }
@@ -315,9 +322,9 @@
315 322 }
316 323 }
317 324 }
318 325
319 - $tables = desktop_mode_files_table_names();
326 + $tables = openstation_files_table_names();
320 327 $set = array();
321 328 $fmt = array();
322 329
323 330 if ( isset( $changes['parent_id'] ) ) {
@@ -337,22 +344,22 @@
337 344 if ( empty( $set ) ) {
338 345 return true; // No-op.
339 346 }
340 347
341 - $set['updated_at_ms'] = desktop_mode_files_now_ms();
348 + $set['updated_at_ms'] = openstation_files_now_ms();
342 349 $fmt[] = '%d';
343 350 // Track who actually fired this mutation so a future
344 351 // `If-Match` 409 can name the session that won the race,
345 352 // not just whoever happens to own the row.
346 - $set['updated_by'] = $user_id;
347 - $fmt[] = '%d';
353 + $set['updated_by'] = $user_id;
354 + $fmt[] = '%d';
348 355
349 356 $ok = $wpdb->update( $tables['placements'], $set, array( 'id' => $placement_id ), $fmt, array( '%d' ) );
350 357 if ( false === $ok ) {
351 - return new WP_Error( 'desktop_mode_files_update_failed', __( 'Failed to update placement.', 'desktop-mode' ), array( 'status' => 500 ) );
358 + return new WP_Error( 'openstation_files_update_failed', __( 'Failed to update placement.', 'desktop-mode' ), array( 'status' => 500 ) );
352 359 }
353 360
354 - $next = desktop_mode_files_get_placement( $placement_id );
361 + $next = openstation_files_get_placement( $placement_id );
355 362
356 363 /**
357 364 * Fires after a placement is moved / mutated.
358 365 *
@@ -359,9 +366,9 @@
359 366 * @param int $id Placement id.
360 367 * @param array $next Row after the change.
361 368 * @param array $prev Row before the change.
362 369 */
363 - do_action( 'desktop_mode_file_moved', $placement_id, $next, $row );
370 + do_action( 'openstation_file_moved', $placement_id, $next, $row );
364 371
365 372 return true;
366 373 }
367 374
@@ -371,20 +378,20 @@
371 378 * @param int $placement_id Placement id.
372 379 * @param int $user_id Acting user.
373 380 * @return true|WP_Error
374 381 */
375 -function desktop_mode_files_remove( $placement_id, $user_id ) {
382 +function openstation_files_remove( $placement_id, $user_id ) {
376 383 global $wpdb;
377 384
378 385 $placement_id = (int) $placement_id;
379 386 $user_id = (int) $user_id;
380 - $row = desktop_mode_files_get_placement( $placement_id );
387 + $row = openstation_files_get_placement( $placement_id );
381 388 if ( ! $row ) {
382 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) );
389 + return new WP_Error( 'openstation_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) );
383 390 }
384 391 // Owner-lock for `upload` placements — removal is destructive
385 392 // for real bytes, so only the stored file's owner may do it.
386 - $upload_lock = desktop_mode_files_upload_owner_lock( $row, $user_id );
393 + $upload_lock = openstation_files_upload_owner_lock( $row, $user_id );
387 394 if ( is_wp_error( $upload_lock ) ) {
388 395 return $upload_lock;
389 396 }
390 397 // Same shared-namespace rule as the trash gate: owner of the
@@ -391,24 +398,24 @@
391 398 // row OR write cap on the parent folder.
392 399 $is_row_owner = (int) $row['owner_id'] === $user_id;
393 400 $allowed = $is_row_owner;
394 401 if ( ! $allowed && (int) $row['parent_id'] > 0 ) {
395 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
396 - ? desktop_mode_folder_share_user_capability( (int) $row['parent_id'], $user_id )
402 + $cap = function_exists( 'openstation_folder_share_user_capability' )
403 + ? openstation_folder_share_user_capability( (int) $row['parent_id'], $user_id )
397 404 : 'none';
398 405 $allowed = 'write' === $cap;
399 406 }
400 407 if ( ! $allowed ) {
401 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot remove this placement.', 'desktop-mode' ), array( 'status' => 403 ) );
408 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot remove this placement.', 'desktop-mode' ), array( 'status' => 403 ) );
402 409 }
403 410
404 - $tables = desktop_mode_files_table_names();
411 + $tables = openstation_files_table_names();
405 412 $ok = $wpdb->delete( $tables['placements'], array( 'id' => $placement_id ), array( '%d' ) );
406 413 if ( false === $ok ) {
407 - return new WP_Error( 'desktop_mode_files_delete_failed', __( 'Failed to remove placement.', 'desktop-mode' ), array( 'status' => 500 ) );
414 + return new WP_Error( 'openstation_files_delete_failed', __( 'Failed to remove placement.', 'desktop-mode' ), array( 'status' => 500 ) );
408 415 }
409 416
410 - desktop_mode_files_write_tombstone( 'placement', $placement_id );
417 + openstation_files_write_tombstone( 'placement', $placement_id );
411 418
412 419 /**
413 420 * Fires after a placement is removed.
414 421 *
@@ -414,9 +421,9 @@
414 421 *
415 422 * @param int $id Placement id.
416 423 * @param array $row Removed row.
417 424 */
418 - do_action( 'desktop_mode_file_unplaced', $placement_id, $row );
425 + do_action( 'openstation_file_unplaced', $placement_id, $row );
419 426
420 427 return true;
421 428 }
422 429
@@ -433,16 +440,16 @@
433 440 * @param array $row Placement row.
434 441 * @param int $user_id Acting user.
435 442 * @return true|WP_Error
436 443 */
437 -function desktop_mode_files_upload_owner_lock( $row, $user_id ) {
444 +function openstation_files_upload_owner_lock( $row, $user_id ) {
438 445 if ( ! is_array( $row ) || 'upload' !== (string) ( $row['file_type'] ?? '' ) ) {
439 446 return true;
440 447 }
441 - if ( ! function_exists( 'desktop_mode_stored_files_get' ) ) {
448 + if ( ! function_exists( 'openstation_stored_files_get' ) ) {
442 449 return true;
443 450 }
444 - $stored = desktop_mode_stored_files_get( (int) $row['file_ref'] );
451 + $stored = openstation_stored_files_get( (int) $row['file_ref'] );
445 452 if ( ! $stored ) {
446 453 return true;
447 454 }
448 455 if ( (int) $stored['owner_id'] === (int) $user_id ) {
@@ -448,9 +455,9 @@
448 455 if ( (int) $stored['owner_id'] === (int) $user_id ) {
449 456 return true;
450 457 }
451 458 return new WP_Error(
452 - 'desktop_mode_files_upload_owner_locked',
459 + 'openstation_files_upload_owner_locked',
453 460 __( 'Only the file’s owner can move or delete an uploaded file.', 'desktop-mode' ),
454 461 array( 'status' => 403 )
455 462 );
456 463 }
@@ -460,11 +467,11 @@
460 467 *
461 468 * @param int $placement_id Placement id.
462 469 * @return array|null
463 470 */
464 -function desktop_mode_files_get_placement( $placement_id ) {
471 +function openstation_files_get_placement( $placement_id ) {
465 472 global $wpdb;
466 - $tables = desktop_mode_files_table_names();
473 + $tables = openstation_files_table_names();
467 474 $row = $wpdb->get_row(
468 475 $wpdb->prepare( "SELECT * FROM {$tables['placements']} WHERE id = %d", (int) $placement_id ),
469 476 ARRAY_A
470 477 );
@@ -470,14 +477,14 @@
470 477 );
471 478 if ( ! $row ) {
472 479 return null;
473 480 }
474 - return desktop_mode_files_normalize_placement_row( $row );
481 + return openstation_files_normalize_placement_row( $row );
475 482 }
476 483
477 484 /**
478 485 * List placements for a user under a given folder (0 = desktop
479 - * root). Honors the `desktop_mode_files_query_args` filter and
486 + * root). Honors the `openstation_files_query_args` filter and
480 487 * applies the file-type's `can_read()` per row.
481 488 *
482 489 * @param int $user_id Viewer.
483 490 * @param int $parent_id Folder id (0 for desktop root).
@@ -482,9 +489,9 @@
482 489 * @param int $user_id Viewer.
483 490 * @param int $parent_id Folder id (0 for desktop root).
484 491 * @return array[]
485 492 */
486 -function desktop_mode_files_get_for_user_folder( $user_id, $parent_id = 0 ) {
493 +function openstation_files_get_for_user_folder( $user_id, $parent_id = 0 ) {
487 494 global $wpdb;
488 495 $user_id = (int) $user_id;
489 496 $parent_id = max( 0, (int) $parent_id );
490 497 if ( $user_id <= 0 ) {
@@ -490,9 +497,9 @@
490 497 if ( $user_id <= 0 ) {
491 498 return array();
492 499 }
493 500
494 - $tables = desktop_mode_files_table_names();
501 + $tables = openstation_files_table_names();
495 502
496 503 // Access gate + shared-namespace decision for non-root folders.
497 504 // Desktop root (parent_id = 0) is always per-user. For sub-
498 505 // folders, the contents of a SHARED folder are visible to every
@@ -499,15 +506,15 @@
499 506 // user who has at least 'read' on it — the icons inside belong
500 507 // to the folder, not to the user who originally placed them.
501 508 $share_view = false;
502 509 if ( $parent_id > 0 ) {
503 - $folder = desktop_mode_files_get_folder( $parent_id );
510 + $folder = openstation_files_get_folder( $parent_id );
504 511 if ( ! $folder ) {
505 512 return array();
506 513 }
507 514 if ( (int) $folder['owner_id'] !== $user_id ) {
508 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
509 - ? desktop_mode_folder_share_user_capability( $parent_id, $user_id )
515 + $cap = function_exists( 'openstation_folder_share_user_capability' )
516 + ? openstation_folder_share_user_capability( $parent_id, $user_id )
510 517 : 'none';
511 518 if ( 'none' === $cap ) {
512 519 return array();
513 520 }
@@ -526,9 +533,9 @@
526 533 * @param array $args Defaults: `{ user_id, parent_id, share_view }`.
527 534 * @param int $user_id Viewer.
528 535 * @param int $parent_id Folder id.
529 536 */
530 - $args = (array) apply_filters( 'desktop_mode_files_query_args', $args, $user_id, $parent_id );
537 + $args = (array) apply_filters( 'openstation_files_query_args', $args, $user_id, $parent_id );
531 538
532 539 // Active queries always exclude trashed rows. Recycle-bin
533 540 // callers reach for the dedicated trash store.
534 541 if ( ! empty( $args['share_view'] ) ) {
@@ -564,10 +571,10 @@
564 571 return array();
565 572 }
566 573 $out = array();
567 574 foreach ( $rows as $row ) {
568 - $normalized = desktop_mode_files_normalize_placement_row( $row );
569 - $file = desktop_mode_resolve_file( $normalized['file_type'], $normalized['file_ref'] );
575 + $normalized = openstation_files_normalize_placement_row( $row );
576 + $file = openstation_resolve_file( $normalized['file_type'], $normalized['file_ref'] );
570 577 if ( empty( $args['share_view'] ) ) {
571 578 // Private folder / desktop root — keep the existing
572 579 // per-row read filter so stale/inaccessible entities
573 580 // don't clutter the user's own view.
@@ -573,9 +580,9 @@
573 580 // don't clutter the user's own view.
574 581 if ( $file && ! $file->can_read( $user_id ) ) {
575 582 continue;
576 583 }
577 - } else {
584 + } elseif ( $file && ! $file->can_read( $user_id ) ) {
578 585 // Shared folder view — every placement the OWNER chose
579 586 // to include is surfaced to the recipient. When the
580 587 // recipient lacks read on the underlying entity, we
581 588 // mark the row as `access_gated` so the tile renderer
@@ -582,11 +589,9 @@
582 589 // can paint a lock overlay + tooltip + intercept the
583 590 // open. Entity-level access enforcement still happens
584 591 // at open time in each opener — this flag is just the
585 592 // pre-emptive visual cue.
586 - if ( $file && ! $file->can_read( $user_id ) ) {
587 - $normalized['access_gated'] = true;
588 - }
593 + $normalized['access_gated'] = true;
589 594 }
590 595 $out[] = $normalized;
591 596 }
592 597 return $out;
@@ -599,9 +604,9 @@
599 604 * 1. Folders the viewer owns that have no placement anywhere.
600 605 * (Pre-fix folder-create flow could leak these; new flow
601 606 * writes the placement atomically.)
602 607 *
603 - * 2. Plugin shortcuts (`desktop_mode_register_icon()`) the
608 + * 2. Plugin shortcuts (`openstation_register_icon()`) the
604 609 * viewer hasn't placed yet. The unified-rail merge means
605 610 * every registered icon shows up as a `shortcut` placement
606 611 * on first hydrate so plugin shortcuts behave like any
607 612 * other tile (drag, sort, right-click, clean up).
@@ -606,10 +611,12 @@
606 611 * on first hydrate so plugin shortcuts behave like any
607 612 * other tile (drag, sort, right-click, clean up).
608 613 *
609 614 * Idempotent on both axes: a folder/shortcut that already has
610 - * any placement is left alone. Coordinates use the column-major
611 - * grid that `src/desktop-files/grid.ts` mirrors on the JS side.
615 + * any placement is left alone. Coordinates come from
616 + * `includes/desktop-files/grid.php`, which mirrors
617 + * `src/desktop-files/grid.ts` — pitch, reading order, and the
618 + * assumed canvas the scan wraps at.
612 619 *
613 620 * Called by the placements list endpoint when the requested
614 621 * folder is the root (`parent_id=0`).
615 622 *
@@ -615,9 +622,9 @@
615 622 *
616 623 * @param int $user_id Viewer.
617 624 * @return int Total number of orphans that were auto-placed.
618 625 */
619 -function desktop_mode_files_auto_place_orphans( $user_id ) {
626 +function openstation_files_auto_place_orphans( $user_id ) {
620 627 global $wpdb;
621 628 $user_id = (int) $user_id;
622 629 if ( $user_id <= 0 ) {
623 630 return 0;
@@ -622,9 +629,9 @@
622 629 if ( $user_id <= 0 ) {
623 630 return 0;
624 631 }
625 632
626 - $tables = desktop_mode_files_table_names();
633 + $tables = openstation_files_table_names();
627 634
628 635 // 1) Owned folders without any placement. Skip trashed folders
629 636 // and trashed placement rows so a recycled folder doesn't get
630 637 // auto-placed back on the desktop on next hydrate.
@@ -643,20 +650,20 @@
643 650 ARRAY_A
644 651 );
645 652
646 653 // 2) Registered plugin shortcuts the viewer hasn't placed yet.
647 - // Pull the registered ids first, then ask the placements
648 - // table which the viewer already has — set difference
649 - // yields the orphans without a heavy join.
650 - $shortcut_ids = array();
651 - $registry = function_exists( 'desktop_mode_desktop_icon_registry' )
652 - ? desktop_mode_desktop_icon_registry()
654 + // Pull the registered ids first, then ask the placements
655 + // table which the viewer already has — set difference
656 + // yields the orphans without a heavy join.
657 + $shortcut_ids = array();
658 + $registry = function_exists( 'openstation_desktop_icon_registry' )
659 + ? openstation_desktop_icon_registry()
653 660 : array();
654 661 if ( is_array( $registry ) ) {
655 - // Run through the same `desktop_mode_icons` filter the
662 + // Run through the same `openstation_icons` filter the
656 663 // build-payload path uses so plugins (and tests) can inject
657 664 // virtual entries.
658 - $registry = (array) apply_filters( 'desktop_mode_icons', $registry );
665 + $registry = (array) apply_filters( 'openstation_icons', $registry );
659 666 }
660 667 if ( is_array( $registry ) && ! empty( $registry ) ) {
661 668 $registered_ids = array_map( 'strval', array_keys( $registry ) );
662 669 $placeholders = implode( ',', array_fill( 0, count( $registered_ids ), '%s' ) );
@@ -670,9 +677,9 @@
670 677 AND file_ref IN ($placeholders)",
671 678 $args
672 679 )
673 680 );
674 - $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
681 + $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
675 682 foreach ( $registered_ids as $id ) {
676 683 if ( ! isset( $placed_set[ $id ] ) ) {
677 684 $shortcut_ids[] = $id;
678 685 }
@@ -684,10 +691,11 @@
684 691 }
685 692
686 693 // Build an occupied set from EXISTING root placements so
687 694 // we never drop an orphan on top of a tile the user
688 - // already has. Cell math mirrors `src/desktop-files/grid.ts`
689 - // (padding 16 + col 96 + row 110).
695 + // already has. Cell math lives in
696 + // `includes/desktop-files/grid.php`, the mirror of
697 + // `src/desktop-files/grid.ts`.
690 698 $existing = $wpdb->get_results(
691 699 $wpdb->prepare(
692 700 "SELECT x, y FROM {$tables['placements']}
693 701 WHERE owner_id = %d
@@ -696,47 +704,33 @@
696 704 $user_id
697 705 ),
698 706 ARRAY_A
699 707 );
700 - $occupied = array();
701 - foreach ( (array) $existing as $row ) {
702 - $col = max( 0, (int) round( ( (int) $row['x'] - 16 ) / 96 ) );
703 - $row_idx = max( 0, (int) round( ( (int) $row['y'] - 16 ) / 110 ) );
704 - $occupied[ "$col,$row_idx" ] = true;
705 - }
708 + $occupied = openstation_files_grid_occupied( $existing );
706 709
707 - $find_next = function () use ( &$occupied ) {
708 - for ( $col = 0; $col < 999; $col++ ) {
709 - for ( $row = 0; $row < 999; $row++ ) {
710 - $key = "$col,$row";
711 - if ( ! isset( $occupied[ $key ] ) ) {
712 - $occupied[ $key ] = true;
713 - return array( $col, $row );
714 - }
715 - }
716 - }
717 - return array( 0, 0 );
718 - };
710 + // The desktop reads in columns, and the scan wraps to the next one
711 + // at the assumed canvas height rather than running a column 999
712 + // cells deep. The server has no viewport; a slot it invents below
713 + // the fold is a tile the user cannot reach, because the layer that
714 + // renders it does not scroll.
715 + $order = openstation_files_grid_order( 0 );
719 716
720 - $placed = 0;
717 + $placed = 0;
721 718 $emit_at = function ( $type, $ref, $col, $row ) use ( $user_id, &$occupied, &$placed ) {
722 719 $occupied[ "$col,$row" ] = true;
723 - $result = desktop_mode_files_place(
720 + $result = openstation_files_place(
724 721 $user_id,
725 722 0,
726 723 $type,
727 724 (string) $ref,
728 - array(
729 - 'x' => 16 + $col * 96,
730 - 'y' => 16 + $row * 110,
731 - )
725 + openstation_files_grid_cell_to_point( $col, $row )
732 726 );
733 727 if ( ! is_wp_error( $result ) ) {
734 - $placed++;
728 + ++$placed;
735 729 }
736 730 };
737 - $emit_next = function ( $type, $ref ) use ( $find_next, $emit_at ) {
738 - list( $col, $row ) = $find_next();
731 + $emit_next = function ( $type, $ref ) use ( &$occupied, $order, $emit_at ) {
732 + list( $col, $row ) = openstation_files_grid_next_free( $occupied, $order );
739 733 $emit_at( $type, $ref, $col, $row );
740 734 };
741 735
742 736 // Pinned shortcuts get reserved top-left slots. Anchored to
@@ -761,9 +755,9 @@
761 755 // client-side override anyway, but a future cleanup pass
762 756 // can compact the column.
763 757 $occupied[ "0,$pinned_idx" ] = true;
764 758 $emit_at( 'shortcut', $id, 0, $pinned_idx );
765 - $pinned_idx++;
759 + ++$pinned_idx;
766 760 }
767 761
768 762 foreach ( $folder_rows as $row ) {
769 763 $emit_next( 'folder', $row['id'] );
@@ -779,15 +773,15 @@
779 773
780 774 /**
781 775 * Backwards-compat alias for the older folder-only name.
782 776 *
783 - * @deprecated Use {@see desktop_mode_files_auto_place_orphans}.
777 + * @deprecated Use {@see openstation_files_auto_place_orphans}.
784 778 *
785 779 * @param int $user_id Viewer.
786 780 * @return int
787 781 */
788 -function desktop_mode_files_auto_place_orphan_folders( $user_id ) {
789 - return desktop_mode_files_auto_place_orphans( $user_id );
782 +function openstation_files_auto_place_orphan_folders( $user_id ) {
783 + return openstation_files_auto_place_orphans( $user_id );
790 784 }
791 785
792 786 /**
793 787 * Coerce wpdb's stringly-typed row into typed values + decoded
@@ -797,9 +791,9 @@
797 791 *
798 792 * @param array $row Raw wpdb row.
799 793 * @return array
800 794 */
801 -function desktop_mode_files_normalize_placement_row( $row ) {
795 +function openstation_files_normalize_placement_row( $row ) {
802 796 $meta_raw = isset( $row['meta'] ) ? (string) $row['meta'] : '';
803 797 $meta = '' !== $meta_raw ? json_decode( $meta_raw, true ) : null;
804 798 return array(
805 799 'id' => (int) $row['id'],
@@ -804,9 +798,9 @@
804 798 return array(
805 799 'id' => (int) $row['id'],
806 800 'owner_id' => (int) $row['owner_id'],
807 801 // `updated_by` is v10. Null on legacy rows — callers that
808 - // need the actor (e.g. `desktop_mode_files_check_if_match`)
802 + // need the actor (e.g. `openstation_files_check_if_match`)
809 803 // fall back to `owner_id` when this is null/missing.
810 804 'updated_by' => isset( $row['updated_by'] ) ? (int) $row['updated_by'] : null,
811 805 'parent_id' => (int) $row['parent_id'],
812 806 'file_type' => (string) $row['file_type'],
@@ -825,17 +819,17 @@
825 819 * Invariant (enforced by callers): tombstones may exist only for
826 820 * ids of PERMANENTLY-DELETED rows. Never write one for a soft-
827 821 * trashed row — soft-trash is reversible and the heartbeat already
828 822 * surfaces it via the `trashed_at_ms IS NOT NULL` query in
829 - * `desktop_mode_files_compute_heartbeat_delta`. A tombstone on a
823 + * `openstation_files_compute_heartbeat_delta`. A tombstone on a
830 824 * soft-trashed row lingers past restore and tells clients the row
831 825 * is gone while it is in fact alive — see the "shared folder
832 826 * disappears on refresh" bug.
833 827 *
834 - * Pair every revival path (`desktop_mode_files_restore_placement`,
835 - * `desktop_mode_files_restore_folder`, and the duplicate-key
836 - * revival branch in `desktop_mode_files_place`) with
837 - * {@see desktop_mode_files_clear_tombstones_for} so a row coming
828 + * Pair every revival path (`openstation_files_restore_placement`,
829 + * `openstation_files_restore_folder`, and the duplicate-key
830 + * revival branch in `openstation_files_place`) with
831 + * {@see openstation_files_clear_tombstones_for} so a row coming
838 832 * back to life never carries lingering tombstones from a previous
839 833 * removal that turned out to be reversible.
840 834 *
841 835 * @param string $kind 'placement' | 'folder'.
@@ -840,17 +834,17 @@
840 834 *
841 835 * @param string $kind 'placement' | 'folder'.
842 836 * @param int $ref Removed id.
843 837 */
844 -function desktop_mode_files_write_tombstone( $kind, $ref ) {
838 +function openstation_files_write_tombstone( $kind, $ref ) {
845 839 global $wpdb;
846 - $tables = desktop_mode_files_table_names();
840 + $tables = openstation_files_table_names();
847 841 $wpdb->insert(
848 842 $tables['tombstones'],
849 843 array(
850 844 'kind' => (string) $kind,
851 845 'ref_id' => (int) $ref,
852 - 'removed_at_ms' => desktop_mode_files_now_ms(),
846 + 'removed_at_ms' => openstation_files_now_ms(),
853 847 ),
854 848 array( '%s', '%d', '%d' )
855 849 );
856 850 }
@@ -865,15 +859,15 @@
865 859 *
866 860 * @param string $kind 'placement' | 'folder'.
867 861 * @param int $ref_id Row id whose tombstones should be dropped.
868 862 */
869 -function desktop_mode_files_clear_tombstones_for( $kind, $ref_id ) {
863 +function openstation_files_clear_tombstones_for( $kind, $ref_id ) {
870 864 global $wpdb;
871 865 $ref_id = (int) $ref_id;
872 866 if ( $ref_id <= 0 ) {
873 867 return;
874 868 }
875 - $tables = desktop_mode_files_table_names();
869 + $tables = openstation_files_table_names();
876 870 $wpdb->delete(
877 871 $tables['tombstones'],
878 872 array(
879 873 'kind' => (string) $kind,
@@ -888,15 +882,15 @@
888 882 * the retention window when the Heartbeat sync lands; for now 7d
889 883 * is plenty since a client that's been offline that long will
890 884 * always need a full REST resync anyway.
891 885 */
892 -function desktop_mode_files_prune_tombstones() {
886 +function openstation_files_prune_tombstones() {
893 887 global $wpdb;
894 - $tables = desktop_mode_files_table_names();
895 - $cutoff = desktop_mode_files_now_ms() - ( 7 * DAY_IN_SECONDS * 1000 );
888 + $tables = openstation_files_table_names();
889 + $cutoff = openstation_files_now_ms() - ( 7 * DAY_IN_SECONDS * 1000 );
896 890 $wpdb->query( $wpdb->prepare( "DELETE FROM {$tables['tombstones']} WHERE removed_at_ms < %d", $cutoff ) );
897 891 }
898 -add_action( 'desktop_mode_files_daily_prune', 'desktop_mode_files_prune_tombstones' );
892 +add_action( 'desktop_mode_files_daily_prune', 'openstation_files_prune_tombstones' );
899 893
900 894 /**
901 895 * Schedule the daily prune. Hooked on `init` and idempotent via
902 896 * wp_next_scheduled(), so a manual file-copy install (no activation
@@ -901,14 +895,14 @@
901 895 * Schedule the daily prune. Hooked on `init` and idempotent via
902 896 * wp_next_scheduled(), so a manual file-copy install (no activation
903 897 * hook) still gets the cron event.
904 898 */
905 -function desktop_mode_files_schedule_prune() {
899 +function openstation_files_schedule_prune() {
906 900 if ( ! wp_next_scheduled( 'desktop_mode_files_daily_prune' ) ) {
907 901 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'desktop_mode_files_daily_prune' );
908 902 }
909 903 }
910 -add_action( 'init', 'desktop_mode_files_schedule_prune' );
904 +add_action( 'init', 'openstation_files_schedule_prune' );
911 905
912 906 /**
913 907 * Walk the folder-parentage chain upward from `$target_parent_id` and
914 908 * return `true` when `$moving_folder_id` appears anywhere in it —
@@ -931,9 +925,9 @@
931 925 * @param int $moving_folder_id Folder being moved (its `folders.id`).
932 926 * @param int $target_parent_id New container folder id (0 = desktop root).
933 927 * @return bool True when the move would create a cycle.
934 928 */
935 -function desktop_mode_files_would_create_folder_cycle( $user_id, $moving_folder_id, $target_parent_id ) {
929 +function openstation_files_would_create_folder_cycle( $user_id, $moving_folder_id, $target_parent_id ) {
936 930 $moving_folder_id = (int) $moving_folder_id;
937 931 $target_parent_id = (int) $target_parent_id;
938 932 $user_id = (int) $user_id;
939 933 if ( $moving_folder_id <= 0 || $target_parent_id <= 0 || $user_id <= 0 ) {
@@ -942,9 +936,9 @@
942 936 if ( $moving_folder_id === $target_parent_id ) {
943 937 return true;
944 938 }
945 939 global $wpdb;
946 - $tables = desktop_mode_files_table_names();
940 + $tables = openstation_files_table_names();
947 941 $visited = array();
948 942 $cursor = $target_parent_id;
949 943 // Hard cap to defend against catastrophically deep trees too —
950 944 // real installs won't approach 256.