PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
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 0.8.9 0.8.8 All 35 releases
← All changes | includes/desktop-files/folders-store.php +149 -141 0.8.9 → 1.1.11 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Folders store.
3 + * OpenStation — Folders store.
4 4 *
5 5 * CRUD primitives for the `_desktop_mode_folders` table. Folders
6 6 * are first-class files: they have an owner, a name, a share
7 7 * mode, and a JSON `share_meta` column carrying the user / role
@@ -6,20 +6,20 @@
6 6 * are first-class files: they have an owner, a name, a share
7 7 * mode, and a JSON `share_meta` column carrying the user / role
8 8 * lists when `share_mode` is `users` or `roles`.
9 9 *
10 - * Phase 2 ships private folders only. Phase 6 lights up the
11 - * other share modes plus the `desktop_mode_files_visible_folders`
12 - * filter that gates which folders a viewer can see.
10 + * Visibility beyond the owner is computed by sharing.php, which
11 + * hooks the `openstation_files_visible_folders` filter at
12 + * priority 5 to merge accepted shares and `share_mode='all'`
13 + * folders onto the owner's list.
13 14 *
14 - * @package WPDesktopMode
15 - * @since 0.9.0
15 + * @package OpenStation
16 16 */
17 17
18 18 defined( 'ABSPATH' ) || exit;
19 19
20 20 /** Allowed share-mode values. */
21 -function desktop_mode_files_share_modes() {
21 +function openstation_files_share_modes() {
22 22 $modes = array( 'private', 'users', 'roles', 'all' );
23 23 /**
24 24 * Filter the allowed `share_mode` values. Plugins can add
25 25 * (e.g. 'team') by registering both the value here and a
@@ -24,51 +24,57 @@
24 24 * Filter the allowed `share_mode` values. Plugins can add
25 25 * (e.g. 'team') by registering both the value here and a
26 26 * matching visibility callback.
27 27 *
28 - * @since 0.9.0
29 - *
30 28 * @param string[] $modes Default modes.
31 29 */
32 - return (array) apply_filters( 'desktop_mode_files_share_modes', $modes );
30 + return (array) apply_filters( 'openstation_files_share_modes', $modes );
33 31 }
34 32
35 33 /**
36 34 * Create a folder.
37 35 *
38 - * @since 0.9.0
39 - *
40 36 * @param int $owner_id Owner.
41 37 * @param array $args `name`, `share_mode`, `share_meta`.
42 38 * @return int|WP_Error Folder id on success.
43 39 */
44 -function desktop_mode_files_create_folder( $owner_id, $args = array() ) {
40 +function openstation_files_create_folder( $owner_id, $args = array() ) {
45 41 global $wpdb;
46 42
47 43 $owner_id = (int) $owner_id;
48 44 if ( $owner_id <= 0 ) {
49 - return new WP_Error( 'desktop_mode_files_invalid_user', __( 'A user id is required.', 'desktop-mode' ), array( 'status' => 400 ) );
45 + return new WP_Error( 'openstation_files_invalid_user', __( 'A user id is required.', 'desktop-mode' ), array( 'status' => 400 ) );
50 46 }
51 47
52 - $args = wp_parse_args( $args, array(
53 - 'name' => '',
54 - 'share_mode' => 'private',
55 - 'share_meta' => null,
56 - ) );
48 + $args = wp_parse_args(
49 + $args,
50 + array(
51 + 'name' => '',
52 + 'share_mode' => 'private',
53 + 'share_meta' => null,
54 + )
55 + );
57 56
58 57 $name = sanitize_text_field( (string) $args['name'] );
59 58 if ( '' === $name ) {
60 - return new WP_Error( 'desktop_mode_files_missing_name', __( 'Folder name is required.', 'desktop-mode' ), array( 'status' => 400 ) );
59 + return new WP_Error( 'openstation_files_missing_name', __( 'Folder name is required.', 'desktop-mode' ), array( 'status' => 400 ) );
61 60 }
62 61
63 62 $mode = (string) $args['share_mode'];
64 - $modes = desktop_mode_files_share_modes();
63 + $modes = openstation_files_share_modes();
65 64 if ( ! in_array( $mode, $modes, true ) ) {
66 - return new WP_Error( 'desktop_mode_files_invalid_share_mode', __( 'Invalid share mode.', 'desktop-mode' ), array( 'status' => 400, 'mode' => $mode ) );
65 + return new WP_Error(
66 + 'openstation_files_invalid_share_mode',
67 + __( 'Invalid share mode.', 'desktop-mode' ),
68 + array(
69 + 'status' => 400,
70 + 'mode' => $mode,
71 + )
72 + );
67 73 }
68 74
69 - $tables = desktop_mode_files_table_names();
70 - $now = desktop_mode_files_now_ms();
75 + $tables = openstation_files_table_names();
76 + $now = openstation_files_now_ms();
71 77 $row = array(
72 78 'owner_id' => $owner_id,
73 79 'name' => $name,
74 80 'share_mode' => $mode,
@@ -77,9 +83,9 @@
77 83 );
78 84
79 85 $ok = $wpdb->insert( $tables['folders'], $row, array( '%d', '%s', '%s', '%s', '%d' ) );
80 86 if ( false === $ok ) {
81 - return new WP_Error( 'desktop_mode_files_insert_failed', __( 'Failed to create folder.', 'desktop-mode' ), array( 'status' => 500 ) );
87 + return new WP_Error( 'openstation_files_insert_failed', __( 'Failed to create folder.', 'desktop-mode' ), array( 'status' => 500 ) );
82 88 }
83 89 $id = (int) $wpdb->insert_id;
84 90
85 91 $row['id'] = $id;
@@ -86,14 +92,12 @@
86 92
87 93 /**
88 94 * Fires after a folder is created.
89 95 *
90 - * @since 0.9.0
91 - *
92 96 * @param int $id Folder id.
93 97 * @param array $row Inserted row.
94 98 */
95 - do_action( 'desktop_mode_folder_created', $id, $row );
99 + do_action( 'openstation_folder_created', $id, $row );
96 100
97 101 return $id;
98 102 }
99 103
@@ -99,26 +103,24 @@
99 103
100 104 /**
101 105 * Update a folder. Only the owner can update for now.
102 106 *
103 - * @since 0.9.0
104 - *
105 107 * @param int $folder_id Folder id.
106 108 * @param int $user_id Acting user.
107 109 * @param array $changes `name`, `share_mode`, `share_meta`.
108 110 * @return true|WP_Error
109 111 */
110 -function desktop_mode_files_update_folder( $folder_id, $user_id, $changes = array() ) {
112 +function openstation_files_update_folder( $folder_id, $user_id, $changes = array() ) {
111 113 global $wpdb;
112 114
113 115 $folder_id = (int) $folder_id;
114 116 $user_id = (int) $user_id;
115 - $prev = desktop_mode_files_get_folder( $folder_id );
117 + $prev = openstation_files_get_folder( $folder_id );
116 118 if ( ! $prev ) {
117 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
119 + return new WP_Error( 'openstation_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
118 120 }
119 121 if ( (int) $prev['owner_id'] !== $user_id ) {
120 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot edit this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
122 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot edit this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
121 123 }
122 124
123 125 $set = array();
124 126 $fmt = array();
@@ -125,9 +127,9 @@
125 127
126 128 if ( isset( $changes['name'] ) ) {
127 129 $name = sanitize_text_field( (string) $changes['name'] );
128 130 if ( '' === $name ) {
129 - return new WP_Error( 'desktop_mode_files_missing_name', __( 'Folder name cannot be empty.', 'desktop-mode' ), array( 'status' => 400 ) );
131 + return new WP_Error( 'openstation_files_missing_name', __( 'Folder name cannot be empty.', 'desktop-mode' ), array( 'status' => 400 ) );
130 132 }
131 133 $set['name'] = $name;
132 134 $fmt[] = '%s';
133 135 }
@@ -132,11 +134,11 @@
132 134 $fmt[] = '%s';
133 135 }
134 136 if ( isset( $changes['share_mode'] ) ) {
135 137 $mode = (string) $changes['share_mode'];
136 - $modes = desktop_mode_files_share_modes();
138 + $modes = openstation_files_share_modes();
137 139 if ( ! in_array( $mode, $modes, true ) ) {
138 - return new WP_Error( 'desktop_mode_files_invalid_share_mode', __( 'Invalid share mode.', 'desktop-mode' ), array( 'status' => 400 ) );
140 + return new WP_Error( 'openstation_files_invalid_share_mode', __( 'Invalid share mode.', 'desktop-mode' ), array( 'status' => 400 ) );
139 141 }
140 142 $set['share_mode'] = $mode;
141 143 $fmt[] = '%s';
142 144 }
@@ -147,16 +149,16 @@
147 149 if ( empty( $set ) ) {
148 150 return true;
149 151 }
150 152
151 - $now = desktop_mode_files_now_ms();
153 + $now = openstation_files_now_ms();
152 154 $set['updated_at_ms'] = $now;
153 155 $fmt[] = '%d';
154 156
155 - $tables = desktop_mode_files_table_names();
157 + $tables = openstation_files_table_names();
156 158 $ok = $wpdb->update( $tables['folders'], $set, array( 'id' => $folder_id ), $fmt, array( '%d' ) );
157 159 if ( false === $ok ) {
158 - return new WP_Error( 'desktop_mode_files_update_failed', __( 'Failed to update folder.', 'desktop-mode' ), array( 'status' => 500 ) );
160 + return new WP_Error( 'openstation_files_update_failed', __( 'Failed to update folder.', 'desktop-mode' ), array( 'status' => 500 ) );
159 161 }
160 162
161 163 // Propagate rename to every placement that POINTS AT this folder
162 164 // (file_type='folder', file_ref=folder_id) by bumping their
@@ -179,16 +181,14 @@
179 181 * different `file_type` (e.g. an "alias" placement) can join
180 182 * the propagation by returning a non-null SQL fragment via
181 183 * this filter. Return `null` to opt OUT entirely (rare).
182 184 *
183 - * @since 0.18.x
184 - *
185 185 * @param string $where Default WHERE clause body.
186 186 * @param int $folder_id Folder being renamed.
187 187 * @param int $user_id Acting user (folder owner).
188 188 */
189 189 $where = (string) apply_filters(
190 - 'desktop_mode_folder_rename_bump_where',
190 + 'openstation_folder_rename_bump_where',
191 191 $wpdb->prepare(
192 192 "file_type = 'folder' AND file_ref = %s",
193 193 (string) $folder_id
194 194 ),
@@ -210,10 +210,8 @@
210 210 * placements have been bumped. Subscribers can react (e.g.
211 211 * dispatch their own cross-window broadcasts, refresh sidebar
212 212 * displays of the folder name).
213 213 *
214 - * @since 0.18.x
215 - *
216 214 * @param int $folder_id Folder id.
217 215 * @param string $new_name New name (sanitized).
218 216 * @param string $old_name Previous name.
219 217 * @param int $user_id Acting user (folder owner).
@@ -218,9 +216,9 @@
218 216 * @param string $old_name Previous name.
219 217 * @param int $user_id Acting user (folder owner).
220 218 */
221 219 do_action(
222 - 'desktop_mode_folder_renamed',
220 + 'openstation_folder_renamed',
223 221 $folder_id,
224 222 (string) $set['name'],
225 223 (string) $prev['name'],
226 224 $user_id
@@ -226,34 +224,30 @@
226 224 $user_id
227 225 );
228 226 }
229 227
230 - $next = desktop_mode_files_get_folder( $folder_id );
228 + $next = openstation_files_get_folder( $folder_id );
231 229
232 230 /**
233 231 * Fires after a folder is updated.
234 232 *
235 - * @since 0.9.0
236 - *
237 233 * @param int $id Folder id.
238 234 * @param array $next Row after.
239 235 * @param array $prev Row before.
240 236 */
241 - do_action( 'desktop_mode_folder_updated', $folder_id, $next, $prev );
237 + do_action( 'openstation_folder_updated', $folder_id, $next, $prev );
242 238
243 239 if ( isset( $changes['share_mode'] ) || array_key_exists( 'share_meta', $changes ) ) {
244 240 /**
245 241 * Fires after a folder's share state changes (mode or
246 242 * meta). Plugins listening for sharing events can subscribe
247 - * to this rather than diff `desktop_mode_folder_updated`.
243 + * to this rather than diff `openstation_folder_updated`.
248 244 *
249 - * @since 0.9.0
250 - *
251 245 * @param int $id Folder id.
252 246 * @param array $next Row after.
253 247 * @param array $prev Row before.
254 248 */
255 - do_action( 'desktop_mode_folder_shared', $folder_id, $next, $prev );
249 + do_action( 'openstation_folder_shared', $folder_id, $next, $prev );
256 250 }
257 251
258 252 return true;
259 253 }
@@ -281,23 +275,21 @@
281 275 * 4. Every placement INSIDE the folder (parent_id=$folder_id) is
282 276 * deleted with tombstones.
283 277 * 5. The folder row itself is deleted with a folder tombstone.
284 278 *
285 - * @since 0.9.0
286 - *
287 279 * @param int $folder_id Folder id.
288 280 * @param int $user_id Acting user.
289 281 * @return true|WP_Error
290 282 */
291 -function desktop_mode_files_delete_folder( $folder_id, $user_id ) {
283 +function openstation_files_delete_folder( $folder_id, $user_id ) {
292 284 $folder_id = (int) $folder_id;
293 285 $user_id = (int) $user_id;
294 - $row = desktop_mode_files_get_folder( $folder_id );
286 + $row = openstation_files_get_folder( $folder_id );
295 287 if ( ! $row ) {
296 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
288 + return new WP_Error( 'openstation_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
297 289 }
298 290 if ( (int) $row['owner_id'] !== $user_id ) {
299 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot delete this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
291 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot delete this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
300 292 }
301 293
302 294 /**
303 295 * Filter whether a folder delete is allowed to proceed. Default
@@ -309,10 +301,8 @@
309 301 * guard for accidental cascades).
310 302 * - Require a confirmation token / nonce stored in the user's
311 303 * session.
312 304 *
313 - * @since 0.18.x
314 - *
315 305 * @param bool|WP_Error $can Default `true`.
316 306 * @param int $folder_id Folder id about to be deleted.
317 307 * @param int $user_id Acting user (folder owner).
318 308 * @param array $row Folder row.
@@ -317,9 +307,9 @@
317 307 * @param int $user_id Acting user (folder owner).
318 308 * @param array $row Folder row.
319 309 */
320 310 $can = apply_filters(
321 - 'desktop_mode_files_can_delete_folder',
311 + 'openstation_files_can_delete_folder',
322 312 true,
323 313 $folder_id,
324 314 $user_id,
325 315 $row
@@ -328,9 +318,9 @@
328 318 return $can;
329 319 }
330 320 if ( true !== $can ) {
331 321 return new WP_Error(
332 - 'desktop_mode_files_delete_vetoed',
322 + 'openstation_files_delete_vetoed',
333 323 __( 'A plugin blocked this folder from being deleted.', 'desktop-mode' ),
334 324 array( 'status' => 403 )
335 325 );
336 326 }
@@ -340,24 +330,22 @@
340 330 * Listeners can persist a snapshot, log an audit entry, or
341 331 * stage a notification to recipients ("the folder you had
342 332 * access to is being deleted in 10 s").
343 333 *
344 - * @since 0.18.x
345 - *
346 334 * @param int $folder_id Folder being deleted.
347 335 * @param int $user_id Acting user.
348 336 * @param array $row Folder row.
349 337 */
350 - do_action( 'desktop_mode_files_before_delete_folder', $folder_id, $user_id, $row );
338 + do_action( 'openstation_files_before_delete_folder', $folder_id, $user_id, $row );
351 339
352 340 $visited = array();
353 341 $summary = array(
354 - 'folders_deleted' => array(),
355 - 'shares_revoked' => array(),
356 - 'placements_pointing' => array(),
357 - 'placements_inside' => array(),
342 + 'folders_deleted' => array(),
343 + 'shares_revoked' => array(),
344 + 'placements_pointing' => array(),
345 + 'placements_inside' => array(),
358 346 );
359 - $result = desktop_mode_files_delete_folder_recursive( $folder_id, $user_id, $visited, $summary );
347 + $result = openstation_files_delete_folder_recursive( $folder_id, $user_id, $visited, $summary );
360 348 if ( is_wp_error( $result ) ) {
361 349 return $result;
362 350 }
363 351
@@ -374,16 +362,14 @@
374 362 * any deleted folder, across users).
375 363 * - `placements_inside` — placement ids removed (contents of
376 364 * the deleted folders).
377 365 *
378 - * @since 0.18.x
379 - *
380 366 * @param int $folder_id Root folder of the cascade.
381 367 * @param int $user_id Acting user.
382 368 * @param array $summary Cascade summary (see above).
383 369 */
384 370 do_action(
385 - 'desktop_mode_files_after_delete_folder_cascade',
371 + 'openstation_files_after_delete_folder_cascade',
386 372 $folder_id,
387 373 $user_id,
388 374 $summary
389 375 );
@@ -391,9 +377,9 @@
391 377 return true;
392 378 }
393 379
394 380 /**
395 - * Recursive worker for {@see desktop_mode_files_delete_folder}.
381 + * Recursive worker for {@see openstation_files_delete_folder}.
396 382 *
397 383 * Walks the folder's sub-tree (sub-folders the same owner owns),
398 384 * then on the way back up cleans up share rows, decisions,
399 385 * pointing-at placements, contained placements, and the folder
@@ -403,17 +389,21 @@
403 389 * `$visited` guards against cycles in case the placement graph is
404 390 * ever corrupted with one. The owner check happens at the public
405 391 * entry point above; this worker trusts its caller.
406 392 *
407 - * @since 0.18.x
408 393 * @internal
409 394 *
410 - * @param int $folder_id Folder id to delete.
411 - * @param int $user_id Owner.
412 - * @param array $visited Folder ids already processed.
395 + * @param int $folder_id Folder id to delete.
396 + * @param int $user_id Owner.
397 + * @param array $visited Folder ids already processed.
398 + * @param array|null $summary Optional. By-reference cascade summary
399 + * accumulator (`folders_deleted`,
400 + * `shares_revoked`, `placements_pointing`,
401 + * `placements_inside`); initialized when
402 + * null.
413 403 * @return true|WP_Error
414 404 */
415 -function desktop_mode_files_delete_folder_recursive( $folder_id, $user_id, &$visited, &$summary = null ) {
405 +function openstation_files_delete_folder_recursive( $folder_id, $user_id, &$visited, &$summary = null ) {
416 406 global $wpdb;
417 407 $folder_id = (int) $folder_id;
418 408 if ( isset( $visited[ $folder_id ] ) ) {
419 409 return true;
@@ -418,29 +408,29 @@
418 408 if ( isset( $visited[ $folder_id ] ) ) {
419 409 return true;
420 410 }
421 411 $visited[ $folder_id ] = true;
422 - $row = desktop_mode_files_get_folder( $folder_id );
412 + $row = openstation_files_get_folder( $folder_id );
423 413 if ( ! $row ) {
424 414 return true;
425 415 }
426 416
427 - $tables = desktop_mode_files_table_names();
417 + $tables = openstation_files_table_names();
428 418 if ( null === $summary || ! is_array( $summary ) ) {
429 419 $summary = array(
430 - 'folders_deleted' => array(),
431 - 'shares_revoked' => array(),
432 - 'placements_pointing' => array(),
433 - 'placements_inside' => array(),
420 + 'folders_deleted' => array(),
421 + 'shares_revoked' => array(),
422 + 'placements_pointing' => array(),
423 + 'placements_inside' => array(),
434 424 );
435 425 }
436 426
437 427 // 1) Recurse into sub-folders the owner owns. A sub-folder
438 - // owned by SOMEONE ELSE (e.g. a writer recipient who built
439 - // their own folder inside this one) is left intact —
440 - // deleting the parent only severs the containment for the
441 - // owner; the sub-folder's owner can still reach it through
442 - // their own placements.
428 + // owned by SOMEONE ELSE (e.g. a writer recipient who built
429 + // their own folder inside this one) is left intact —
430 + // deleting the parent only severs the containment for the
431 + // owner; the sub-folder's owner can still reach it through
432 + // their own placements.
443 433 $sub_folder_refs = (array) $wpdb->get_col(
444 434 $wpdb->prepare(
445 435 "SELECT DISTINCT file_ref FROM {$tables['placements']}
446 436 WHERE parent_id = %d
@@ -452,26 +442,30 @@
452 442 $sub_id = (int) $ref;
453 443 if ( $sub_id <= 0 || $sub_id === $folder_id ) {
454 444 continue;
455 445 }
456 - $sub_row = desktop_mode_files_get_folder( $sub_id );
446 + $sub_row = openstation_files_get_folder( $sub_id );
457 447 if ( $sub_row && (int) $sub_row['owner_id'] === $user_id ) {
458 - desktop_mode_files_delete_folder_recursive( $sub_id, $user_id, $visited, $summary );
448 + openstation_files_delete_folder_recursive( $sub_id, $user_id, $visited, $summary );
459 449 }
460 450 }
461 451
462 452 // 2) Revoke every share for this folder: shares table + per-
463 - // user decisions table. The folder is going away, so the
464 - // rows are obsolete; leaving them would let the heartbeat
465 - // keep delivering a `removed` tombstone for ghost rows.
453 + // user decisions table. The folder is going away, so the
454 + // rows are obsolete; leaving them would let the heartbeat
455 + // keep delivering a `removed` tombstone for ghost rows.
456 + // `target_type` scoping is load-bearing: `folder_id` carries a
457 + // STORED-FILE id on `target_type='file'` rows — without the
458 + // predicate this cascade would revoke an unrelated user's file
459 + // share whose id collides with the deleted folder's.
466 460 $share_rows = (array) $wpdb->get_results(
467 461 $wpdb->prepare(
468 - "SELECT * FROM {$tables['shares']} WHERE folder_id = %d",
462 + "SELECT * FROM {$tables['shares']} WHERE target_type = 'folder' AND folder_id = %d",
469 463 $folder_id
470 464 ),
471 465 ARRAY_A
472 466 );
473 - $share_ids = array();
467 + $share_ids = array();
474 468 foreach ( $share_rows as $share_row ) {
475 469 $share_ids[] = (int) $share_row['id'];
476 470 }
477 471 if ( ! empty( $share_ids ) ) {
@@ -495,11 +489,11 @@
495 489 // don't have to also subscribe to the cascade-specific
496 490 // hook. `$row` carries the pre-delete share data so
497 491 // listeners can read principal / capability for audit.
498 492 foreach ( $share_rows as $share_row ) {
499 - /** @see desktop_mode_folder_share_revoke */
493 + /** @see openstation_folder_share_revoke */
500 494 do_action(
501 - 'desktop_mode_files_share_revoked',
495 + 'openstation_files_share_revoked',
502 496 (int) $share_row['id'],
503 497 $share_row,
504 498 $user_id
505 499 );
@@ -510,9 +504,9 @@
510 504 );
511 505 }
512 506
513 507 // 3) Placements POINTING AT this folder (every recipient's
514 - // accept-created root placement, plus the owner's own).
508 + // accept-created root placement, plus the owner's own).
515 509 $pointing_ids = (array) $wpdb->get_col(
516 510 $wpdb->prepare(
517 511 "SELECT id FROM {$tables['placements']}
518 512 WHERE file_type = 'folder' AND file_ref = %s",
@@ -519,9 +513,9 @@
519 513 (string) $folder_id
520 514 )
521 515 );
522 516 foreach ( $pointing_ids as $pid ) {
523 - desktop_mode_files_write_tombstone( 'placement', (int) $pid );
517 + openstation_files_write_tombstone( 'placement', (int) $pid );
524 518 }
525 519 if ( ! empty( $pointing_ids ) ) {
526 520 $placeholders = implode( ',', array_fill( 0, count( $pointing_ids ), '%d' ) );
527 521 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -537,24 +531,41 @@
537 531 );
538 532 }
539 533
540 534 // 4) Placements INSIDE this folder. After step 1 the sub-folder
541 - // placements have been recursively handled for owner-owned
542 - // sub-folders; whatever remains here (loose post / link /
543 - // user / etc. placements, plus orphan folder placements
544 - // whose folder we did NOT recurse into because someone else
545 - // owns it) gets deleted with a tombstone each.
546 - $inside_ids = (array) $wpdb->get_col(
535 + // placements have been recursively handled for owner-owned
536 + // sub-folders; whatever remains here (loose post / link /
537 + // user / etc. placements, plus orphan folder placements
538 + // whose folder we did NOT recurse into because someone else
539 + // owns it) gets deleted with a tombstone each.
540 + $inside_rows = (array) $wpdb->get_results(
547 541 $wpdb->prepare(
548 - "SELECT id FROM {$tables['placements']} WHERE parent_id = %d",
542 + "SELECT * FROM {$tables['placements']} WHERE parent_id = %d",
549 543 $folder_id
550 - )
544 + ),
545 + ARRAY_A
551 546 );
552 - foreach ( $inside_ids as $cid ) {
553 - desktop_mode_files_write_tombstone( 'placement', (int) $cid );
547 + $inside_ids = array();
548 + foreach ( $inside_rows as $inside_row ) {
549 + $inside_ids[] = (int) $inside_row['id'];
550 + openstation_files_write_tombstone( 'placement', (int) $inside_row['id'] );
554 551 }
555 552 if ( ! empty( $inside_ids ) ) {
556 553 $wpdb->delete( $tables['placements'], array( 'parent_id' => $folder_id ), array( '%d' ) );
554 + // Upload placements carry real bytes — run the stored-files
555 + // deletion contract now that the rows are gone. Direct
556 + // guarded call (not the public unplaced action) so cascade
557 + // hook semantics for other types stay unchanged.
558 + if ( function_exists( 'openstation_stored_files_handle_unplaced' ) ) {
559 + foreach ( $inside_rows as $inside_row ) {
560 + if ( 'upload' === (string) $inside_row['file_type'] ) {
561 + openstation_stored_files_handle_unplaced(
562 + (int) $inside_row['id'],
563 + openstation_files_normalize_placement_row( $inside_row )
564 + );
565 + }
566 + }
567 + }
557 568 $summary['placements_inside'] = array_merge(
558 569 $summary['placements_inside'],
559 570 array_map( 'intval', $inside_ids )
560 571 );
@@ -562,25 +573,23 @@
562 573
563 574 // 5) The folder row itself + its tombstone.
564 575 $ok = $wpdb->delete( $tables['folders'], array( 'id' => $folder_id ), array( '%d' ) );
565 576 if ( false === $ok ) {
566 - return new WP_Error( 'desktop_mode_files_delete_failed', __( 'Failed to delete folder.', 'desktop-mode' ), array( 'status' => 500 ) );
577 + return new WP_Error( 'openstation_files_delete_failed', __( 'Failed to delete folder.', 'desktop-mode' ), array( 'status' => 500 ) );
567 578 }
568 - desktop_mode_files_write_tombstone( 'folder', $folder_id );
579 + openstation_files_write_tombstone( 'folder', $folder_id );
569 580 $summary['folders_deleted'][] = $folder_id;
570 581
571 582 /**
572 583 * Fires after a folder is deleted. Plugins listening for share
573 584 * lifecycle can subscribe alongside
574 - * `desktop_mode_files_share_revoked` if they want to react to
585 + * `openstation_files_share_revoked` if they want to react to
575 586 * cascade-revokes triggered by folder deletion.
576 587 *
577 - * @since 0.9.0
578 - *
579 588 * @param int $id Folder id.
580 589 * @param array $row Removed row.
581 590 */
582 - do_action( 'desktop_mode_folder_deleted', $folder_id, $row );
591 + do_action( 'openstation_folder_deleted', $folder_id, $row );
583 592
584 593 return true;
585 594 }
586 595
@@ -586,16 +595,18 @@
586 595
587 596 /**
588 597 * Lookup a folder row by id.
589 598 *
590 - * @since 0.9.0
591 - *
592 - * @param int $folder_id Folder id.
599 + * @param int $folder_id Folder id.
600 + * @param bool $include_trashed Optional. Return the row even when
601 + * soft-trashed (recycle-bin callers).
602 + * Default false — trashed folders
603 + * resolve to null.
593 604 * @return array|null
594 605 */
595 -function desktop_mode_files_get_folder( $folder_id, $include_trashed = false ) {
606 +function openstation_files_get_folder( $folder_id, $include_trashed = false ) {
596 607 global $wpdb;
597 - $tables = desktop_mode_files_table_names();
608 + $tables = openstation_files_table_names();
598 609 $row = $wpdb->get_row(
599 610 $wpdb->prepare( "SELECT * FROM {$tables['folders']} WHERE id = %d", (int) $folder_id ),
600 611 ARRAY_A
601 612 );
@@ -606,22 +617,20 @@
606 617 // default — recycle-bin callers pass `true` to opt in.
607 618 if ( ! $include_trashed && ! empty( $row['trashed_at_ms'] ) ) {
608 619 return null;
609 620 }
610 - return desktop_mode_files_normalize_folder_row( $row );
621 + return openstation_files_normalize_folder_row( $row );
611 622 }
612 623
613 624 /**
614 - * Folders visible to `$user_id`. Phase 2 ships private only —
615 - * the viewer is the owner. Phase 6 expands this with the
616 - * `desktop_mode_files_visible_folders` filter.
625 + * Folders visible to `$user_id`. Returns the folders the viewer
626 + * owns; sharing.php merges shared folders in via the
627 + * `openstation_files_visible_folders` filter.
617 628 *
618 - * @since 0.9.0
619 - *
620 629 * @param int $user_id Viewer.
621 630 * @return array[]
622 631 */
623 -function desktop_mode_files_get_visible_folders( $user_id ) {
632 +function openstation_files_get_visible_folders( $user_id ) {
624 633 global $wpdb;
625 634 $user_id = (int) $user_id;
626 635 if ( $user_id <= 0 ) {
627 636 return array();
@@ -626,9 +635,9 @@
626 635 if ( $user_id <= 0 ) {
627 636 return array();
628 637 }
629 638
630 - $tables = desktop_mode_files_table_names();
639 + $tables = openstation_files_table_names();
631 640 $rows = $wpdb->get_results(
632 641 $wpdb->prepare(
633 642 "SELECT * FROM {$tables['folders']}
634 643 WHERE owner_id = %d AND trashed_at_ms IS NULL",
@@ -635,33 +644,32 @@
635 644 $user_id
636 645 ),
637 646 ARRAY_A
638 647 );
639 - $out = array();
648 + $out = array();
640 649 foreach ( (array) $rows as $row ) {
641 - $out[] = desktop_mode_files_normalize_folder_row( $row );
650 + $out[] = openstation_files_normalize_folder_row( $row );
642 651 }
643 652
644 653 /**
645 - * Filter the folders visible to a viewer. Phase 6's sharing
646 - * logic merges shared folders onto this list.
654 + * Filter the folders visible to a viewer. sharing.php's
655 + * `openstation_files_compute_visible_folders` (priority 5)
656 + * merges accepted shares and `share_mode='all'` folders onto
657 + * this list.
647 658 *
648 - * @since 0.9.0
649 - *
650 659 * @param array[] $folders Folders the viewer owns.
651 660 * @param int $user_id Viewer.
652 661 */
653 - return (array) apply_filters( 'desktop_mode_files_visible_folders', $out, $user_id );
662 + return (array) apply_filters( 'openstation_files_visible_folders', $out, $user_id );
654 663 }
655 664
656 665 /**
657 - * @since 0.9.0
658 666 * @internal
659 667 *
660 668 * @param array $row Raw wpdb row.
661 669 * @return array
662 670 */
663 -function desktop_mode_files_normalize_folder_row( $row ) {
671 +function openstation_files_normalize_folder_row( $row ) {
664 672 $meta_raw = isset( $row['share_meta'] ) ? (string) $row['share_meta'] : '';
665 673 $meta = '' !== $meta_raw ? json_decode( $meta_raw, true ) : null;
666 674 return array(
667 675 'id' => (int) $row['id'],