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 +163 -204 0.9.6 → 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,18 +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
23 - * @since 0.9.0
22 + * @package OpenStation
24 23 */
25 24
26 25 defined( 'ABSPATH' ) || exit;
27 26
@@ -27,10 +26,8 @@
27 26
28 27 /**
29 28 * Insert a placement.
30 29 *
31 - * @since 0.9.0
32 - *
33 30 * @param int $user_id Owner of the placement (the user the
34 31 * tile lives on).
35 32 * @param int $parent_id Folder id, or 0 for the desktop root.
36 33 * @param string $type File-type slug.
@@ -37,9 +34,9 @@
37 34 * @param string $ref Entity reference.
38 35 * @param array $args Optional. `x`, `y`, `sort_order`, `meta`.
39 36 * @return int|WP_Error Placement id on success, `WP_Error` otherwise.
40 37 */
41 -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() ) {
42 39 global $wpdb;
43 40
44 41 $user_id = (int) $user_id;
45 42 $parent_id = (int) $parent_id;
@@ -46,13 +43,13 @@
46 43 $type = (string) $type;
47 44 $ref = (string) $ref;
48 45
49 46 if ( $user_id <= 0 ) {
50 - 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 ) );
51 48 }
52 - $entry = desktop_mode_get_file_type( $type );
49 + $entry = openstation_get_file_type( $type );
53 50 if ( ! $entry ) {
54 - 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 ) );
55 52 }
56 53
57 54 /**
58 55 * Gate placement creation. Defaults to allowing the user to
@@ -58,20 +55,18 @@
58 55 * Gate placement creation. Defaults to allowing the user to
59 56 * place any type they can read; plugins use this to enforce
60 57 * stricter rules (e.g. only admins may place users).
61 58 *
62 - * @since 0.9.0
63 - *
64 59 * @param bool $can Default: file's `can_read( $user_id )`.
65 60 * @param int $user_id Owner.
66 61 * @param string $type File-type slug.
67 62 * @param string $ref Entity reference.
68 63 */
69 - $file = desktop_mode_resolve_file( $type, $ref );
64 + $file = openstation_resolve_file( $type, $ref );
70 65 $can = $file ? $file->can_read( $user_id ) : false;
71 - $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 );
72 67 if ( ! $can ) {
73 - 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 ) );
74 69 }
75 70
76 71 // Write-gate: placing INTO a non-owned folder requires the
77 72 // folder's `write` cap. Owner / desktop-root placements are
@@ -76,16 +71,16 @@
76 71 // Write-gate: placing INTO a non-owned folder requires the
77 72 // folder's `write` cap. Owner / desktop-root placements are
78 73 // always allowed.
79 74 if ( (int) $parent_id > 0 ) {
80 - $target_folder = desktop_mode_files_get_folder( (int) $parent_id );
75 + $target_folder = openstation_files_get_folder( (int) $parent_id );
81 76 if ( $target_folder && (int) $target_folder['owner_id'] !== $user_id ) {
82 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
83 - ? 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 )
84 79 : 'none';
85 80 if ( 'write' !== $cap ) {
86 81 return new WP_Error(
87 - 'desktop_mode_files_no_write_in_shared_folder',
82 + 'openstation_files_no_write_in_shared_folder',
88 83 __( 'You only have read access to that folder.', 'desktop-mode' ),
89 84 array( 'status' => 403 )
90 85 );
91 86 }
@@ -101,10 +96,10 @@
101 96 'meta' => null,
102 97 )
103 98 );
104 99
105 - $tables = desktop_mode_files_table_names();
106 - $now = desktop_mode_files_now_ms();
100 + $tables = openstation_files_table_names();
101 + $now = openstation_files_now_ms();
107 102 $row = array(
108 103 'owner_id' => $user_id,
109 104 'updated_by' => $user_id,
110 105 'parent_id' => max( 0, $parent_id ),
@@ -123,21 +118,30 @@
123 118 // `<div class="wpdberror">…</div>` to the REST response body and
124 119 // break `await response.json()` on the client. `$wpdb->last_error`
125 120 // still holds the message, so genuine DB failures surface via the
126 121 // `WP_Error` we return when no existing row is found.
127 - $prev_suppress = $wpdb->suppress_errors( true );
128 - $ok = $wpdb->insert( $tables['placements'], $row, array( '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ) );
129 - $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;
130 134 if ( false === $ok ) {
131 135 // Disambiguate the two cases hidden behind a generic `false`:
132 - // (a) The `placement_unique` index (since 0.8.0) collided
133 - // with an existing row for this (user, parent, type,
134 - // ref). The collider may be active (the orphan placer
135 - // won a race against this caller, or a stale duplicate
136 - // client request) or soft-trashed (the user removed a
137 - // link tile and is now recreating the same URL).
138 - // (b) Any other DB failure — connection, deadlock, bad
139 - // 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.
140 144 // We treat (a) idempotently: restore if trashed, then apply
141 145 // the caller's coords / meta so the new placement lands
142 146 // where the user clicked. Reported as #167.
143 147 $existing = $wpdb->get_row(
@@ -155,15 +159,15 @@
155 159 ),
156 160 ARRAY_A
157 161 );
158 162 if ( ! $existing ) {
159 - 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 ) );
160 164 }
161 165
162 166 $existing_id = (int) $existing['id'];
163 167
164 168 if ( ! empty( $existing['trashed_at_ms'] ) ) {
165 - $restore = desktop_mode_files_restore_placement( $user_id, $existing_id );
169 + $restore = openstation_files_restore_placement( $user_id, $existing_id );
166 170 if ( is_wp_error( $restore ) ) {
167 171 return $restore;
168 172 }
169 173 }
@@ -173,11 +177,11 @@
173 177 // stale tombstones for this id should be cleared so a fresh
174 178 // heartbeat tick can't surface "alive + removed" together.
175 179 // `restore_placement` already clears its own tombstones, so
176 180 // this is a no-op in the soft-trashed branch above.
177 - desktop_mode_files_clear_tombstones_for( 'placement', $existing_id );
181 + openstation_files_clear_tombstones_for( 'placement', $existing_id );
178 182
179 - $move = desktop_mode_files_move(
183 + $move = openstation_files_move(
180 184 $existing_id,
181 185 $user_id,
182 186 array(
183 187 'parent_id' => max( 0, $parent_id ),
@@ -192,21 +196,17 @@
192 196 }
193 197
194 198 return $existing_id;
195 199 }
196 - $id = (int) $wpdb->insert_id;
197 -
198 200 $row['id'] = $id;
199 201
200 202 /**
201 203 * Fires after a placement is created.
202 204 *
203 - * @since 0.9.0
204 - *
205 205 * @param int $id Placement id.
206 206 * @param array $row Inserted row.
207 207 */
208 - do_action( 'desktop_mode_file_placed', $id, $row );
208 + do_action( 'openstation_file_placed', $id, $row );
209 209
210 210 return $id;
211 211 }
212 212
@@ -216,27 +216,25 @@
216 216 * the same as omitting the key; for `meta`, an explicit
217 217 * `meta => null` CLEARS the column (keyed on array_key_exists) —
218 218 * omit the key to preserve it.
219 219 *
220 - * @since 0.9.0
221 - *
222 220 * @param int $placement_id Placement id.
223 221 * @param int $user_id Acting user (for capability gate).
224 222 * @param array $changes `parent_id`, `x`, `y`, `sort_order`, `meta`.
225 223 * @return true|WP_Error
226 224 */
227 -function desktop_mode_files_move( $placement_id, $user_id, $changes = array() ) {
225 +function openstation_files_move( $placement_id, $user_id, $changes = array() ) {
228 226 global $wpdb;
229 227
230 228 $placement_id = (int) $placement_id;
231 229 $user_id = (int) $user_id;
232 230 if ( $placement_id <= 0 || $user_id <= 0 ) {
233 - 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 ) );
234 232 }
235 233
236 - $row = desktop_mode_files_get_placement( $placement_id );
234 + $row = openstation_files_get_placement( $placement_id );
237 235 if ( ! $row ) {
238 - 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 ) );
239 237 }
240 238
241 239 // Owner-lock for `upload` placements: only the stored file's
242 240 // owner may move them — folder-share write capability does NOT
@@ -241,9 +239,9 @@
241 239 // Owner-lock for `upload` placements: only the stored file's
242 240 // owner may move them — folder-share write capability does NOT
243 241 // extend to uploaded files (recipients are read + download
244 242 // only; see stored-files-store.php).
245 - $upload_lock = desktop_mode_files_upload_owner_lock( $row, $user_id );
243 + $upload_lock = openstation_files_upload_owner_lock( $row, $user_id );
246 244 if ( is_wp_error( $upload_lock ) ) {
247 245 return $upload_lock;
248 246 }
249 247
@@ -253,17 +251,17 @@
253 251 // every icon in it, regardless of which user originally placed
254 252 // the row (shared-namespace semantics).
255 253 $is_row_owner = (int) $row['owner_id'] === $user_id;
256 254 if ( (int) $row['parent_id'] > 0 ) {
257 - $source_folder = desktop_mode_files_get_folder( (int) $row['parent_id'] );
255 + $source_folder = openstation_files_get_folder( (int) $row['parent_id'] );
258 256 if ( $source_folder ) {
259 257 $is_folder_owner = (int) $source_folder['owner_id'] === $user_id;
260 - $source_cap = function_exists( 'desktop_mode_folder_share_user_capability' )
261 - ? 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 )
262 260 : 'none';
263 261 if ( ! $is_row_owner && ! $is_folder_owner && 'write' !== $source_cap ) {
264 262 return new WP_Error(
265 - 'desktop_mode_files_no_write_in_shared_folder',
263 + 'openstation_files_no_write_in_shared_folder',
266 264 __( 'You only have read access to this folder.', 'desktop-mode' ),
267 265 array( 'status' => 403 )
268 266 );
269 267 }
@@ -269,9 +267,9 @@
269 267 }
270 268 // Folder reader on their own row inside the folder — still no.
271 269 if ( $is_row_owner && ! $is_folder_owner && 'write' !== $source_cap ) {
272 270 return new WP_Error(
273 - 'desktop_mode_files_no_write_in_shared_folder',
271 + 'openstation_files_no_write_in_shared_folder',
274 272 __( 'You only have read access to this folder.', 'desktop-mode' ),
275 273 array( 'status' => 403 )
276 274 );
277 275 }
@@ -277,21 +275,21 @@
277 275 }
278 276 }
279 277 } elseif ( ! $is_row_owner ) {
280 278 // Row at root, viewer doesn't own it.
281 - 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 ) );
282 280 }
283 281 if ( isset( $changes['parent_id'] ) ) {
284 282 $target_parent = max( 0, (int) $changes['parent_id'] );
285 283 if ( $target_parent > 0 ) {
286 - $target = desktop_mode_files_get_folder( $target_parent );
284 + $target = openstation_files_get_folder( $target_parent );
287 285 if ( $target && (int) $target['owner_id'] !== $user_id ) {
288 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
289 - ? 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 )
290 288 : 'none';
291 289 if ( 'write' !== $cap ) {
292 290 return new WP_Error(
293 - 'desktop_mode_files_no_write_in_shared_folder',
291 + 'openstation_files_no_write_in_shared_folder',
294 292 __( 'You only have read access to that folder.', 'desktop-mode' ),
295 293 array( 'status' => 403 )
296 294 );
297 295 }
@@ -308,9 +306,9 @@
308 306 if ( 'folder' === (string) $row['file_type'] && $target_parent > 0 ) {
309 307 $moving_folder_id = (int) $row['file_ref'];
310 308 if ( $moving_folder_id > 0 ) {
311 309 if (
312 - desktop_mode_files_would_create_folder_cycle(
310 + openstation_files_would_create_folder_cycle(
313 311 $user_id,
314 312 $moving_folder_id,
315 313 $target_parent
316 314 )
@@ -315,9 +313,9 @@
315 313 $target_parent
316 314 )
317 315 ) {
318 316 return new WP_Error(
319 - 'desktop_mode_files_folder_cycle',
317 + 'openstation_files_folder_cycle',
320 318 __( 'A folder cannot be placed inside itself or one of its descendants.', 'desktop-mode' ),
321 319 array( 'status' => 409 )
322 320 );
323 321 }
@@ -324,9 +322,9 @@
324 322 }
325 323 }
326 324 }
327 325
328 - $tables = desktop_mode_files_table_names();
326 + $tables = openstation_files_table_names();
329 327 $set = array();
330 328 $fmt = array();
331 329
332 330 if ( isset( $changes['parent_id'] ) ) {
@@ -346,33 +344,31 @@
346 344 if ( empty( $set ) ) {
347 345 return true; // No-op.
348 346 }
349 347
350 - $set['updated_at_ms'] = desktop_mode_files_now_ms();
348 + $set['updated_at_ms'] = openstation_files_now_ms();
351 349 $fmt[] = '%d';
352 350 // Track who actually fired this mutation so a future
353 351 // `If-Match` 409 can name the session that won the race,
354 352 // not just whoever happens to own the row.
355 - $set['updated_by'] = $user_id;
356 - $fmt[] = '%d';
353 + $set['updated_by'] = $user_id;
354 + $fmt[] = '%d';
357 355
358 356 $ok = $wpdb->update( $tables['placements'], $set, array( 'id' => $placement_id ), $fmt, array( '%d' ) );
359 357 if ( false === $ok ) {
360 - 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 ) );
361 359 }
362 360
363 - $next = desktop_mode_files_get_placement( $placement_id );
361 + $next = openstation_files_get_placement( $placement_id );
364 362
365 363 /**
366 364 * Fires after a placement is moved / mutated.
367 365 *
368 - * @since 0.9.0
369 - *
370 366 * @param int $id Placement id.
371 367 * @param array $next Row after the change.
372 368 * @param array $prev Row before the change.
373 369 */
374 - do_action( 'desktop_mode_file_moved', $placement_id, $next, $row );
370 + do_action( 'openstation_file_moved', $placement_id, $next, $row );
375 371
376 372 return true;
377 373 }
378 374
@@ -378,26 +374,24 @@
378 374
379 375 /**
380 376 * Remove a placement. Writes a tombstone for Phase-6 sync.
381 377 *
382 - * @since 0.9.0
383 - *
384 378 * @param int $placement_id Placement id.
385 379 * @param int $user_id Acting user.
386 380 * @return true|WP_Error
387 381 */
388 -function desktop_mode_files_remove( $placement_id, $user_id ) {
382 +function openstation_files_remove( $placement_id, $user_id ) {
389 383 global $wpdb;
390 384
391 385 $placement_id = (int) $placement_id;
392 386 $user_id = (int) $user_id;
393 - $row = desktop_mode_files_get_placement( $placement_id );
387 + $row = openstation_files_get_placement( $placement_id );
394 388 if ( ! $row ) {
395 - 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 ) );
396 390 }
397 391 // Owner-lock for `upload` placements — removal is destructive
398 392 // for real bytes, so only the stored file's owner may do it.
399 - $upload_lock = desktop_mode_files_upload_owner_lock( $row, $user_id );
393 + $upload_lock = openstation_files_upload_owner_lock( $row, $user_id );
400 394 if ( is_wp_error( $upload_lock ) ) {
401 395 return $upload_lock;
402 396 }
403 397 // Same shared-namespace rule as the trash gate: owner of the
@@ -404,34 +398,32 @@
404 398 // row OR write cap on the parent folder.
405 399 $is_row_owner = (int) $row['owner_id'] === $user_id;
406 400 $allowed = $is_row_owner;
407 401 if ( ! $allowed && (int) $row['parent_id'] > 0 ) {
408 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
409 - ? 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 )
410 404 : 'none';
411 405 $allowed = 'write' === $cap;
412 406 }
413 407 if ( ! $allowed ) {
414 - 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 ) );
415 409 }
416 410
417 - $tables = desktop_mode_files_table_names();
411 + $tables = openstation_files_table_names();
418 412 $ok = $wpdb->delete( $tables['placements'], array( 'id' => $placement_id ), array( '%d' ) );
419 413 if ( false === $ok ) {
420 - 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 ) );
421 415 }
422 416
423 - desktop_mode_files_write_tombstone( 'placement', $placement_id );
417 + openstation_files_write_tombstone( 'placement', $placement_id );
424 418
425 419 /**
426 420 * Fires after a placement is removed.
427 421 *
428 - * @since 0.9.0
429 - *
430 422 * @param int $id Placement id.
431 423 * @param array $row Removed row.
432 424 */
433 - do_action( 'desktop_mode_file_unplaced', $placement_id, $row );
425 + do_action( 'openstation_file_unplaced', $placement_id, $row );
434 426
435 427 return true;
436 428 }
437 429
@@ -444,22 +436,20 @@
444 436 * `true` for every other file type, and falls back to the normal
445 437 * rules when the stored-file row is gone (dangling tiles must stay
446 438 * cleanable).
447 439 *
448 - * @since 0.9.6
449 - *
450 440 * @param array $row Placement row.
451 441 * @param int $user_id Acting user.
452 442 * @return true|WP_Error
453 443 */
454 -function desktop_mode_files_upload_owner_lock( $row, $user_id ) {
444 +function openstation_files_upload_owner_lock( $row, $user_id ) {
455 445 if ( ! is_array( $row ) || 'upload' !== (string) ( $row['file_type'] ?? '' ) ) {
456 446 return true;
457 447 }
458 - if ( ! function_exists( 'desktop_mode_stored_files_get' ) ) {
448 + if ( ! function_exists( 'openstation_stored_files_get' ) ) {
459 449 return true;
460 450 }
461 - $stored = desktop_mode_stored_files_get( (int) $row['file_ref'] );
451 + $stored = openstation_stored_files_get( (int) $row['file_ref'] );
462 452 if ( ! $stored ) {
463 453 return true;
464 454 }
465 455 if ( (int) $stored['owner_id'] === (int) $user_id ) {
@@ -465,9 +455,9 @@
465 455 if ( (int) $stored['owner_id'] === (int) $user_id ) {
466 456 return true;
467 457 }
468 458 return new WP_Error(
469 - 'desktop_mode_files_upload_owner_locked',
459 + 'openstation_files_upload_owner_locked',
470 460 __( 'Only the file’s owner can move or delete an uploaded file.', 'desktop-mode' ),
471 461 array( 'status' => 403 )
472 462 );
473 463 }
@@ -474,16 +464,14 @@
474 464
475 465 /**
476 466 * Read a single placement row by id.
477 467 *
478 - * @since 0.9.0
479 - *
480 468 * @param int $placement_id Placement id.
481 469 * @return array|null
482 470 */
483 -function desktop_mode_files_get_placement( $placement_id ) {
471 +function openstation_files_get_placement( $placement_id ) {
484 472 global $wpdb;
485 - $tables = desktop_mode_files_table_names();
473 + $tables = openstation_files_table_names();
486 474 $row = $wpdb->get_row(
487 475 $wpdb->prepare( "SELECT * FROM {$tables['placements']} WHERE id = %d", (int) $placement_id ),
488 476 ARRAY_A
489 477 );
@@ -489,23 +477,21 @@
489 477 );
490 478 if ( ! $row ) {
491 479 return null;
492 480 }
493 - return desktop_mode_files_normalize_placement_row( $row );
481 + return openstation_files_normalize_placement_row( $row );
494 482 }
495 483
496 484 /**
497 485 * List placements for a user under a given folder (0 = desktop
498 - * root). Honors the `desktop_mode_files_query_args` filter and
486 + * root). Honors the `openstation_files_query_args` filter and
499 487 * applies the file-type's `can_read()` per row.
500 488 *
501 - * @since 0.9.0
502 - *
503 489 * @param int $user_id Viewer.
504 490 * @param int $parent_id Folder id (0 for desktop root).
505 491 * @return array[]
506 492 */
507 -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 ) {
508 494 global $wpdb;
509 495 $user_id = (int) $user_id;
510 496 $parent_id = max( 0, (int) $parent_id );
511 497 if ( $user_id <= 0 ) {
@@ -511,9 +497,9 @@
511 497 if ( $user_id <= 0 ) {
512 498 return array();
513 499 }
514 500
515 - $tables = desktop_mode_files_table_names();
501 + $tables = openstation_files_table_names();
516 502
517 503 // Access gate + shared-namespace decision for non-root folders.
518 504 // Desktop root (parent_id = 0) is always per-user. For sub-
519 505 // folders, the contents of a SHARED folder are visible to every
@@ -520,15 +506,15 @@
520 506 // user who has at least 'read' on it — the icons inside belong
521 507 // to the folder, not to the user who originally placed them.
522 508 $share_view = false;
523 509 if ( $parent_id > 0 ) {
524 - $folder = desktop_mode_files_get_folder( $parent_id );
510 + $folder = openstation_files_get_folder( $parent_id );
525 511 if ( ! $folder ) {
526 512 return array();
527 513 }
528 514 if ( (int) $folder['owner_id'] !== $user_id ) {
529 - $cap = function_exists( 'desktop_mode_folder_share_user_capability' )
530 - ? 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 )
531 517 : 'none';
532 518 if ( 'none' === $cap ) {
533 519 return array();
534 520 }
@@ -543,23 +529,22 @@
543 529 );
544 530 /**
545 531 * Filter the args used to read placements.
546 532 *
547 - * @since 0.9.0
548 - *
549 533 * @param array $args Defaults: `{ user_id, parent_id, share_view }`.
550 534 * @param int $user_id Viewer.
551 535 * @param int $parent_id Folder id.
552 536 */
553 - $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 );
554 538
555 539 // Active queries always exclude trashed rows. Recycle-bin
556 540 // callers reach for the dedicated trash store.
557 - if ( ! empty( $args['share_view'] ) ) {
558 - // Shared sub-folder — return every placement in the folder
559 - // regardless of which user originally placed it. The icons
560 - // are part of the folder; the owner_id column is audit info,
561 - // 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.
562 547 $rows = $wpdb->get_results(
563 548 $wpdb->prepare(
564 549 "SELECT * FROM {$tables['placements']}
565 550 WHERE parent_id = %d
@@ -587,18 +572,18 @@
587 572 return array();
588 573 }
589 574 $out = array();
590 575 foreach ( $rows as $row ) {
591 - $normalized = desktop_mode_files_normalize_placement_row( $row );
592 - $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'] );
593 578 if ( empty( $args['share_view'] ) ) {
594 - // Private folder / desktop root — keep the existing
595 - // per-row read filter so stale/inaccessible entities
596 - // 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.
597 582 if ( $file && ! $file->can_read( $user_id ) ) {
598 583 continue;
599 584 }
600 - } else {
585 + } elseif ( $file && ! $file->can_read( $user_id ) ) {
601 586 // Shared folder view — every placement the OWNER chose
602 587 // to include is surfaced to the recipient. When the
603 588 // recipient lacks read on the underlying entity, we
604 589 // mark the row as `access_gated` so the tile renderer
@@ -605,11 +590,9 @@
605 590 // can paint a lock overlay + tooltip + intercept the
606 591 // open. Entity-level access enforcement still happens
607 592 // at open time in each opener — this flag is just the
608 593 // pre-emptive visual cue.
609 - if ( $file && ! $file->can_read( $user_id ) ) {
610 - $normalized['access_gated'] = true;
611 - }
594 + $normalized['access_gated'] = true;
612 595 }
613 596 $out[] = $normalized;
614 597 }
615 598 return $out;
@@ -622,9 +605,9 @@
622 605 * 1. Folders the viewer owns that have no placement anywhere.
623 606 * (Pre-fix folder-create flow could leak these; new flow
624 607 * writes the placement atomically.)
625 608 *
626 - * 2. Plugin shortcuts (`desktop_mode_register_icon()`) the
609 + * 2. Plugin shortcuts (`openstation_register_icon()`) the
627 610 * viewer hasn't placed yet. The unified-rail merge means
628 611 * every registered icon shows up as a `shortcut` placement
629 612 * on first hydrate so plugin shortcuts behave like any
630 613 * other tile (drag, sort, right-click, clean up).
@@ -629,20 +612,20 @@
629 612 * on first hydrate so plugin shortcuts behave like any
630 613 * other tile (drag, sort, right-click, clean up).
631 614 *
632 615 * Idempotent on both axes: a folder/shortcut that already has
633 - * any placement is left alone. Coordinates use the column-major
634 - * 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.
635 620 *
636 621 * Called by the placements list endpoint when the requested
637 622 * folder is the root (`parent_id=0`).
638 623 *
639 - * @since 0.9.0
640 - *
641 624 * @param int $user_id Viewer.
642 625 * @return int Total number of orphans that were auto-placed.
643 626 */
644 -function desktop_mode_files_auto_place_orphans( $user_id ) {
627 +function openstation_files_auto_place_orphans( $user_id ) {
645 628 global $wpdb;
646 629 $user_id = (int) $user_id;
647 630 if ( $user_id <= 0 ) {
648 631 return 0;
@@ -647,9 +630,9 @@
647 630 if ( $user_id <= 0 ) {
648 631 return 0;
649 632 }
650 633
651 - $tables = desktop_mode_files_table_names();
634 + $tables = openstation_files_table_names();
652 635
653 636 // 1) Owned folders without any placement. Skip trashed folders
654 637 // and trashed placement rows so a recycled folder doesn't get
655 638 // auto-placed back on the desktop on next hydrate.
@@ -668,20 +651,20 @@
668 651 ARRAY_A
669 652 );
670 653
671 654 // 2) Registered plugin shortcuts the viewer hasn't placed yet.
672 - // Pull the registered ids first, then ask the placements
673 - // table which the viewer already has — set difference
674 - // yields the orphans without a heavy join.
675 - $shortcut_ids = array();
676 - $registry = function_exists( 'desktop_mode_desktop_icon_registry' )
677 - ? 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()
678 661 : array();
679 662 if ( is_array( $registry ) ) {
680 - // Run through the same `desktop_mode_icons` filter the
663 + // Run through the same `openstation_icons` filter the
681 664 // build-payload path uses so plugins (and tests) can inject
682 665 // virtual entries.
683 - $registry = (array) apply_filters( 'desktop_mode_icons', $registry );
666 + $registry = (array) apply_filters( 'openstation_icons', $registry );
684 667 }
685 668 if ( is_array( $registry ) && ! empty( $registry ) ) {
686 669 $registered_ids = array_map( 'strval', array_keys( $registry ) );
687 670 $placeholders = implode( ',', array_fill( 0, count( $registered_ids ), '%s' ) );
@@ -695,9 +678,9 @@
695 678 AND file_ref IN ($placeholders)",
696 679 $args
697 680 )
698 681 );
699 - $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
682 + $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
700 683 foreach ( $registered_ids as $id ) {
701 684 if ( ! isset( $placed_set[ $id ] ) ) {
702 685 $shortcut_ids[] = $id;
703 686 }
@@ -709,10 +692,11 @@
709 692 }
710 693
711 694 // Build an occupied set from EXISTING root placements so
712 695 // we never drop an orphan on top of a tile the user
713 - // already has. Cell math mirrors `src/desktop-files/grid.ts`
714 - // (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`.
715 699 $existing = $wpdb->get_results(
716 700 $wpdb->prepare(
717 701 "SELECT x, y FROM {$tables['placements']}
718 702 WHERE owner_id = %d
@@ -721,47 +705,33 @@
721 705 $user_id
722 706 ),
723 707 ARRAY_A
724 708 );
725 - $occupied = array();
726 - foreach ( (array) $existing as $row ) {
727 - $col = max( 0, (int) round( ( (int) $row['x'] - 16 ) / 96 ) );
728 - $row_idx = max( 0, (int) round( ( (int) $row['y'] - 16 ) / 110 ) );
729 - $occupied[ "$col,$row_idx" ] = true;
730 - }
709 + $occupied = openstation_files_grid_occupied( $existing );
731 710
732 - $find_next = function () use ( &$occupied ) {
733 - for ( $col = 0; $col < 999; $col++ ) {
734 - for ( $row = 0; $row < 999; $row++ ) {
735 - $key = "$col,$row";
736 - if ( ! isset( $occupied[ $key ] ) ) {
737 - $occupied[ $key ] = true;
738 - return array( $col, $row );
739 - }
740 - }
741 - }
742 - return array( 0, 0 );
743 - };
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 );
744 717
745 - $placed = 0;
718 + $placed = 0;
746 719 $emit_at = function ( $type, $ref, $col, $row ) use ( $user_id, &$occupied, &$placed ) {
747 720 $occupied[ "$col,$row" ] = true;
748 - $result = desktop_mode_files_place(
721 + $result = openstation_files_place(
749 722 $user_id,
750 723 0,
751 724 $type,
752 725 (string) $ref,
753 - array(
754 - 'x' => 16 + $col * 96,
755 - 'y' => 16 + $row * 110,
756 - )
726 + openstation_files_grid_cell_to_point( $col, $row )
757 727 );
758 728 if ( ! is_wp_error( $result ) ) {
759 - $placed++;
729 + ++$placed;
760 730 }
761 731 };
762 - $emit_next = function ( $type, $ref ) use ( $find_next, $emit_at ) {
763 - 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 );
764 734 $emit_at( $type, $ref, $col, $row );
765 735 };
766 736
767 737 // Pinned shortcuts get reserved top-left slots. Anchored to
@@ -786,9 +756,9 @@
786 756 // client-side override anyway, but a future cleanup pass
787 757 // can compact the column.
788 758 $occupied[ "0,$pinned_idx" ] = true;
789 759 $emit_at( 'shortcut', $id, 0, $pinned_idx );
790 - $pinned_idx++;
760 + ++$pinned_idx;
791 761 }
792 762
793 763 foreach ( $folder_rows as $row ) {
794 764 $emit_next( 'folder', $row['id'] );
@@ -804,15 +774,15 @@
804 774
805 775 /**
806 776 * Backwards-compat alias for the older folder-only name.
807 777 *
808 - * @deprecated 0.9.0 Use {@see desktop_mode_files_auto_place_orphans}.
778 + * @deprecated Use {@see openstation_files_auto_place_orphans}.
809 779 *
810 780 * @param int $user_id Viewer.
811 781 * @return int
812 782 */
813 -function desktop_mode_files_auto_place_orphan_folders( $user_id ) {
814 - 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 );
815 785 }
816 786
817 787 /**
818 788 * Coerce wpdb's stringly-typed row into typed values + decoded
@@ -817,15 +787,14 @@
817 787 /**
818 788 * Coerce wpdb's stringly-typed row into typed values + decoded
819 789 * meta. Internal helper.
820 790 *
821 - * @since 0.9.0
822 791 * @internal
823 792 *
824 793 * @param array $row Raw wpdb row.
825 794 * @return array
826 795 */
827 -function desktop_mode_files_normalize_placement_row( $row ) {
796 +function openstation_files_normalize_placement_row( $row ) {
828 797 $meta_raw = isset( $row['meta'] ) ? (string) $row['meta'] : '';
829 798 $meta = '' !== $meta_raw ? json_decode( $meta_raw, true ) : null;
830 799 return array(
831 800 'id' => (int) $row['id'],
@@ -830,9 +799,9 @@
830 799 return array(
831 800 'id' => (int) $row['id'],
832 801 'owner_id' => (int) $row['owner_id'],
833 802 // `updated_by` is v10. Null on legacy rows — callers that
834 - // need the actor (e.g. `desktop_mode_files_check_if_match`)
803 + // need the actor (e.g. `openstation_files_check_if_match`)
835 804 // fall back to `owner_id` when this is null/missing.
836 805 'updated_by' => isset( $row['updated_by'] ) ? (int) $row['updated_by'] : null,
837 806 'parent_id' => (int) $row['parent_id'],
838 807 'file_type' => (string) $row['file_type'],
@@ -851,34 +820,32 @@
851 820 * Invariant (enforced by callers): tombstones may exist only for
852 821 * ids of PERMANENTLY-DELETED rows. Never write one for a soft-
853 822 * trashed row — soft-trash is reversible and the heartbeat already
854 823 * surfaces it via the `trashed_at_ms IS NOT NULL` query in
855 - * `desktop_mode_files_compute_heartbeat_delta`. A tombstone on a
824 + * `openstation_files_compute_heartbeat_delta`. A tombstone on a
856 825 * soft-trashed row lingers past restore and tells clients the row
857 826 * is gone while it is in fact alive — see the "shared folder
858 - * disappears on refresh" bug fixed in 0.8.5.
827 + * disappears on refresh" bug.
859 828 *
860 - * Pair every revival path (`desktop_mode_files_restore_placement`,
861 - * `desktop_mode_files_restore_folder`, and the duplicate-key
862 - * revival branch in `desktop_mode_files_place`) with
863 - * {@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
864 833 * back to life never carries lingering tombstones from a previous
865 834 * removal that turned out to be reversible.
866 835 *
867 - * @since 0.9.0
868 - *
869 836 * @param string $kind 'placement' | 'folder'.
870 837 * @param int $ref Removed id.
871 838 */
872 -function desktop_mode_files_write_tombstone( $kind, $ref ) {
839 +function openstation_files_write_tombstone( $kind, $ref ) {
873 840 global $wpdb;
874 - $tables = desktop_mode_files_table_names();
841 + $tables = openstation_files_table_names();
875 842 $wpdb->insert(
876 843 $tables['tombstones'],
877 844 array(
878 845 'kind' => (string) $kind,
879 846 'ref_id' => (int) $ref,
880 - 'removed_at_ms' => desktop_mode_files_now_ms(),
847 + 'removed_at_ms' => openstation_files_now_ms(),
881 848 ),
882 849 array( '%s', '%d', '%d' )
883 850 );
884 851 }
@@ -890,20 +857,18 @@
890 857 * previous removal that turned out to be reversible.
891 858 *
892 859 * Idempotent — running it on a ref with no tombstones is a no-op.
893 860 *
894 - * @since 0.8.5
895 - *
896 861 * @param string $kind 'placement' | 'folder'.
897 862 * @param int $ref_id Row id whose tombstones should be dropped.
898 863 */
899 -function desktop_mode_files_clear_tombstones_for( $kind, $ref_id ) {
864 +function openstation_files_clear_tombstones_for( $kind, $ref_id ) {
900 865 global $wpdb;
901 866 $ref_id = (int) $ref_id;
902 867 if ( $ref_id <= 0 ) {
903 868 return;
904 869 }
905 - $tables = desktop_mode_files_table_names();
870 + $tables = openstation_files_table_names();
906 871 $wpdb->delete(
907 872 $tables['tombstones'],
908 873 array(
909 874 'kind' => (string) $kind,
@@ -917,32 +882,28 @@
917 882 * Daily prune of tombstones older than 7 days. Phase 6 may tune
918 883 * the retention window when the Heartbeat sync lands; for now 7d
919 884 * is plenty since a client that's been offline that long will
920 885 * always need a full REST resync anyway.
921 - *
922 - * @since 0.9.0
923 886 */
924 -function desktop_mode_files_prune_tombstones() {
887 +function openstation_files_prune_tombstones() {
925 888 global $wpdb;
926 - $tables = desktop_mode_files_table_names();
927 - $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 );
928 891 $wpdb->query( $wpdb->prepare( "DELETE FROM {$tables['tombstones']} WHERE removed_at_ms < %d", $cutoff ) );
929 892 }
930 -add_action( 'desktop_mode_files_daily_prune', 'desktop_mode_files_prune_tombstones' );
893 +add_action( 'desktop_mode_files_daily_prune', 'openstation_files_prune_tombstones' );
931 894
932 895 /**
933 896 * Schedule the daily prune. Hooked on `init` and idempotent via
934 897 * wp_next_scheduled(), so a manual file-copy install (no activation
935 898 * hook) still gets the cron event.
936 - *
937 - * @since 0.9.0
938 899 */
939 -function desktop_mode_files_schedule_prune() {
900 +function openstation_files_schedule_prune() {
940 901 if ( ! wp_next_scheduled( 'desktop_mode_files_daily_prune' ) ) {
941 902 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'desktop_mode_files_daily_prune' );
942 903 }
943 904 }
944 -add_action( 'init', 'desktop_mode_files_schedule_prune' );
905 +add_action( 'init', 'openstation_files_schedule_prune' );
945 906
946 907 /**
947 908 * Walk the folder-parentage chain upward from `$target_parent_id` and
948 909 * return `true` when `$moving_folder_id` appears anywhere in it —
@@ -960,16 +921,14 @@
960 921 * Defends against pre-existing cycles in the data: if we re-visit a
961 922 * cursor we've already seen, we treat it as a cycle and reject, so a
962 923 * corrupted history can't drive this function into an infinite loop.
963 924 *
964 - * @since 0.8.6
965 - *
966 925 * @param int $user_id Acting user.
967 926 * @param int $moving_folder_id Folder being moved (its `folders.id`).
968 927 * @param int $target_parent_id New container folder id (0 = desktop root).
969 928 * @return bool True when the move would create a cycle.
970 929 */
971 -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 ) {
972 931 $moving_folder_id = (int) $moving_folder_id;
973 932 $target_parent_id = (int) $target_parent_id;
974 933 $user_id = (int) $user_id;
975 934 if ( $moving_folder_id <= 0 || $target_parent_id <= 0 || $user_id <= 0 ) {
@@ -978,9 +937,9 @@
978 937 if ( $moving_folder_id === $target_parent_id ) {
979 938 return true;
980 939 }
981 940 global $wpdb;
982 - $tables = desktop_mode_files_table_names();
941 + $tables = openstation_files_table_names();
983 942 $visited = array();
984 943 $cursor = $target_parent_id;
985 944 // Hard cap to defend against catastrophically deep trees too —
986 945 // real installs won't approach 256.