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