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 +154 -196 0.9.71.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,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,15 +529,13 @@
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 541 if ( ! empty( $args['share_view'] ) ) {
@@ -587,10 +571,10 @@
587 571 return array();
588 572 }
589 573 $out = array();
590 574 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'] );
575 + $normalized = openstation_files_normalize_placement_row( $row );
576 + $file = openstation_resolve_file( $normalized['file_type'], $normalized['file_ref'] );
593 577 if ( empty( $args['share_view'] ) ) {
594 578 // Private folder / desktop root — keep the existing
595 579 // per-row read filter so stale/inaccessible entities
596 580 // don't clutter the user's own view.
@@ -596,9 +580,9 @@
596 580 // don't clutter the user's own view.
597 581 if ( $file && ! $file->can_read( $user_id ) ) {
598 582 continue;
599 583 }
600 - } else {
584 + } elseif ( $file && ! $file->can_read( $user_id ) ) {
601 585 // Shared folder view — every placement the OWNER chose
602 586 // to include is surfaced to the recipient. When the
603 587 // recipient lacks read on the underlying entity, we
604 588 // mark the row as `access_gated` so the tile renderer
@@ -605,11 +589,9 @@
605 589 // can paint a lock overlay + tooltip + intercept the
606 590 // open. Entity-level access enforcement still happens
607 591 // at open time in each opener — this flag is just the
608 592 // pre-emptive visual cue.
609 - if ( $file && ! $file->can_read( $user_id ) ) {
610 - $normalized['access_gated'] = true;
611 - }
593 + $normalized['access_gated'] = true;
612 594 }
613 595 $out[] = $normalized;
614 596 }
615 597 return $out;
@@ -622,9 +604,9 @@
622 604 * 1. Folders the viewer owns that have no placement anywhere.
623 605 * (Pre-fix folder-create flow could leak these; new flow
624 606 * writes the placement atomically.)
625 607 *
626 - * 2. Plugin shortcuts (`desktop_mode_register_icon()`) the
608 + * 2. Plugin shortcuts (`openstation_register_icon()`) the
627 609 * viewer hasn't placed yet. The unified-rail merge means
628 610 * every registered icon shows up as a `shortcut` placement
629 611 * on first hydrate so plugin shortcuts behave like any
630 612 * other tile (drag, sort, right-click, clean up).
@@ -629,20 +611,20 @@
629 611 * on first hydrate so plugin shortcuts behave like any
630 612 * other tile (drag, sort, right-click, clean up).
631 613 *
632 614 * 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.
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.
635 619 *
636 620 * Called by the placements list endpoint when the requested
637 621 * folder is the root (`parent_id=0`).
638 622 *
639 - * @since 0.9.0
640 - *
641 623 * @param int $user_id Viewer.
642 624 * @return int Total number of orphans that were auto-placed.
643 625 */
644 -function desktop_mode_files_auto_place_orphans( $user_id ) {
626 +function openstation_files_auto_place_orphans( $user_id ) {
645 627 global $wpdb;
646 628 $user_id = (int) $user_id;
647 629 if ( $user_id <= 0 ) {
648 630 return 0;
@@ -647,9 +629,9 @@
647 629 if ( $user_id <= 0 ) {
648 630 return 0;
649 631 }
650 632
651 - $tables = desktop_mode_files_table_names();
633 + $tables = openstation_files_table_names();
652 634
653 635 // 1) Owned folders without any placement. Skip trashed folders
654 636 // and trashed placement rows so a recycled folder doesn't get
655 637 // auto-placed back on the desktop on next hydrate.
@@ -668,20 +650,20 @@
668 650 ARRAY_A
669 651 );
670 652
671 653 // 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()
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()
678 660 : array();
679 661 if ( is_array( $registry ) ) {
680 - // Run through the same `desktop_mode_icons` filter the
662 + // Run through the same `openstation_icons` filter the
681 663 // build-payload path uses so plugins (and tests) can inject
682 664 // virtual entries.
683 - $registry = (array) apply_filters( 'desktop_mode_icons', $registry );
665 + $registry = (array) apply_filters( 'openstation_icons', $registry );
684 666 }
685 667 if ( is_array( $registry ) && ! empty( $registry ) ) {
686 668 $registered_ids = array_map( 'strval', array_keys( $registry ) );
687 669 $placeholders = implode( ',', array_fill( 0, count( $registered_ids ), '%s' ) );
@@ -695,9 +677,9 @@
695 677 AND file_ref IN ($placeholders)",
696 678 $args
697 679 )
698 680 );
699 - $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
681 + $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) );
700 682 foreach ( $registered_ids as $id ) {
701 683 if ( ! isset( $placed_set[ $id ] ) ) {
702 684 $shortcut_ids[] = $id;
703 685 }
@@ -709,10 +691,11 @@
709 691 }
710 692
711 693 // Build an occupied set from EXISTING root placements so
712 694 // 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).
695 + // already has. Cell math lives in
696 + // `includes/desktop-files/grid.php`, the mirror of
697 + // `src/desktop-files/grid.ts`.
715 698 $existing = $wpdb->get_results(
716 699 $wpdb->prepare(
717 700 "SELECT x, y FROM {$tables['placements']}
718 701 WHERE owner_id = %d
@@ -721,47 +704,33 @@
721 704 $user_id
722 705 ),
723 706 ARRAY_A
724 707 );
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 - }
708 + $occupied = openstation_files_grid_occupied( $existing );
731 709
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 - };
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 );
744 716
745 - $placed = 0;
717 + $placed = 0;
746 718 $emit_at = function ( $type, $ref, $col, $row ) use ( $user_id, &$occupied, &$placed ) {
747 719 $occupied[ "$col,$row" ] = true;
748 - $result = desktop_mode_files_place(
720 + $result = openstation_files_place(
749 721 $user_id,
750 722 0,
751 723 $type,
752 724 (string) $ref,
753 - array(
754 - 'x' => 16 + $col * 96,
755 - 'y' => 16 + $row * 110,
756 - )
725 + openstation_files_grid_cell_to_point( $col, $row )
757 726 );
758 727 if ( ! is_wp_error( $result ) ) {
759 - $placed++;
728 + ++$placed;
760 729 }
761 730 };
762 - $emit_next = function ( $type, $ref ) use ( $find_next, $emit_at ) {
763 - 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 );
764 733 $emit_at( $type, $ref, $col, $row );
765 734 };
766 735
767 736 // Pinned shortcuts get reserved top-left slots. Anchored to
@@ -786,9 +755,9 @@
786 755 // client-side override anyway, but a future cleanup pass
787 756 // can compact the column.
788 757 $occupied[ "0,$pinned_idx" ] = true;
789 758 $emit_at( 'shortcut', $id, 0, $pinned_idx );
790 - $pinned_idx++;
759 + ++$pinned_idx;
791 760 }
792 761
793 762 foreach ( $folder_rows as $row ) {
794 763 $emit_next( 'folder', $row['id'] );
@@ -804,15 +773,15 @@
804 773
805 774 /**
806 775 * Backwards-compat alias for the older folder-only name.
807 776 *
808 - * @deprecated 0.9.0 Use {@see desktop_mode_files_auto_place_orphans}.
777 + * @deprecated Use {@see openstation_files_auto_place_orphans}.
809 778 *
810 779 * @param int $user_id Viewer.
811 780 * @return int
812 781 */
813 -function desktop_mode_files_auto_place_orphan_folders( $user_id ) {
814 - 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 );
815 784 }
816 785
817 786 /**
818 787 * Coerce wpdb's stringly-typed row into typed values + decoded
@@ -817,15 +786,14 @@
817 786 /**
818 787 * Coerce wpdb's stringly-typed row into typed values + decoded
819 788 * meta. Internal helper.
820 789 *
821 - * @since 0.9.0
822 790 * @internal
823 791 *
824 792 * @param array $row Raw wpdb row.
825 793 * @return array
826 794 */
827 -function desktop_mode_files_normalize_placement_row( $row ) {
795 +function openstation_files_normalize_placement_row( $row ) {
828 796 $meta_raw = isset( $row['meta'] ) ? (string) $row['meta'] : '';
829 797 $meta = '' !== $meta_raw ? json_decode( $meta_raw, true ) : null;
830 798 return array(
831 799 'id' => (int) $row['id'],
@@ -830,9 +798,9 @@
830 798 return array(
831 799 'id' => (int) $row['id'],
832 800 'owner_id' => (int) $row['owner_id'],
833 801 // `updated_by` is v10. Null on legacy rows — callers that
834 - // need the actor (e.g. `desktop_mode_files_check_if_match`)
802 + // need the actor (e.g. `openstation_files_check_if_match`)
835 803 // fall back to `owner_id` when this is null/missing.
836 804 'updated_by' => isset( $row['updated_by'] ) ? (int) $row['updated_by'] : null,
837 805 'parent_id' => (int) $row['parent_id'],
838 806 'file_type' => (string) $row['file_type'],
@@ -851,34 +819,32 @@
851 819 * Invariant (enforced by callers): tombstones may exist only for
852 820 * ids of PERMANENTLY-DELETED rows. Never write one for a soft-
853 821 * trashed row — soft-trash is reversible and the heartbeat already
854 822 * surfaces it via the `trashed_at_ms IS NOT NULL` query in
855 - * `desktop_mode_files_compute_heartbeat_delta`. A tombstone on a
823 + * `openstation_files_compute_heartbeat_delta`. A tombstone on a
856 824 * soft-trashed row lingers past restore and tells clients the row
857 825 * is gone while it is in fact alive — see the "shared folder
858 - * disappears on refresh" bug fixed in 0.8.5.
826 + * disappears on refresh" bug.
859 827 *
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
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
864 832 * back to life never carries lingering tombstones from a previous
865 833 * removal that turned out to be reversible.
866 834 *
867 - * @since 0.9.0
868 - *
869 835 * @param string $kind 'placement' | 'folder'.
870 836 * @param int $ref Removed id.
871 837 */
872 -function desktop_mode_files_write_tombstone( $kind, $ref ) {
838 +function openstation_files_write_tombstone( $kind, $ref ) {
873 839 global $wpdb;
874 - $tables = desktop_mode_files_table_names();
840 + $tables = openstation_files_table_names();
875 841 $wpdb->insert(
876 842 $tables['tombstones'],
877 843 array(
878 844 'kind' => (string) $kind,
879 845 'ref_id' => (int) $ref,
880 - 'removed_at_ms' => desktop_mode_files_now_ms(),
846 + 'removed_at_ms' => openstation_files_now_ms(),
881 847 ),
882 848 array( '%s', '%d', '%d' )
883 849 );
884 850 }
@@ -890,20 +856,18 @@
890 856 * previous removal that turned out to be reversible.
891 857 *
892 858 * Idempotent — running it on a ref with no tombstones is a no-op.
893 859 *
894 - * @since 0.8.5
895 - *
896 860 * @param string $kind 'placement' | 'folder'.
897 861 * @param int $ref_id Row id whose tombstones should be dropped.
898 862 */
899 -function desktop_mode_files_clear_tombstones_for( $kind, $ref_id ) {
863 +function openstation_files_clear_tombstones_for( $kind, $ref_id ) {
900 864 global $wpdb;
901 865 $ref_id = (int) $ref_id;
902 866 if ( $ref_id <= 0 ) {
903 867 return;
904 868 }
905 - $tables = desktop_mode_files_table_names();
869 + $tables = openstation_files_table_names();
906 870 $wpdb->delete(
907 871 $tables['tombstones'],
908 872 array(
909 873 'kind' => (string) $kind,
@@ -917,32 +881,28 @@
917 881 * Daily prune of tombstones older than 7 days. Phase 6 may tune
918 882 * the retention window when the Heartbeat sync lands; for now 7d
919 883 * is plenty since a client that's been offline that long will
920 884 * always need a full REST resync anyway.
921 - *
922 - * @since 0.9.0
923 885 */
924 -function desktop_mode_files_prune_tombstones() {
886 +function openstation_files_prune_tombstones() {
925 887 global $wpdb;
926 - $tables = desktop_mode_files_table_names();
927 - $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 );
928 890 $wpdb->query( $wpdb->prepare( "DELETE FROM {$tables['tombstones']} WHERE removed_at_ms < %d", $cutoff ) );
929 891 }
930 -add_action( 'desktop_mode_files_daily_prune', 'desktop_mode_files_prune_tombstones' );
892 +add_action( 'desktop_mode_files_daily_prune', 'openstation_files_prune_tombstones' );
931 893
932 894 /**
933 895 * Schedule the daily prune. Hooked on `init` and idempotent via
934 896 * wp_next_scheduled(), so a manual file-copy install (no activation
935 897 * hook) still gets the cron event.
936 - *
937 - * @since 0.9.0
938 898 */
939 -function desktop_mode_files_schedule_prune() {
899 +function openstation_files_schedule_prune() {
940 900 if ( ! wp_next_scheduled( 'desktop_mode_files_daily_prune' ) ) {
941 901 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'desktop_mode_files_daily_prune' );
942 902 }
943 903 }
944 -add_action( 'init', 'desktop_mode_files_schedule_prune' );
904 +add_action( 'init', 'openstation_files_schedule_prune' );
945 905
946 906 /**
947 907 * Walk the folder-parentage chain upward from `$target_parent_id` and
948 908 * return `true` when `$moving_folder_id` appears anywhere in it —
@@ -960,16 +920,14 @@
960 920 * Defends against pre-existing cycles in the data: if we re-visit a
961 921 * cursor we've already seen, we treat it as a cycle and reject, so a
962 922 * corrupted history can't drive this function into an infinite loop.
963 923 *
964 - * @since 0.8.6
965 - *
966 924 * @param int $user_id Acting user.
967 925 * @param int $moving_folder_id Folder being moved (its `folders.id`).
968 926 * @param int $target_parent_id New container folder id (0 = desktop root).
969 927 * @return bool True when the move would create a cycle.
970 928 */
971 -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 ) {
972 930 $moving_folder_id = (int) $moving_folder_id;
973 931 $target_parent_id = (int) $target_parent_id;
974 932 $user_id = (int) $user_id;
975 933 if ( $moving_folder_id <= 0 || $target_parent_id <= 0 || $user_id <= 0 ) {
@@ -978,9 +936,9 @@
978 936 if ( $moving_folder_id === $target_parent_id ) {
979 937 return true;
980 938 }
981 939 global $wpdb;
982 - $tables = desktop_mode_files_table_names();
940 + $tables = openstation_files_table_names();
983 941 $visited = array();
984 942 $cursor = $target_parent_id;
985 943 // Hard cap to defend against catastrophically deep trees too —
986 944 // real installs won't approach 256.