PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 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 All 37 releases
← All changes | includes/desktop-files/store.php +162 -167 0.9.8 → 1.1.13 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,17 +533,18 @@
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 - if ( ! empty( $args['share_view'] ) ) {
535 - // Shared sub-folder — return every placement in the folder
536 - // regardless of which user originally placed it. The icons
537 - // are part of the folder; the owner_id column is audit info,
538 - // not a permission gate.
541 + if ( ! empty( $args['share_view'] ) || (int) $args['parent_id'] > 0 ) {
542 + // A sub-folder, the viewer's own or shared with them: return
543 + // every placement in it regardless of which user placed it,
544 + // so the owner of a shared folder sees what a writer added.
545 + // The icons are part of the folder; the owner_id column is
546 + // audit info, not a permission gate.
539 547 $rows = $wpdb->get_results(
540 548 $wpdb->prepare(
541 549 "SELECT * FROM {$tables['placements']}
542 550 WHERE parent_id = %d
@@ -564,18 +572,18 @@
564 572 return array();
565 573 }
566 574 $out = array();
567 575 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'] );
576 + $normalized = openstation_files_normalize_placement_row( $row );
577 + $file = openstation_resolve_file( $normalized['file_type'], $normalized['file_ref'] );
570 578 if ( empty( $args['share_view'] ) ) {
571 - // Private folder / desktop root — keep the existing
572 - // per-row read filter so stale/inaccessible entities
573 - // don't clutter the user's own view.
579 + // The viewer's own folder or desktop root: keep the
580 + // per-row read filter so stale or inaccessible entities,
581 + // a writer's included, don't clutter the user's own view.
574 582 if ( $file && ! $file->can_read( $user_id ) ) {
575 583 continue;
576 584 }
577 - } else {
585 + } elseif ( $file && ! $file->can_read( $user_id ) ) {
578 586 // Shared folder view — every placement the OWNER chose
579 587 // to include is surfaced to the recipient. When the
580 588 // recipient lacks read on the underlying entity, we
581 589 // mark the row as `access_gated` so the tile renderer
@@ -582,11 +590,9 @@
582 590 // can paint a lock overlay + tooltip + intercept the
583 591 // open. Entity-level access enforcement still happens
584 592 // at open time in each opener — this flag is just the
585 593 // pre-emptive visual cue.
586 - if ( $file && ! $file->can_read( $user_id ) ) {
587 - $normalized['access_gated'] = true;
588 - }
594 + $normalized['access_gated'] = true;
589 595 }
590 596 $out[] = $normalized;
591 597 }
592 598 return $out;
@@ -599,9 +605,9 @@
599 605 * 1. Folders the viewer owns that have no placement anywhere.
600 606 * (Pre-fix folder-create flow could leak these; new flow
601 607 * writes the placement atomically.)
602 608 *
603 - * 2. Plugin shortcuts (`desktop_mode_register_icon()`) the
609 + * 2. Plugin shortcuts (`openstation_register_icon()`) the
604 610 * viewer hasn't placed yet. The unified-rail merge means
605 611 * every registered icon shows up as a `shortcut` placement
606 612 * on first hydrate so plugin shortcuts behave like any
607 613 * other tile (drag, sort, right-click, clean up).
@@ -606,10 +612,12 @@
606 612 * on first hydrate so plugin shortcuts behave like any
607 613 * other tile (drag, sort, right-click, clean up).
608 614 *
609 615 * 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.
616 + * any placement is left alone. Coordinates come from
617 + * `includes/desktop-files/grid.php`, which mirrors
618 + * `src/desktop-files/grid.ts` — pitch, reading order, and the
619 + * assumed canvas the scan wraps at.
612 620 *
613 621 * Called by the placements list endpoint when the requested
614 622 * folder is the root (`parent_id=0`).
615 623 *
@@ -615,9 +623,9 @@
615 623 *
616 624 * @param int $user_id Viewer.
617 625 * @return int Total number of orphans that were auto-placed.
618 626 */
619 -function desktop_mode_files_auto_place_orphans( $user_id ) {
627 +function openstation_files_auto_place_orphans( $user_id ) {
620 628 global $wpdb;
621 629 $user_id = (int) $user_id;
622 630 if ( $user_id <= 0 ) {
623 631 return 0;
@@ -622,9 +630,9 @@
622 630 if ( $user_id <= 0 ) {
623 631 return 0;
624 632 }
625 633
626 - $tables = desktop_mode_files_table_names();
634 + $tables = openstation_files_table_names();
627 635
628 636 // 1) Owned folders without any placement. Skip trashed folders
629 637 // and trashed placement rows so a recycled folder doesn't get
630 638 // auto-placed back on the desktop on next hydrate.
@@ -643,20 +651,20 @@
643 651 ARRAY_A
644 652 );
645 653
646 654 // 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()
655 + // Pull the registered ids first, then ask the placements
656 + // table which the viewer already has — set difference
657 + // yields the orphans without a heavy join.
658 + $shortcut_ids = array();
659 + $registry = function_exists( 'openstation_desktop_icon_registry' )
660 + ? openstation_desktop_icon_registry()
653 661 : array();
654 662 if ( is_array( $registry ) ) {
655 - // Run through the same `desktop_mode_icons` filter the
663 + // Run through the same `openstation_icons` filter the
656 664 // build-payload path uses so plugins (and tests) can inject
657 665 // virtual entries.
658 - $registry = (array) apply_filters( 'desktop_mode_icons', $registry );
666 + $registry = (array) apply_filters( 'openstation_icons', $registry );
659 667 }
660 668 if ( is_array( $registry ) && ! empty( $registry ) ) {
661 669 $registered_ids = array_map( 'strval', array_keys( $registry ) );
662 670 $placeholders = implode( ',', array_fill( 0, count( $registered_ids ), '%s' ) );
@@ -670,9 +678,9 @@
670 678 AND file_ref IN ($placeholders)",
671 679 $args
672 680 )
673 681 );
674 - $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
682 + $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
675 683 foreach ( $registered_ids as $id ) {
676 684 if ( ! isset( $placed_set[ $id ] ) ) {
677 685 $shortcut_ids[] = $id;
678 686 }
@@ -684,10 +692,11 @@
684 692 }
685 693
686 694 // Build an occupied set from EXISTING root placements so
687 695 // 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).
696 + // already has. Cell math lives in
697 + // `includes/desktop-files/grid.php`, the mirror of
698 + // `src/desktop-files/grid.ts`.
690 699 $existing = $wpdb->get_results(
691 700 $wpdb->prepare(
692 701 "SELECT x, y FROM {$tables['placements']}
693 702 WHERE owner_id = %d
@@ -696,47 +705,33 @@
696 705 $user_id
697 706 ),
698 707 ARRAY_A
699 708 );
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 - }
709 + $occupied = openstation_files_grid_occupied( $existing );
706 710
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 - };
711 + // The desktop reads in columns, and the scan wraps to the next one
712 + // at the assumed canvas height rather than running a column 999
713 + // cells deep. The server has no viewport; a slot it invents below
714 + // the fold is a tile the user cannot reach, because the layer that
715 + // renders it does not scroll.
716 + $order = openstation_files_grid_order( 0 );
719 717
720 - $placed = 0;
718 + $placed = 0;
721 719 $emit_at = function ( $type, $ref, $col, $row ) use ( $user_id, &$occupied, &$placed ) {
722 720 $occupied[ "$col,$row" ] = true;
723 - $result = desktop_mode_files_place(
721 + $result = openstation_files_place(
724 722 $user_id,
725 723 0,
726 724 $type,
727 725 (string) $ref,
728 - array(
729 - 'x' => 16 + $col * 96,
730 - 'y' => 16 + $row * 110,
731 - )
726 + openstation_files_grid_cell_to_point( $col, $row )
732 727 );
733 728 if ( ! is_wp_error( $result ) ) {
734 - $placed++;
729 + ++$placed;
735 730 }
736 731 };
737 - $emit_next = function ( $type, $ref ) use ( $find_next, $emit_at ) {
738 - list( $col, $row ) = $find_next();
732 + $emit_next = function ( $type, $ref ) use ( &$occupied, $order, $emit_at ) {
733 + list( $col, $row ) = openstation_files_grid_next_free( $occupied, $order );
739 734 $emit_at( $type, $ref, $col, $row );
740 735 };
741 736
742 737 // Pinned shortcuts get reserved top-left slots. Anchored to
@@ -761,9 +756,9 @@
761 756 // client-side override anyway, but a future cleanup pass
762 757 // can compact the column.
763 758 $occupied[ "0,$pinned_idx" ] = true;
764 759 $emit_at( 'shortcut', $id, 0, $pinned_idx );
765 - $pinned_idx++;
760 + ++$pinned_idx;
766 761 }
767 762
768 763 foreach ( $folder_rows as $row ) {
769 764 $emit_next( 'folder', $row['id'] );
@@ -779,15 +774,15 @@
779 774
780 775 /**
781 776 * Backwards-compat alias for the older folder-only name.
782 777 *
783 - * @deprecated Use {@see desktop_mode_files_auto_place_orphans}.
778 + * @deprecated Use {@see openstation_files_auto_place_orphans}.
784 779 *
785 780 * @param int $user_id Viewer.
786 781 * @return int
787 782 */
788 -function desktop_mode_files_auto_place_orphan_folders( $user_id ) {
789 - return desktop_mode_files_auto_place_orphans( $user_id );
783 +function openstation_files_auto_place_orphan_folders( $user_id ) {
784 + return openstation_files_auto_place_orphans( $user_id );
790 785 }
791 786
792 787 /**
793 788 * Coerce wpdb's stringly-typed row into typed values + decoded
@@ -797,9 +792,9 @@
797 792 *
798 793 * @param array $row Raw wpdb row.
799 794 * @return array
800 795 */
801 -function desktop_mode_files_normalize_placement_row( $row ) {
796 +function openstation_files_normalize_placement_row( $row ) {
802 797 $meta_raw = isset( $row['meta'] ) ? (string) $row['meta'] : '';
803 798 $meta = '' !== $meta_raw ? json_decode( $meta_raw, true ) : null;
804 799 return array(
805 800 'id' => (int) $row['id'],
@@ -804,9 +799,9 @@
804 799 return array(
805 800 'id' => (int) $row['id'],
806 801 'owner_id' => (int) $row['owner_id'],
807 802 // `updated_by` is v10. Null on legacy rows — callers that
808 - // need the actor (e.g. `desktop_mode_files_check_if_match`)
803 + // need the actor (e.g. `openstation_files_check_if_match`)
809 804 // fall back to `owner_id` when this is null/missing.
810 805 'updated_by' => isset( $row['updated_by'] ) ? (int) $row['updated_by'] : null,
811 806 'parent_id' => (int) $row['parent_id'],
812 807 'file_type' => (string) $row['file_type'],
@@ -825,17 +820,17 @@
825 820 * Invariant (enforced by callers): tombstones may exist only for
826 821 * ids of PERMANENTLY-DELETED rows. Never write one for a soft-
827 822 * trashed row — soft-trash is reversible and the heartbeat already
828 823 * surfaces it via the `trashed_at_ms IS NOT NULL` query in
829 - * `desktop_mode_files_compute_heartbeat_delta`. A tombstone on a
824 + * `openstation_files_compute_heartbeat_delta`. A tombstone on a
830 825 * soft-trashed row lingers past restore and tells clients the row
831 826 * is gone while it is in fact alive — see the "shared folder
832 827 * disappears on refresh" bug.
833 828 *
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
829 + * Pair every revival path (`openstation_files_restore_placement`,
830 + * `openstation_files_restore_folder`, and the duplicate-key
831 + * revival branch in `openstation_files_place`) with
832 + * {@see openstation_files_clear_tombstones_for} so a row coming
838 833 * back to life never carries lingering tombstones from a previous
839 834 * removal that turned out to be reversible.
840 835 *
841 836 * @param string $kind 'placement' | 'folder'.
@@ -840,17 +835,17 @@
840 835 *
841 836 * @param string $kind 'placement' | 'folder'.
842 837 * @param int $ref Removed id.
843 838 */
844 -function desktop_mode_files_write_tombstone( $kind, $ref ) {
839 +function openstation_files_write_tombstone( $kind, $ref ) {
845 840 global $wpdb;
846 - $tables = desktop_mode_files_table_names();
841 + $tables = openstation_files_table_names();
847 842 $wpdb->insert(
848 843 $tables['tombstones'],
849 844 array(
850 845 'kind' => (string) $kind,
851 846 'ref_id' => (int) $ref,
852 - 'removed_at_ms' => desktop_mode_files_now_ms(),
847 + 'removed_at_ms' => openstation_files_now_ms(),
853 848 ),
854 849 array( '%s', '%d', '%d' )
855 850 );
856 851 }
@@ -865,15 +860,15 @@
865 860 *
866 861 * @param string $kind 'placement' | 'folder'.
867 862 * @param int $ref_id Row id whose tombstones should be dropped.
868 863 */
869 -function desktop_mode_files_clear_tombstones_for( $kind, $ref_id ) {
864 +function openstation_files_clear_tombstones_for( $kind, $ref_id ) {
870 865 global $wpdb;
871 866 $ref_id = (int) $ref_id;
872 867 if ( $ref_id <= 0 ) {
873 868 return;
874 869 }
875 - $tables = desktop_mode_files_table_names();
870 + $tables = openstation_files_table_names();
876 871 $wpdb->delete(
877 872 $tables['tombstones'],
878 873 array(
879 874 'kind' => (string) $kind,
@@ -888,15 +883,15 @@
888 883 * the retention window when the Heartbeat sync lands; for now 7d
889 884 * is plenty since a client that's been offline that long will
890 885 * always need a full REST resync anyway.
891 886 */
892 -function desktop_mode_files_prune_tombstones() {
887 +function openstation_files_prune_tombstones() {
893 888 global $wpdb;
894 - $tables = desktop_mode_files_table_names();
895 - $cutoff = desktop_mode_files_now_ms() - ( 7 * DAY_IN_SECONDS * 1000 );
889 + $tables = openstation_files_table_names();
890 + $cutoff = openstation_files_now_ms() - ( 7 * DAY_IN_SECONDS * 1000 );
896 891 $wpdb->query( $wpdb->prepare( "DELETE FROM {$tables['tombstones']} WHERE removed_at_ms < %d", $cutoff ) );
897 892 }
898 -add_action( 'desktop_mode_files_daily_prune', 'desktop_mode_files_prune_tombstones' );
893 +add_action( 'desktop_mode_files_daily_prune', 'openstation_files_prune_tombstones' );
899 894
900 895 /**
901 896 * Schedule the daily prune. Hooked on `init` and idempotent via
902 897 * wp_next_scheduled(), so a manual file-copy install (no activation
@@ -901,14 +896,14 @@
901 896 * Schedule the daily prune. Hooked on `init` and idempotent via
902 897 * wp_next_scheduled(), so a manual file-copy install (no activation
903 898 * hook) still gets the cron event.
904 899 */
905 -function desktop_mode_files_schedule_prune() {
900 +function openstation_files_schedule_prune() {
906 901 if ( ! wp_next_scheduled( 'desktop_mode_files_daily_prune' ) ) {
907 902 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'desktop_mode_files_daily_prune' );
908 903 }
909 904 }
910 -add_action( 'init', 'desktop_mode_files_schedule_prune' );
905 +add_action( 'init', 'openstation_files_schedule_prune' );
911 906
912 907 /**
913 908 * Walk the folder-parentage chain upward from `$target_parent_id` and
914 909 * return `true` when `$moving_folder_id` appears anywhere in it —
@@ -931,9 +926,9 @@
931 926 * @param int $moving_folder_id Folder being moved (its `folders.id`).
932 927 * @param int $target_parent_id New container folder id (0 = desktop root).
933 928 * @return bool True when the move would create a cycle.
934 929 */
935 -function desktop_mode_files_would_create_folder_cycle( $user_id, $moving_folder_id, $target_parent_id ) {
930 +function openstation_files_would_create_folder_cycle( $user_id, $moving_folder_id, $target_parent_id ) {
936 931 $moving_folder_id = (int) $moving_folder_id;
937 932 $target_parent_id = (int) $target_parent_id;
938 933 $user_id = (int) $user_id;
939 934 if ( $moving_folder_id <= 0 || $target_parent_id <= 0 || $user_id <= 0 ) {
@@ -942,9 +937,9 @@
942 937 if ( $moving_folder_id === $target_parent_id ) {
943 938 return true;
944 939 }
945 940 global $wpdb;
946 - $tables = desktop_mode_files_table_names();
941 + $tables = openstation_files_table_names();
947 942 $visited = array();
948 943 $cursor = $target_parent_id;
949 944 // Hard cap to defend against catastrophically deep trees too —
950 945 // real installs won't approach 256.