PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 All 36 releases
← All changes | includes/desktop-files/shares-store.php +265 -216 0.9.8 → 1.1.12 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Folder shares store.
3 + * OpenStation — Folder shares store.
4 4 *
5 5 * CRUD + ACL resolver for the v8 `_desktop_mode_folder_shares`
6 6 * table. Each row is a single (folder, principal) grant carrying:
7 7 *
@@ -18,9 +18,9 @@
18 18 *
19 19 * The owner of a folder (folders.owner_id) is the implicit "admin"
20 20 * of its shares: always writes, never visible in the shares table.
21 21 *
22 - * @package WPDesktopMode
22 + * @package OpenStation
23 23 */
24 24
25 25 defined( 'ABSPATH' ) || exit;
26 26
@@ -36,22 +36,22 @@
36 36 * plugin that doesn't ship the feature, no information leak).
37 37 * - The client suppresses every share UI surface.
38 38 *
39 39 * Plugins can short-circuit via the
40 - * `desktop_mode_files_sharing_enabled_for` filter (e.g. force-off
40 + * `openstation_files_sharing_enabled_for` filter (e.g. force-off
41 41 * on a multisite subsite, gate by capability, etc.).
42 42 *
43 43 * @param int $user_id Viewer id. `0` is treated as disabled.
44 44 * @return bool
45 45 */
46 -function desktop_mode_files_sharing_enabled_for( $user_id ) {
46 +function openstation_files_sharing_enabled_for( $user_id ) {
47 47 $user_id = (int) $user_id;
48 48 if ( $user_id <= 0 ) {
49 49 return false;
50 50 }
51 51 $enabled = true;
52 - if ( function_exists( 'desktop_mode_get_os_settings' ) ) {
53 - $settings = desktop_mode_get_os_settings( $user_id );
52 + if ( function_exists( 'openstation_get_os_settings' ) ) {
53 + $settings = openstation_get_os_settings( $user_id );
54 54 $enabled = ! empty( $settings['foldersSharingEnabled'] );
55 55 }
56 56 /**
57 57 * Filter the per-user folder-sharing kill switch.
@@ -58,13 +58,13 @@
58 58 *
59 59 * @param bool $enabled Default reads from OS Settings.
60 60 * @param int $user_id Viewer.
61 61 */
62 - return (bool) apply_filters( 'desktop_mode_files_sharing_enabled_for', $enabled, $user_id );
62 + return (bool) apply_filters( 'openstation_files_sharing_enabled_for', $enabled, $user_id );
63 63 }
64 64
65 65 /** Allowed principal-type values. */
66 -function desktop_mode_files_share_principal_types() {
66 +function openstation_files_share_principal_types() {
67 67 return array( 'user', 'role' );
68 68 }
69 69
70 70 /**
@@ -69,9 +69,9 @@
69 69
70 70 /**
71 71 * Target types that support sharing. The shares table carries a
72 72 * `target_type` column on every row; this filter declares which
73 - * values the framework will accept on `desktop_mode_share_invite`
73 + * values the framework will accept on `openstation_share_invite`
74 74 * + friends.
75 75 *
76 76 * Default ships with `'folder'`. A plugin that wants to add
77 77 * shareable posts (or any other entity) registers their type plus
@@ -77,13 +77,13 @@
77 77 * shareable posts (or any other entity) registers their type plus
78 78 * an owner resolver:
79 79 *
80 80 * ```php
81 - * add_filter( 'desktop_mode_files_shareable_types', function ( $types ) {
81 + * add_filter( 'openstation_files_shareable_types', function ( $types ) {
82 82 * $types[] = 'post';
83 83 * return $types;
84 84 * } );
85 - * add_filter( 'desktop_mode_files_share_target_owner', function ( $owner_id, $type, $ref ) {
85 + * add_filter( 'openstation_files_share_target_owner', function ( $owner_id, $type, $ref ) {
86 86 * if ( 'post' === $type ) {
87 87 * return (int) get_post_field( 'post_author', (int) $ref );
88 88 * }
89 89 * return $owner_id;
@@ -96,9 +96,9 @@
96 96 * functions with a different `$target_type`.
97 97 *
98 98 * @return string[]
99 99 */
100 -function desktop_mode_files_shareable_types() {
100 +function openstation_files_shareable_types() {
101 101 /**
102 102 * Filter the list of target types that support sharing.
103 103 *
104 104 * @param string[] $types Default `[ 'folder', 'file' ]` ('file'
@@ -103,9 +103,9 @@
103 103 *
104 104 * @param string[] $types Default `[ 'folder', 'file' ]` ('file'
105 105 * = stored uploads).
106 106 */
107 - $types = (array) apply_filters( 'desktop_mode_files_shareable_types', array( 'folder', 'file' ) );
107 + $types = (array) apply_filters( 'openstation_files_shareable_types', array( 'folder', 'file' ) );
108 108 return array_values( array_unique( array_filter( array_map( 'strval', $types ) ) ) );
109 109 }
110 110
111 111 /**
@@ -118,17 +118,17 @@
118 118 * are integers, but custom types may
119 119 * use slugs).
120 120 * @return int Owner user id, or 0 if unknown.
121 121 */
122 -function desktop_mode_files_share_target_owner( $target_type, $target_id ) {
122 +function openstation_files_share_target_owner( $target_type, $target_id ) {
123 123 $owner = 0;
124 124 if ( 'folder' === $target_type ) {
125 - $folder = desktop_mode_files_get_folder( (int) $target_id );
125 + $folder = openstation_files_get_folder( (int) $target_id );
126 126 if ( $folder ) {
127 127 $owner = (int) $folder['owner_id'];
128 128 }
129 - } elseif ( 'file' === $target_type && function_exists( 'desktop_mode_stored_files_get' ) ) {
130 - $file = desktop_mode_stored_files_get( (int) $target_id );
129 + } elseif ( 'file' === $target_type && function_exists( 'openstation_stored_files_get' ) ) {
130 + $file = openstation_stored_files_get( (int) $target_id );
131 131 if ( $file ) {
132 132 $owner = (int) $file['owner_id'];
133 133 }
134 134 }
@@ -138,18 +138,18 @@
138 138 * @param int $owner Default owner. 0 = unknown.
139 139 * @param string $target_type Target type slug.
140 140 * @param string $target_id Target id.
141 141 */
142 - return (int) apply_filters( 'desktop_mode_files_share_target_owner', $owner, (string) $target_type, (string) $target_id );
142 + return (int) apply_filters( 'openstation_files_share_target_owner', $owner, (string) $target_type, (string) $target_id );
143 143 }
144 144
145 145 /** Allowed capability values. */
146 -function desktop_mode_files_share_capabilities() {
146 +function openstation_files_share_capabilities() {
147 147 return array( 'read', 'write' );
148 148 }
149 149
150 150 /** Allowed state values. */
151 -function desktop_mode_files_share_states() {
151 +function openstation_files_share_states() {
152 152 return array( 'pending', 'accepted', 'denied' );
153 153 }
154 154
155 155 /**
@@ -154,14 +154,14 @@
154 154
155 155 /**
156 156 * Roles eligible to appear in the share picker. Defaults to every
157 157 * role on the site that carries `edit_posts`. Plugins can override
158 - * via the `desktop_mode_files_share_eligible_roles` filter — site
158 + * via the `openstation_files_share_eligible_roles` filter — site
159 159 * owners typically use this to whitelist a custom team role.
160 160 *
161 161 * @return array<int, array{ slug:string, name:string }>
162 162 */
163 -function desktop_mode_files_share_eligible_roles() {
163 +function openstation_files_share_eligible_roles() {
164 164 $out = array();
165 165 $roles = wp_roles();
166 166 if ( $roles && is_array( $roles->roles ) ) {
167 167 foreach ( $roles->roles as $slug => $info ) {
@@ -178,9 +178,9 @@
178 178 * Filter the roles eligible to appear in the folder share picker.
179 179 *
180 180 * @param array<int, array{ slug:string, name:string }> $roles Default = roles with `edit_posts`.
181 181 */
182 - $out = (array) apply_filters( 'desktop_mode_files_share_eligible_roles', $out );
182 + $out = (array) apply_filters( 'openstation_files_share_eligible_roles', $out );
183 183 return $out;
184 184 }
185 185
186 186 /**
@@ -191,10 +191,10 @@
191 191 * @param int $folder_id Folder id.
192 192 * @param int $user_id Viewer.
193 193 * @return bool
194 194 */
195 -function desktop_mode_files_share_can_manage( $folder_id, $user_id ) {
196 - $folder = desktop_mode_files_get_folder( (int) $folder_id );
195 +function openstation_files_share_can_manage( $folder_id, $user_id ) {
196 + $folder = openstation_files_get_folder( (int) $folder_id );
197 197 $can = $folder && (int) $folder['owner_id'] === (int) $user_id;
198 198 /**
199 199 * Filter who can manage a folder's share rules.
200 200 *
@@ -202,9 +202,9 @@
202 202 * @param int $folder_id Folder id.
203 203 * @param int $user_id Viewer.
204 204 * @param array|null $folder Normalized folder row (null when missing).
205 205 */
206 - return (bool) apply_filters( 'desktop_mode_files_share_can_manage', $can, (int) $folder_id, (int) $user_id, $folder );
206 + return (bool) apply_filters( 'openstation_files_share_can_manage', $can, (int) $folder_id, (int) $user_id, $folder );
207 207 }
208 208
209 209 /**
210 210 * Coerce a raw wpdb shares row to typed values.
@@ -213,9 +213,9 @@
213 213 *
214 214 * @param array $row Raw wpdb row.
215 215 * @return array
216 216 */
217 -function desktop_mode_files_normalize_share_row( $row ) {
217 +function openstation_files_normalize_share_row( $row ) {
218 218 return array(
219 219 'id' => (int) $row['id'],
220 220 // `target_type` defaults to 'folder' for rows that predate
221 221 // the column. The `folder_id` column carries the TARGET id —
@@ -242,11 +242,11 @@
242 242 *
243 243 * @param int $share_id Share id.
244 244 * @return array|null
245 245 */
246 -function desktop_mode_files_get_share( $share_id ) {
246 +function openstation_files_get_share( $share_id ) {
247 247 global $wpdb;
248 - $tables = desktop_mode_files_table_names();
248 + $tables = openstation_files_table_names();
249 249 $row = $wpdb->get_row(
250 250 $wpdb->prepare( "SELECT * FROM {$tables['shares']} WHERE id = %d", (int) $share_id ),
251 251 ARRAY_A
252 252 );
@@ -252,20 +252,72 @@
252 252 );
253 253 if ( ! $row ) {
254 254 return null;
255 255 }
256 - return desktop_mode_files_normalize_share_row( $row );
256 + return openstation_files_normalize_share_row( $row );
257 257 }
258 258
259 259 /**
260 + * The cheap "is this folder shared" summary the desktop paints tiles
261 + * from, so a tile never has to load the full share roster to decide
262 + * whether to wear a badge.
263 + *
264 + * `shared` is deliberately viewer-agnostic — a recipient needs to
265 + * see the badge on a folder someone shared with them just as much as
266 + * the owner does. `recipientCount` is not: the full roster is
267 + * owner-internal, so only a viewer who can manage the folder's
268 + * shares gets a real number. Everyone else gets `0`, which keeps the
269 + * wire shape stable rather than making the key conditional.
270 + *
271 + * Lives here rather than inline in the two callers because both the
272 + * folder response shape and the `folder` file type serialize it, and
273 + * a badge that appeared on one path but not the other is exactly the
274 + * bug this consolidates away.
275 + *
276 + * @param array|null $folder_row Normalized folder row.
277 + * @param int|null $viewer_id Viewer; defaults to the current user.
278 + * @return array{shared: bool, recipientCount: int}
279 + */
280 +function openstation_files_folder_share_summary( $folder_row, $viewer_id = null ) {
281 + $summary = array(
282 + 'shared' => false,
283 + 'recipientCount' => 0,
284 + );
285 + if ( ! is_array( $folder_row ) || ! isset( $folder_row['id'] ) ) {
286 + return $summary;
287 + }
288 +
289 + $folder_id = (int) $folder_row['id'];
290 + $viewer_id = null === $viewer_id ? get_current_user_id() : (int) $viewer_id;
291 + $has_all = 'all' === (string) ( isset( $folder_row['share_mode'] ) ? $folder_row['share_mode'] : '' );
292 +
293 + $accepted = 0;
294 + foreach ( openstation_files_get_folder_shares( $folder_id ) as $share ) {
295 + if ( 'accepted' === $share['state'] ) {
296 + ++$accepted;
297 + }
298 + }
299 +
300 + $summary['shared'] = $has_all || $accepted > 0;
301 + // The manage check costs a folder read of its own, and an
302 + // unshared folder counts zero recipients for everyone anyway —
303 + // so only pay for it when there is something to count. Every
304 + // folder tile on the desktop serializes through here.
305 + if ( $summary['shared'] && openstation_files_share_can_manage( $folder_id, $viewer_id ) ) {
306 + $summary['recipientCount'] = $accepted + ( $has_all ? 1 : 0 );
307 + }
308 + return $summary;
309 +}
310 +
311 +/**
260 312 * Every share row for a folder. Owner-internal view.
261 313 *
262 314 * @param int $folder_id Folder id.
263 315 * @return array[]
264 316 */
265 -function desktop_mode_files_get_folder_shares( $folder_id ) {
317 +function openstation_files_get_folder_shares( $folder_id ) {
266 318 global $wpdb;
267 - $tables = desktop_mode_files_table_names();
319 + $tables = openstation_files_table_names();
268 320 $rows = $wpdb->get_results(
269 321 $wpdb->prepare(
270 322 "SELECT * FROM {$tables['shares']} WHERE target_type = 'folder' AND folder_id = %d ORDER BY invited_at_ms ASC, id ASC",
271 323 (int) $folder_id
@@ -271,11 +323,11 @@
271 323 (int) $folder_id
272 324 ),
273 325 ARRAY_A
274 326 );
275 - $out = array();
327 + $out = array();
276 328 foreach ( (array) $rows as $row ) {
277 - $out[] = desktop_mode_files_normalize_share_row( $row );
329 + $out[] = openstation_files_normalize_share_row( $row );
278 330 }
279 331 return $out;
280 332 }
281 333
@@ -288,9 +340,9 @@
288 340 * @param string $principal_ref User id (stringified) or role slug.
289 341 * @param string $capability 'read' | 'write'.
290 342 * @return int|WP_Error Share id on success.
291 343 */
292 -function desktop_mode_folder_share_invite( $folder_id, $actor_id, $principal_type, $principal_ref, $capability = 'read' ) {
344 +function openstation_folder_share_invite( $folder_id, $actor_id, $principal_type, $principal_ref, $capability = 'read' ) {
293 345 global $wpdb;
294 346 $folder_id = (int) $folder_id;
295 347 $actor_id = (int) $actor_id;
296 348 $principal_type = (string) $principal_type;
@@ -296,16 +348,16 @@
296 348 $principal_type = (string) $principal_type;
297 349 $principal_ref = (string) $principal_ref;
298 350 $capability = (string) $capability;
299 351
300 - if ( ! in_array( $principal_type, desktop_mode_files_share_principal_types(), true ) ) {
301 - return new WP_Error( 'desktop_mode_files_invalid_principal_type', __( 'Invalid principal type.', 'desktop-mode' ), array( 'status' => 400 ) );
352 + if ( ! in_array( $principal_type, openstation_files_share_principal_types(), true ) ) {
353 + return new WP_Error( 'openstation_files_invalid_principal_type', __( 'Invalid principal type.', 'desktop-mode' ), array( 'status' => 400 ) );
302 354 }
303 - if ( ! in_array( $capability, desktop_mode_files_share_capabilities(), true ) ) {
304 - return new WP_Error( 'desktop_mode_files_invalid_capability', __( 'Invalid capability.', 'desktop-mode' ), array( 'status' => 400 ) );
355 + if ( ! in_array( $capability, openstation_files_share_capabilities(), true ) ) {
356 + return new WP_Error( 'openstation_files_invalid_capability', __( 'Invalid capability.', 'desktop-mode' ), array( 'status' => 400 ) );
305 357 }
306 - if ( ! desktop_mode_files_share_can_manage( $folder_id, $actor_id ) ) {
307 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot manage shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
358 + if ( ! openstation_files_share_can_manage( $folder_id, $actor_id ) ) {
359 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot manage shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
308 360 }
309 361
310 362 // Eligibility gate. Users must have `edit_posts`; roles must
311 363 // appear in the eligible-roles list. This is the only place
@@ -313,32 +365,32 @@
313 365 // is computed downstream from accepted rows on this table.
314 366 if ( 'user' === $principal_type ) {
315 367 $uid = (int) $principal_ref;
316 368 if ( $uid <= 0 ) {
317 - return new WP_Error( 'desktop_mode_files_invalid_user', __( 'Invalid user id.', 'desktop-mode' ), array( 'status' => 400 ) );
369 + return new WP_Error( 'openstation_files_invalid_user', __( 'Invalid user id.', 'desktop-mode' ), array( 'status' => 400 ) );
318 370 }
319 371 $user = get_userdata( $uid );
320 372 if ( ! $user ) {
321 - return new WP_Error( 'desktop_mode_files_unknown_user', __( 'Unknown user.', 'desktop-mode' ), array( 'status' => 404 ) );
373 + return new WP_Error( 'openstation_files_unknown_user', __( 'Unknown user.', 'desktop-mode' ), array( 'status' => 404 ) );
322 374 }
323 - $folder = desktop_mode_files_get_folder( $folder_id );
324 - $owner_id = $folder ? (int) $folder['owner_id'] : 0;
375 + $folder = openstation_files_get_folder( $folder_id );
376 + $owner_id = $folder ? (int) $folder['owner_id'] : 0;
325 377 if ( $uid === $owner_id ) {
326 - return new WP_Error( 'desktop_mode_files_share_owner', __( 'You cannot share with the folder owner.', 'desktop-mode' ), array( 'status' => 400 ) );
378 + return new WP_Error( 'openstation_files_share_owner', __( 'You cannot share with the folder owner.', 'desktop-mode' ), array( 'status' => 400 ) );
327 379 }
328 380 if ( ! user_can( $user, 'edit_posts' ) ) {
329 - return new WP_Error( 'desktop_mode_files_ineligible_principal', __( 'This user is not eligible.', 'desktop-mode' ), array( 'status' => 400 ) );
381 + return new WP_Error( 'openstation_files_ineligible_principal', __( 'This user is not eligible.', 'desktop-mode' ), array( 'status' => 400 ) );
330 382 }
331 383 $principal_ref = (string) $uid;
332 384 } else {
333 - $eligible = wp_list_pluck( desktop_mode_files_share_eligible_roles(), 'slug' );
385 + $eligible = wp_list_pluck( openstation_files_share_eligible_roles(), 'slug' );
334 386 if ( ! in_array( $principal_ref, $eligible, true ) ) {
335 - return new WP_Error( 'desktop_mode_files_ineligible_role', __( 'This role is not eligible.', 'desktop-mode' ), array( 'status' => 400 ) );
387 + return new WP_Error( 'openstation_files_ineligible_role', __( 'This role is not eligible.', 'desktop-mode' ), array( 'status' => 400 ) );
336 388 }
337 389 }
338 390
339 - $tables = desktop_mode_files_table_names();
340 - $now = desktop_mode_files_now_ms();
391 + $tables = openstation_files_table_names();
392 + $now = openstation_files_now_ms();
341 393
342 394 // Idempotent invite: a pre-existing row with state='denied'
343 395 // becomes 'pending' again (owner re-inviting after a no);
344 396 // a pre-existing 'pending' or 'accepted' row keeps its state
@@ -353,24 +405,24 @@
353 405 ),
354 406 ARRAY_A
355 407 );
356 408 if ( $existing ) {
357 - $id = (int) $existing['id'];
358 - $next_state = 'denied' === $existing['state'] ? 'pending' : $existing['state'];
359 - $next_cap = $capability;
360 - $set = array(
409 + $id = (int) $existing['id'];
410 + $next_state = 'denied' === $existing['state'] ? 'pending' : $existing['state'];
411 + $next_cap = $capability;
412 + $set = array(
361 413 'capability' => $next_cap,
362 414 'state' => $next_state,
363 415 'invited_by' => $actor_id,
364 416 'invited_at_ms' => $now,
365 417 );
366 - $fmt = array( '%s', '%s', '%d', '%d' );
418 + $fmt = array( '%s', '%s', '%d', '%d' );
367 419 if ( 'denied' === $existing['state'] ) {
368 420 $set['decided_at_ms'] = null;
369 421 $fmt[] = '%s';
370 422 }
371 423 $wpdb->update( $tables['shares'], $set, array( 'id' => $id ), $fmt, array( '%d' ) );
372 - $row = desktop_mode_files_get_share( $id );
424 + $row = openstation_files_get_share( $id );
373 425 } else {
374 426 $ok = $wpdb->insert(
375 427 $tables['shares'],
376 428 array(
@@ -385,15 +437,15 @@
385 437 ),
386 438 array( '%s', '%d', '%s', '%s', '%s', '%s', '%d', '%d' )
387 439 );
388 440 if ( false === $ok ) {
389 - return new WP_Error( 'desktop_mode_files_share_insert_failed', __( 'Failed to record share.', 'desktop-mode' ), array( 'status' => 500 ) );
441 + return new WP_Error( 'openstation_files_share_insert_failed', __( 'Failed to record share.', 'desktop-mode' ), array( 'status' => 500 ) );
390 442 }
391 443 $id = (int) $wpdb->insert_id;
392 - $row = desktop_mode_files_get_share( $id );
444 + $row = openstation_files_get_share( $id );
393 445 }
394 446
395 - desktop_mode_files_bump_folder_updated_at( $folder_id );
447 + openstation_files_bump_folder_updated_at( $folder_id );
396 448
397 449 /**
398 450 * Fires after a share is invited (or re-invited).
399 451 *
@@ -400,9 +452,9 @@
400 452 * @param int $share_id Share id.
401 453 * @param array $row Share row.
402 454 * @param int $actor_id Acting user.
403 455 */
404 - do_action( 'desktop_mode_files_share_invited', $id, $row, $actor_id );
456 + do_action( 'openstation_files_share_invited', $id, $row, $actor_id );
405 457
406 458 return $id;
407 459 }
408 460
@@ -412,27 +464,27 @@
412 464 * @param int $share_id Share id.
413 465 * @param int $actor_id Actor.
414 466 * @return true|WP_Error
415 467 */
416 -function desktop_mode_folder_share_revoke( $share_id, $actor_id ) {
468 +function openstation_folder_share_revoke( $share_id, $actor_id ) {
417 469 global $wpdb;
418 470 $share_id = (int) $share_id;
419 471 $actor_id = (int) $actor_id;
420 - $row = desktop_mode_files_get_share( $share_id );
472 + $row = openstation_files_get_share( $share_id );
421 473 if ( ! $row ) {
422 - return new WP_Error( 'desktop_mode_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
474 + return new WP_Error( 'openstation_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
423 475 }
424 - if ( ! desktop_mode_files_share_can_manage( $row['folder_id'], $actor_id ) ) {
425 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot manage shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
476 + if ( ! openstation_files_share_can_manage( $row['folder_id'], $actor_id ) ) {
477 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot manage shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
426 478 }
427 479
428 - $tables = desktop_mode_files_table_names();
480 + $tables = openstation_files_table_names();
429 481 $wpdb->delete( $tables['shares'], array( 'id' => $share_id ), array( '%d' ) );
430 482 // Drop any per-user decision rows attached to this share so
431 483 // they don't leak past the row deletion.
432 484 $wpdb->delete( $tables['decisions'], array( 'share_id' => $share_id ), array( '%d' ) );
433 485
434 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
486 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
435 487
436 488 // Scrub recipient's local view. For user-principal grants
437 489 // that's the single recipient; for role-principal grants we
438 490 // scrub every user who had a decision row (i.e. interacted
@@ -440,9 +492,9 @@
440 492 // also lose visibility but have no local placement to trash.
441 493 if ( 'user' === $row['principal_type'] && 'accepted' === $row['state'] ) {
442 494 $uid = (int) $row['principal_ref'];
443 495 if ( $uid > 0 ) {
444 - desktop_mode_files_trash_folder_for_user( $row['folder_id'], $uid );
496 + openstation_files_trash_folder_for_user( $row['folder_id'], $uid );
445 497 }
446 498 } elseif ( 'role' === $row['principal_type'] ) {
447 499 $decided_users = $wpdb->get_col(
448 500 $wpdb->prepare(
@@ -450,9 +502,9 @@
450 502 $share_id
451 503 )
452 504 );
453 505 foreach ( (array) $decided_users as $uid ) {
454 - desktop_mode_files_trash_folder_for_user( $row['folder_id'], (int) $uid );
506 + openstation_files_trash_folder_for_user( $row['folder_id'], (int) $uid );
455 507 }
456 508 }
457 509
458 510 /**
@@ -461,9 +513,9 @@
461 513 * @param int $share_id Share id.
462 514 * @param array $row Share row (last-known state).
463 515 * @param int $actor_id Acting user.
464 516 */
465 - do_action( 'desktop_mode_files_share_revoked', $share_id, $row, $actor_id );
517 + do_action( 'openstation_files_share_revoked', $share_id, $row, $actor_id );
466 518
467 519 return true;
468 520 }
469 521
@@ -474,31 +526,31 @@
474 526 * @param int $actor_id Actor.
475 527 * @param string $capability New capability.
476 528 * @return true|WP_Error
477 529 */
478 -function desktop_mode_folder_share_update_capability( $share_id, $actor_id, $capability ) {
530 +function openstation_folder_share_update_capability( $share_id, $actor_id, $capability ) {
479 531 global $wpdb;
480 532 $share_id = (int) $share_id;
481 533 $actor_id = (int) $actor_id;
482 534 $capability = (string) $capability;
483 535
484 - if ( ! in_array( $capability, desktop_mode_files_share_capabilities(), true ) ) {
485 - return new WP_Error( 'desktop_mode_files_invalid_capability', __( 'Invalid capability.', 'desktop-mode' ), array( 'status' => 400 ) );
536 + if ( ! in_array( $capability, openstation_files_share_capabilities(), true ) ) {
537 + return new WP_Error( 'openstation_files_invalid_capability', __( 'Invalid capability.', 'desktop-mode' ), array( 'status' => 400 ) );
486 538 }
487 - $row = desktop_mode_files_get_share( $share_id );
539 + $row = openstation_files_get_share( $share_id );
488 540 if ( ! $row ) {
489 - return new WP_Error( 'desktop_mode_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
541 + return new WP_Error( 'openstation_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
490 542 }
491 - if ( ! desktop_mode_files_share_can_manage( $row['folder_id'], $actor_id ) ) {
492 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot manage shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
543 + if ( ! openstation_files_share_can_manage( $row['folder_id'], $actor_id ) ) {
544 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot manage shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
493 545 }
494 546
495 - $tables = desktop_mode_files_table_names();
547 + $tables = openstation_files_table_names();
496 548 $wpdb->update( $tables['shares'], array( 'capability' => $capability ), array( 'id' => $share_id ), array( '%s' ), array( '%d' ) );
497 549
498 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
550 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
499 551
500 - $next = desktop_mode_files_get_share( $share_id );
552 + $next = openstation_files_get_share( $share_id );
501 553
502 554 /**
503 555 * Fires after a share's capability is changed.
504 556 *
@@ -506,9 +558,9 @@
506 558 * @param array $next Row after.
507 559 * @param array $prev Row before.
508 560 * @param int $actor_id Acting user.
509 561 */
510 - do_action( 'desktop_mode_files_share_capability_changed', $share_id, $next, $row, $actor_id );
562 + do_action( 'openstation_files_share_capability_changed', $share_id, $next, $row, $actor_id );
511 563
512 564 return true;
513 565 }
514 566
@@ -520,11 +572,11 @@
520 572 * @param int $share_id Share id.
521 573 * @param int $user_id User.
522 574 * @return array|null Normalized decision row or null.
523 575 */
524 -function desktop_mode_files_get_user_decision( $share_id, $user_id ) {
576 +function openstation_files_get_user_decision( $share_id, $user_id ) {
525 577 global $wpdb;
526 - $tables = desktop_mode_files_table_names();
578 + $tables = openstation_files_table_names();
527 579 $row = $wpdb->get_row(
528 580 $wpdb->prepare(
529 581 "SELECT * FROM {$tables['decisions']} WHERE share_id = %d AND user_id = %d",
530 582 (int) $share_id,
@@ -552,12 +604,12 @@
552 604 * @param int $share_id Share id.
553 605 * @param int $user_id User.
554 606 * @param string $state 'pending' | 'accepted' | 'denied'.
555 607 */
556 -function desktop_mode_files_upsert_user_decision( $share_id, $user_id, $state ) {
608 +function openstation_files_upsert_user_decision( $share_id, $user_id, $state ) {
557 609 global $wpdb;
558 - $tables = desktop_mode_files_table_names();
559 - $now = desktop_mode_files_now_ms();
610 + $tables = openstation_files_table_names();
611 + $now = openstation_files_now_ms();
560 612 $wpdb->query(
561 613 $wpdb->prepare(
562 614 "INSERT INTO {$tables['decisions']}
563 615 (share_id, user_id, state, decided_at_ms)
@@ -581,14 +633,14 @@
581 633 * @param array $share_row Normalized share row.
582 634 * @param int $user_id Viewer.
583 635 * @return string 'pending' | 'accepted' | 'denied'
584 636 */
585 -function desktop_mode_files_share_user_state( $share_row, $user_id ) {
637 +function openstation_files_share_user_state( $share_row, $user_id ) {
586 638 if ( 'user' === $share_row['principal_type'] ) {
587 639 return (string) $share_row['state'];
588 640 }
589 641 if ( 'role' === $share_row['principal_type'] ) {
590 - $dec = desktop_mode_files_get_user_decision( (int) $share_row['id'], (int) $user_id );
642 + $dec = openstation_files_get_user_decision( (int) $share_row['id'], (int) $user_id );
591 643 if ( $dec ) {
592 644 return (string) $dec['state'];
593 645 }
594 646 return 'pending';
@@ -603,49 +655,52 @@
603 655 * @param int $share_id Share id.
604 656 * @param int $user_id Acting user (must be the share's principal).
605 657 * @return array|WP_Error Share row on success.
606 658 */
607 -function desktop_mode_folder_share_accept( $share_id, $user_id ) {
659 +function openstation_folder_share_accept( $share_id, $user_id ) {
608 660 global $wpdb;
609 661 $share_id = (int) $share_id;
610 662 $user_id = (int) $user_id;
611 - $row = desktop_mode_files_get_share( $share_id );
663 + $row = openstation_files_get_share( $share_id );
612 664 if ( ! $row || 'folder' !== $row['target_type'] ) {
613 - return new WP_Error( 'desktop_mode_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
665 + return new WP_Error( 'openstation_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
614 666 }
615 - if ( ! desktop_mode_files_share_principal_matches_user( $row, $user_id ) ) {
616 - return new WP_Error( 'desktop_mode_files_share_not_recipient', __( 'This invite is not for you.', 'desktop-mode' ), array( 'status' => 403 ) );
667 + if ( ! openstation_files_share_principal_matches_user( $row, $user_id ) ) {
668 + return new WP_Error( 'openstation_files_share_not_recipient', __( 'This invite is not for you.', 'desktop-mode' ), array( 'status' => 403 ) );
617 669 }
618 - $state = desktop_mode_files_share_user_state( $row, $user_id );
670 + $state = openstation_files_share_user_state( $row, $user_id );
619 671 if ( 'accepted' === $state ) {
620 672 return $row;
621 673 }
622 674 if ( 'denied' === $state && 'user' === $row['principal_type'] ) {
623 - return new WP_Error( 'desktop_mode_files_share_already_denied', __( 'This invite was denied.', 'desktop-mode' ), array( 'status' => 410 ) );
675 + return new WP_Error( 'openstation_files_share_already_denied', __( 'This invite was denied.', 'desktop-mode' ), array( 'status' => 410 ) );
624 676 }
625 677
626 - $tables = desktop_mode_files_table_names();
627 - $now = desktop_mode_files_now_ms();
678 + $tables = openstation_files_table_names();
679 + $now = openstation_files_now_ms();
628 680
629 681 if ( 'user' === $row['principal_type'] ) {
630 682 $wpdb->update(
631 683 $tables['shares'],
632 - array( 'state' => 'accepted', 'decided_at_ms' => $now ),
684 + array(
685 + 'state' => 'accepted',
686 + 'decided_at_ms' => $now,
687 + ),
633 688 array( 'id' => $share_id ),
634 689 array( '%s', '%d' ),
635 690 array( '%d' )
636 691 );
637 692 } else {
638 - desktop_mode_files_upsert_user_decision( $share_id, $user_id, 'accepted' );
693 + openstation_files_upsert_user_decision( $share_id, $user_id, 'accepted' );
639 694 }
640 695
641 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
696 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
642 697
643 698 // Place the folder on the recipient's desktop root.
644 - $parent_id = (int) apply_filters( 'desktop_mode_folder_share_accept_default_parent', 0, $row['folder_id'], $user_id, $row );
645 - desktop_mode_files_place_at_next_free_slot( $user_id, $parent_id, 'folder', (string) $row['folder_id'] );
699 + $parent_id = (int) apply_filters( 'openstation_folder_share_accept_default_parent', 0, $row['folder_id'], $user_id, $row );
700 + openstation_files_place_at_next_free_slot( $user_id, $parent_id, 'folder', (string) $row['folder_id'] );
646 701
647 - $next = desktop_mode_files_get_share( $share_id );
702 + $next = openstation_files_get_share( $share_id );
648 703
649 704 /**
650 705 * Fires after a share is accepted by its recipient.
651 706 *
@@ -652,9 +707,9 @@
652 707 * @param int $share_id Share id.
653 708 * @param array $row Updated share row.
654 709 * @param int $user_id Acting user (recipient).
655 710 */
656 - do_action( 'desktop_mode_files_share_accepted', $share_id, $next, $user_id );
711 + do_action( 'openstation_files_share_accepted', $share_id, $next, $user_id );
657 712
658 713 return $next;
659 714 }
660 715
@@ -664,31 +719,34 @@
664 719 * @param int $share_id Share id.
665 720 * @param int $user_id Recipient.
666 721 * @return array|WP_Error Share row on success.
667 722 */
668 -function desktop_mode_folder_share_deny( $share_id, $user_id ) {
723 +function openstation_folder_share_deny( $share_id, $user_id ) {
669 724 global $wpdb;
670 725 $share_id = (int) $share_id;
671 726 $user_id = (int) $user_id;
672 - $row = desktop_mode_files_get_share( $share_id );
727 + $row = openstation_files_get_share( $share_id );
673 728 if ( ! $row || 'folder' !== $row['target_type'] ) {
674 - return new WP_Error( 'desktop_mode_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
729 + return new WP_Error( 'openstation_files_share_not_found', __( 'Share not found.', 'desktop-mode' ), array( 'status' => 404 ) );
675 730 }
676 - if ( ! desktop_mode_files_share_principal_matches_user( $row, $user_id ) ) {
677 - return new WP_Error( 'desktop_mode_files_share_not_recipient', __( 'This invite is not for you.', 'desktop-mode' ), array( 'status' => 403 ) );
731 + if ( ! openstation_files_share_principal_matches_user( $row, $user_id ) ) {
732 + return new WP_Error( 'openstation_files_share_not_recipient', __( 'This invite is not for you.', 'desktop-mode' ), array( 'status' => 403 ) );
678 733 }
679 - $state = desktop_mode_files_share_user_state( $row, $user_id );
734 + $state = openstation_files_share_user_state( $row, $user_id );
680 735 if ( 'denied' === $state ) {
681 736 return $row;
682 737 }
683 738
684 - $tables = desktop_mode_files_table_names();
685 - $now = desktop_mode_files_now_ms();
739 + $tables = openstation_files_table_names();
740 + $now = openstation_files_now_ms();
686 741
687 742 if ( 'user' === $row['principal_type'] ) {
688 743 $wpdb->update(
689 744 $tables['shares'],
690 - array( 'state' => 'denied', 'decided_at_ms' => $now ),
745 + array(
746 + 'state' => 'denied',
747 + 'decided_at_ms' => $now,
748 + ),
691 749 array( 'id' => $share_id ),
692 750 array( '%s', '%d' ),
693 751 array( '%d' )
694 752 );
@@ -693,12 +751,12 @@
693 751 array( '%d' )
694 752 );
695 753 } else {
696 754 // Role-principal: per-user decision keeps other role members untouched.
697 - desktop_mode_files_upsert_user_decision( $share_id, $user_id, 'denied' );
755 + openstation_files_upsert_user_decision( $share_id, $user_id, 'denied' );
698 756 }
699 757
700 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
758 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
701 759
702 760 // If the recipient had previously accepted and is now denying
703 761 // (e.g. they hit deny on a placeholder they already opened),
704 762 // scrub their local placement too. Works for BOTH user- and
@@ -703,12 +761,12 @@
703 761 // (e.g. they hit deny on a placeholder they already opened),
704 762 // scrub their local placement too. Works for BOTH user- and
705 763 // role-principals since the trash helper is user-scoped.
706 764 if ( 'accepted' === $state ) {
707 - desktop_mode_files_trash_folder_for_user( $row['folder_id'], $user_id );
765 + openstation_files_trash_folder_for_user( $row['folder_id'], $user_id );
708 766 }
709 767
710 - $next = desktop_mode_files_get_share( $share_id );
768 + $next = openstation_files_get_share( $share_id );
711 769
712 770 /**
713 771 * Fires after a share is denied.
714 772 *
@@ -715,9 +773,9 @@
715 773 * @param int $share_id Share id.
716 774 * @param array $row Updated share row.
717 775 * @param int $user_id Acting user (recipient).
718 776 */
719 - do_action( 'desktop_mode_files_share_denied', $share_id, $next, $user_id );
777 + do_action( 'openstation_files_share_denied', $share_id, $next, $user_id );
720 778
721 779 return $next;
722 780 }
723 781
@@ -730,28 +788,28 @@
730 788 * @param int $folder_id Folder id.
731 789 * @param int $user_id Recipient leaving.
732 790 * @return true|WP_Error
733 791 */
734 -function desktop_mode_folder_share_leave( $folder_id, $user_id ) {
792 +function openstation_folder_share_leave( $folder_id, $user_id ) {
735 793 global $wpdb;
736 794 $folder_id = (int) $folder_id;
737 795 $user_id = (int) $user_id;
738 796 if ( $folder_id <= 0 || $user_id <= 0 ) {
739 - return new WP_Error( 'desktop_mode_files_bad_request', __( 'Invalid arguments.', 'desktop-mode' ), array( 'status' => 400 ) );
797 + return new WP_Error( 'openstation_files_bad_request', __( 'Invalid arguments.', 'desktop-mode' ), array( 'status' => 400 ) );
740 798 }
741 - $folder = desktop_mode_files_get_folder( $folder_id );
799 + $folder = openstation_files_get_folder( $folder_id );
742 800 if ( ! $folder ) {
743 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
801 + return new WP_Error( 'openstation_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
744 802 }
745 803 if ( (int) $folder['owner_id'] === $user_id ) {
746 - return new WP_Error( 'desktop_mode_files_owner_cannot_leave', __( 'Owners cannot leave their own folder.', 'desktop-mode' ), array( 'status' => 400 ) );
804 + return new WP_Error( 'openstation_files_owner_cannot_leave', __( 'Owners cannot leave their own folder.', 'desktop-mode' ), array( 'status' => 400 ) );
747 805 }
748 806
749 807 $user = get_userdata( $user_id );
750 808 $roles = $user ? (array) $user->roles : array();
751 809
752 - $tables = desktop_mode_files_table_names();
753 - $rows = $wpdb->get_results(
810 + $tables = openstation_files_table_names();
811 + $rows = $wpdb->get_results(
754 812 $wpdb->prepare(
755 813 "SELECT * FROM {$tables['shares']} WHERE target_type = 'folder' AND folder_id = %d",
756 814 $folder_id
757 815 ),
@@ -758,24 +816,27 @@
758 816 ARRAY_A
759 817 );
760 818 $touched = 0;
761 819 foreach ( (array) $rows as $raw ) {
762 - $row = desktop_mode_files_normalize_share_row( $raw );
763 - if ( ! desktop_mode_files_share_principal_matches_user( $row, $user_id ) ) {
820 + $row = openstation_files_normalize_share_row( $raw );
821 + if ( ! openstation_files_share_principal_matches_user( $row, $user_id ) ) {
764 822 continue;
765 823 }
766 824 if ( 'user' === $row['principal_type'] ) {
767 825 $wpdb->update(
768 826 $tables['shares'],
769 - array( 'state' => 'denied', 'decided_at_ms' => desktop_mode_files_now_ms() ),
827 + array(
828 + 'state' => 'denied',
829 + 'decided_at_ms' => openstation_files_now_ms(),
830 + ),
770 831 array( 'id' => $row['id'] ),
771 832 array( '%s', '%d' ),
772 833 array( '%d' )
773 834 );
774 835 } else {
775 - desktop_mode_files_upsert_user_decision( $row['id'], $user_id, 'denied' );
836 + openstation_files_upsert_user_decision( $row['id'], $user_id, 'denied' );
776 837 }
777 - $touched++;
838 + ++$touched;
778 839 /**
779 840 * Fires after a recipient leaves a shared folder. Distinct
780 841 * from `_denied` (owner-side audit) because this is always
781 842 * recipient-initiated, after acceptance.
@@ -783,19 +844,19 @@
783 844 * @param int $share_id Share id.
784 845 * @param array $row Share row (last-known).
785 846 * @param int $user_id Recipient leaving.
786 847 */
787 - do_action( 'desktop_mode_files_share_left', $row['id'], $row, $user_id );
848 + do_action( 'openstation_files_share_left', $row['id'], $row, $user_id );
788 849 }
789 850
790 851 // Scrub the recipient's view regardless of whether a share row
791 852 // matched (the user might have a placement from a previously
792 853 // revoked share that lingered).
793 - desktop_mode_files_trash_folder_for_user( $folder_id, $user_id );
794 - desktop_mode_files_bump_folder_updated_at( $folder_id );
854 + openstation_files_trash_folder_for_user( $folder_id, $user_id );
855 + openstation_files_bump_folder_updated_at( $folder_id );
795 856
796 857 if ( 0 === $touched ) {
797 - return new WP_Error( 'desktop_mode_files_not_member', __( 'You do not have access to this folder.', 'desktop-mode' ), array( 'status' => 404 ) );
858 + return new WP_Error( 'openstation_files_not_member', __( 'You do not have access to this folder.', 'desktop-mode' ), array( 'status' => 404 ) );
798 859 }
799 860 return true;
800 861 }
801 862
@@ -807,9 +868,9 @@
807 868 * @param array $share_row Normalized share row.
808 869 * @param int $user_id User to test.
809 870 * @return bool
810 871 */
811 -function desktop_mode_files_share_principal_matches_user( $share_row, $user_id ) {
872 +function openstation_files_share_principal_matches_user( $share_row, $user_id ) {
812 873 $user_id = (int) $user_id;
813 874 if ( $user_id <= 0 ) {
814 875 return false;
815 876 }
@@ -834,9 +895,9 @@
834 895 * @param int $folder_id Folder id.
835 896 * @param int $user_id Viewer.
836 897 * @return string 'none' | 'read' | 'write'
837 898 */
838 -function desktop_mode_folder_share_user_capability( $folder_id, $user_id ) {
899 +function openstation_folder_share_user_capability( $folder_id, $user_id ) {
839 900 $folder_id = (int) $folder_id;
840 901 $user_id = (int) $user_id;
841 902 if ( $folder_id <= 0 || $user_id <= 0 ) {
842 903 return 'none';
@@ -841,9 +902,9 @@
841 902 if ( $folder_id <= 0 || $user_id <= 0 ) {
842 903 return 'none';
843 904 }
844 905
845 - $folder = desktop_mode_files_get_folder( $folder_id );
906 + $folder = openstation_files_get_folder( $folder_id );
846 907 if ( ! $folder ) {
847 908 return 'none';
848 909 }
849 910 if ( (int) $folder['owner_id'] === $user_id ) {
@@ -852,9 +913,9 @@
852 913
853 914 // Cascade — walk the folder's ancestor chain. A folder nested
854 915 // inside a shared folder inherits the share. The most permissive
855 916 // ancestor cap wins. Bail out as soon as we hit 'write'.
856 - $cascade_cap = desktop_mode_folder_share_user_capability_cascade( $folder_id, $user_id );
917 + $cascade_cap = openstation_folder_share_user_capability_cascade( $folder_id, $user_id );
857 918 if ( 'write' === $cascade_cap ) {
858 919 return 'write';
859 920 }
860 921
@@ -866,9 +927,9 @@
866 927 * @param string $cap Default 'read'.
867 928 * @param int $folder_id Folder id.
868 929 * @param int $user_id Viewer.
869 930 */
870 - $cap = (string) apply_filters( 'desktop_mode_files_share_all_default_capability', 'read', $folder_id, $user_id );
931 + $cap = (string) apply_filters( 'openstation_files_share_all_default_capability', 'read', $folder_id, $user_id );
871 932 }
872 933
873 934 $user_roles = array();
874 935 $user = get_userdata( $user_id );
@@ -876,9 +937,9 @@
876 937 $user_roles = (array) $user->roles;
877 938 }
878 939
879 940 global $wpdb;
880 - $tables = desktop_mode_files_table_names();
941 + $tables = openstation_files_table_names();
881 942 // User-principal grants — state lives on the shares row.
882 943 $rows = $wpdb->get_results(
883 944 $wpdb->prepare(
884 945 "SELECT id, principal_type, principal_ref, capability FROM {$tables['shares']}
@@ -900,9 +961,9 @@
900 961 $folder_id
901 962 ),
902 963 ARRAY_A
903 964 );
904 - $rows = array_merge( (array) $rows, (array) $role_rows );
965 + $rows = array_merge( (array) $rows, (array) $role_rows );
905 966 foreach ( $rows as $row ) {
906 967 $matches = false;
907 968 if ( 'user' === $row['principal_type'] && (int) $row['principal_ref'] === $user_id ) {
908 969 $matches = true;
@@ -936,15 +997,15 @@
936 997 * @param int $folder_id Folder id.
937 998 * @param int $user_id Viewer.
938 999 * @param array $folder Normalized folder row.
939 1000 */
940 - return (string) apply_filters( 'desktop_mode_folder_share_user_capability', $cap, $folder_id, $user_id, $folder );
1001 + return (string) apply_filters( 'openstation_folder_share_user_capability', $cap, $folder_id, $user_id, $folder );
941 1002 }
942 1003
943 1004 /**
944 1005 * Walk the ancestor chain of `$folder_id` and return the most
945 1006 * permissive DIRECT share cap any ancestor has for `$user_id`.
946 - * Used by `desktop_mode_folder_share_user_capability` to cascade
1007 + * Used by `openstation_folder_share_user_capability` to cascade
947 1008 * a share grant from a parent folder into every folder nested
948 1009 * inside it.
949 1010 *
950 1011 * "Direct" means the share row exists for that ancestor — we
@@ -962,17 +1023,17 @@
962 1023 * @param int $folder_id Folder whose ancestors to walk.
963 1024 * @param int $user_id Viewer.
964 1025 * @return string 'none' | 'read' | 'write'
965 1026 */
966 -function desktop_mode_folder_share_user_capability_cascade( $folder_id, $user_id ) {
1027 +function openstation_folder_share_user_capability_cascade( $folder_id, $user_id ) {
967 1028 $user_id = (int) $user_id;
968 - $ancestors = desktop_mode_folder_ancestors( (int) $folder_id );
1029 + $ancestors = openstation_folder_ancestors( (int) $folder_id );
969 1030 if ( empty( $ancestors ) || $user_id <= 0 ) {
970 1031 return 'none';
971 1032 }
972 1033
973 1034 global $wpdb;
974 - $tables = desktop_mode_files_table_names();
1035 + $tables = openstation_files_table_names();
975 1036
976 1037 // Coerce + dedupe to keep the IN clause small and safe to
977 1038 // interpolate. Every value is an int by the time it lands
978 1039 // in the SQL.
@@ -993,9 +1054,9 @@
993 1054 return 'write';
994 1055 }
995 1056 if ( 'all' === $f['share_mode'] ) {
996 1057 $all_cap = (string) apply_filters(
997 - 'desktop_mode_files_share_all_default_capability',
1058 + 'openstation_files_share_all_default_capability',
998 1059 'read',
999 1060 (int) $f['id'],
1000 1061 $user_id
1001 1062 );
@@ -1068,13 +1129,13 @@
1068 1129 }
1069 1130
1070 1131 /**
1071 1132 * Direct (non-cascading) capability resolver. Same logic as
1072 - * `desktop_mode_folder_share_user_capability` minus the cascade
1133 + * `openstation_folder_share_user_capability` minus the cascade
1073 1134 * walk — owner / share rows / role decisions only.
1074 1135 *
1075 1136 * Currently uncalled: the cascade resolver
1076 - * (`desktop_mode_folder_share_user_capability_cascade`) resolves
1137 + * (`openstation_folder_share_user_capability_cascade`) resolves
1077 1138 * every ancestor via three batched `IN (…)` queries instead of
1078 1139 * querying ancestors one at a time. Kept as a single-folder,
1079 1140 * non-cascading resolver.
1080 1141 *
@@ -1079,15 +1140,15 @@
1079 1140 * non-cascading resolver.
1080 1141 *
1081 1142 * @internal
1082 1143 */
1083 -function desktop_mode_folder_share_user_capability_direct( $folder_id, $user_id ) {
1144 +function openstation_folder_share_user_capability_direct( $folder_id, $user_id ) {
1084 1145 $folder_id = (int) $folder_id;
1085 1146 $user_id = (int) $user_id;
1086 1147 if ( $folder_id <= 0 || $user_id <= 0 ) {
1087 1148 return 'none';
1088 1149 }
1089 - $folder = desktop_mode_files_get_folder( $folder_id );
1150 + $folder = openstation_files_get_folder( $folder_id );
1090 1151 if ( ! $folder ) {
1091 1152 return 'none';
1092 1153 }
1093 1154 if ( (int) $folder['owner_id'] === $user_id ) {
@@ -1094,9 +1155,9 @@
1094 1155 return 'write';
1095 1156 }
1096 1157 $cap = 'none';
1097 1158 if ( 'all' === $folder['share_mode'] ) {
1098 - $cap = (string) apply_filters( 'desktop_mode_files_share_all_default_capability', 'read', $folder_id, $user_id );
1159 + $cap = (string) apply_filters( 'openstation_files_share_all_default_capability', 'read', $folder_id, $user_id );
1099 1160 }
1100 1161
1101 1162 $user_roles = array();
1102 1163 $user = get_userdata( $user_id );
@@ -1104,10 +1165,10 @@
1104 1165 $user_roles = (array) $user->roles;
1105 1166 }
1106 1167
1107 1168 global $wpdb;
1108 - $tables = desktop_mode_files_table_names();
1109 - $rows = $wpdb->get_results(
1169 + $tables = openstation_files_table_names();
1170 + $rows = $wpdb->get_results(
1110 1171 $wpdb->prepare(
1111 1172 "SELECT id, principal_type, principal_ref, capability FROM {$tables['shares']}
1112 1173 WHERE target_type = 'folder' AND folder_id = %d AND principal_type = 'user' AND state = 'accepted'",
1113 1174 $folder_id
@@ -1160,15 +1221,15 @@
1160 1221 * @param int $folder_id Folder whose ancestors to walk.
1161 1222 * @param int $limit Max ancestor count (default 16).
1162 1223 * @return int[]
1163 1224 */
1164 -function desktop_mode_folder_ancestors( $folder_id, $limit = 16 ) {
1225 +function openstation_folder_ancestors( $folder_id, $limit = 16 ) {
1165 1226 global $wpdb;
1166 1227 $folder_id = (int) $folder_id;
1167 1228 if ( $folder_id <= 0 ) {
1168 1229 return array();
1169 1230 }
1170 - $tables = desktop_mode_files_table_names();
1231 + $tables = openstation_files_table_names();
1171 1232 $ancestors = array();
1172 1233 $current = $folder_id;
1173 1234 $visited = array();
1174 1235 $depth = 0;
@@ -1176,9 +1237,9 @@
1176 1237 if ( isset( $visited[ $current ] ) ) {
1177 1238 break;
1178 1239 }
1179 1240 $visited[ $current ] = true;
1180 - $folder = desktop_mode_files_get_folder( $current );
1241 + $folder = openstation_files_get_folder( $current );
1181 1242 if ( ! $folder ) {
1182 1243 break;
1183 1244 }
1184 1245 $owner = (int) $folder['owner_id'];
@@ -1204,9 +1265,9 @@
1204 1265 break;
1205 1266 }
1206 1267 $ancestors[] = $parent_id;
1207 1268 $current = $parent_id;
1208 - $depth++;
1269 + ++$depth;
1209 1270 }
1210 1271 return $ancestors;
1211 1272 }
1212 1273
@@ -1217,9 +1278,9 @@
1217 1278 * @param int $user_id Viewer.
1218 1279 * @param int $since_ms Optional. Only include rows with `invited_at_ms > since`.
1219 1280 * @return array[]
1220 1281 */
1221 -function desktop_mode_files_get_pending_shares_for_user( $user_id, $since_ms = 0 ) {
1282 +function openstation_files_get_pending_shares_for_user( $user_id, $since_ms = 0 ) {
1222 1283 global $wpdb;
1223 1284 $user_id = (int) $user_id;
1224 1285 $since_ms = (int) $since_ms;
1225 1286 if ( $user_id <= 0 ) {
@@ -1230,9 +1291,9 @@
1230 1291 return array();
1231 1292 }
1232 1293 $roles = (array) $user->roles;
1233 1294
1234 - $tables = desktop_mode_files_table_names();
1295 + $tables = openstation_files_table_names();
1235 1296
1236 1297 // User-principal: state lives on the share row. Surface where
1237 1298 // state='pending' AND principal_ref matches the user.
1238 1299 $user_pending = $wpdb->get_results(
@@ -1277,9 +1338,9 @@
1277 1338 }
1278 1339
1279 1340 $out = array();
1280 1341 foreach ( array_merge( (array) $user_pending, (array) $role_pending ) as $row ) {
1281 - $out[] = desktop_mode_files_normalize_share_row( $row );
1342 + $out[] = openstation_files_normalize_share_row( $row );
1282 1343 }
1283 1344 return $out;
1284 1345 }
1285 1346
@@ -1291,14 +1352,14 @@
1291 1352 * @internal
1292 1353 *
1293 1354 * @param int $folder_id Folder id.
1294 1355 */
1295 -function desktop_mode_files_bump_folder_updated_at( $folder_id ) {
1356 +function openstation_files_bump_folder_updated_at( $folder_id ) {
1296 1357 global $wpdb;
1297 - $tables = desktop_mode_files_table_names();
1358 + $tables = openstation_files_table_names();
1298 1359 $wpdb->update(
1299 1360 $tables['folders'],
1300 - array( 'updated_at_ms' => desktop_mode_files_now_ms() ),
1361 + array( 'updated_at_ms' => openstation_files_now_ms() ),
1301 1362 array( 'id' => (int) $folder_id ),
1302 1363 array( '%d' ),
1303 1364 array( '%d' )
1304 1365 );
@@ -1312,9 +1373,9 @@
1312 1373 * @param int $folder_id Folder id.
1313 1374 * @param int $user_id Recipient.
1314 1375 * @return int Number of placement rows trashed.
1315 1376 */
1316 -function desktop_mode_files_trash_folder_for_user( $folder_id, $user_id ) {
1377 +function openstation_files_trash_folder_for_user( $folder_id, $user_id ) {
1317 1378 global $wpdb;
1318 1379 $folder_id = (int) $folder_id;
1319 1380 $user_id = (int) $user_id;
1320 1381 if ( $folder_id <= 0 || $user_id <= 0 ) {
@@ -1319,15 +1380,15 @@
1319 1380 $user_id = (int) $user_id;
1320 1381 if ( $folder_id <= 0 || $user_id <= 0 ) {
1321 1382 return 0;
1322 1383 }
1323 - $tables = desktop_mode_files_table_names();
1324 - $now = desktop_mode_files_now_ms();
1384 + $tables = openstation_files_table_names();
1385 + $now = openstation_files_now_ms();
1325 1386
1326 1387 // The recipient's folder-shortcut placement (parent_id=0,
1327 1388 // file_type='folder', file_ref=$folder_id) + every placement
1328 1389 // they own INSIDE this folder (parent_id=$folder_id).
1329 - $rows = $wpdb->get_results(
1390 + $rows = $wpdb->get_results(
1330 1391 $wpdb->prepare(
1331 1392 "SELECT id FROM {$tables['placements']}
1332 1393 WHERE owner_id = %d
1333 1394 AND trashed_at_ms IS NULL
@@ -1357,9 +1418,9 @@
1357 1418 // Soft-trash only — DO NOT write a tombstone here.
1358 1419 // Tombstones represent permanent removal (hard delete); the
1359 1420 // heartbeat already surfaces soft-trashed rows via the
1360 1421 // `trashed_at_ms IS NOT NULL` query in
1361 - // `desktop_mode_files_compute_heartbeat_delta`. Writing a
1422 + // `openstation_files_compute_heartbeat_delta`. Writing a
1362 1423 // tombstone on every soft-trash conflates the two states and,
1363 1424 // when the row is later restored (e.g. the recipient re-
1364 1425 // accepts the same share), the lingering tombstone keeps
1365 1426 // telling clients "this is gone" while the same placement
@@ -1366,9 +1427,9 @@
1366 1427 // row is also being upserted as alive — causing the row to
1367 1428 // disappear from the desktop on every heartbeat tick. See
1368 1429 // the user-reported "shared folder vanishes after refresh"
1369 1430 // bug.
1370 - $count++;
1431 + ++$count;
1371 1432 }
1372 1433 return $count;
1373 1434 }
1374 1435
@@ -1382,9 +1443,9 @@
1382 1443 * @param int $user_id Acting user.
1383 1444 * @param array $row Placement row.
1384 1445 * @return bool
1385 1446 */
1386 -function desktop_mode_files_share_gate_trash( $can, $user_id, $row ) {
1447 +function openstation_files_share_gate_trash( $can, $user_id, $row ) {
1387 1448 $parent_id = isset( $row['parent_id'] ) ? (int) $row['parent_id'] : 0;
1388 1449 $user_id = (int) $user_id;
1389 1450
1390 1451 // Root-level placement of a SHARED FOLDER (the recipient's
@@ -1405,9 +1466,9 @@
1405 1466 isset( $row['file_ref'] )
1406 1467 ) {
1407 1468 $folder_ref = (int) $row['file_ref'];
1408 1469 if ( $folder_ref > 0 ) {
1409 - $folder_row = desktop_mode_files_get_folder( $folder_ref );
1470 + $folder_row = openstation_files_get_folder( $folder_ref );
1410 1471 if (
1411 1472 $folder_row &&
1412 1473 (int) $folder_row['owner_id'] !== $user_id
1413 1474 ) {
@@ -1418,9 +1479,9 @@
1418 1479 // a writer's destructive intent should be expressed
1419 1480 // via the leave flow (which scrubs their own
1420 1481 // placement) instead of via Move to Trash (which is
1421 1482 // reserved for the owner's destructive cascade).
1422 - $root_cap = desktop_mode_folder_share_user_capability( $folder_ref, $user_id );
1483 + $root_cap = openstation_folder_share_user_capability( $folder_ref, $user_id );
1423 1484 if ( 'none' !== $root_cap ) {
1424 1485 return false;
1425 1486 }
1426 1487 }
@@ -1432,14 +1493,14 @@
1432 1493 // Any other root placement (not a shared-folder tile) —
1433 1494 // default ownership rule stands.
1434 1495 return $can;
1435 1496 }
1436 - $folder = desktop_mode_files_get_folder( $parent_id );
1497 + $folder = openstation_files_get_folder( $parent_id );
1437 1498 if ( ! $folder ) {
1438 1499 return $can;
1439 1500 }
1440 1501 $is_owner = (int) $folder['owner_id'] === $user_id;
1441 - $cap = desktop_mode_folder_share_user_capability( $parent_id, $user_id );
1502 + $cap = openstation_folder_share_user_capability( $parent_id, $user_id );
1442 1503
1443 1504 // Folder owner can always trash anything inside their folder.
1444 1505 if ( $is_owner ) {
1445 1506 return true;
@@ -1449,9 +1510,9 @@
1449 1510 // writers can trash any icon in the shared folder. Readers
1450 1511 // can't trash anything (even their own placement in this folder).
1451 1512 return 'write' === $cap;
1452 1513 }
1453 -add_filter( 'desktop_mode_files_user_can_trash_placement', 'desktop_mode_files_share_gate_trash', 10, 3 );
1514 +add_filter( 'openstation_files_user_can_trash_placement', 'openstation_files_share_gate_trash', 10, 3 );
1454 1515
1455 1516 /**
1456 1517 * Inject share-related state into the shell config so the share
1457 1518 * settings modal + role picker have what they need without
@@ -1459,13 +1520,13 @@
1459 1520 *
1460 1521 * @param array $config Shell config.
1461 1522 * @return array
1462 1523 */
1463 -function desktop_mode_files_share_inject_shell_config( $config ) {
1524 +function openstation_files_share_inject_shell_config( $config ) {
1464 1525 if ( ! is_array( $config ) ) {
1465 1526 $config = array();
1466 1527 }
1467 - $config['shareEligibleRoles'] = desktop_mode_files_share_eligible_roles();
1528 + $config['shareEligibleRoles'] = openstation_files_share_eligible_roles();
1468 1529 $config['filesUsersSearchUrl'] = esc_url_raw( rest_url( 'desktop-mode/v1/files/users/search' ) );
1469 1530 $config['folderSharesUrl'] = esc_url_raw( rest_url( 'desktop-mode/v1/files/folders' ) );
1470 1531 $user_id = (int) get_current_user_id();
1471 1532 if ( ! isset( $config['currentUserId'] ) ) {
@@ -1475,28 +1536,28 @@
1475 1536 // Seed the shares store with the viewer's current pending invites
1476 1537 // on the first paint, so the accept/deny modal opens immediately
1477 1538 // on refresh instead of waiting for the first heartbeat tick to
1478 1539 // deliver them. Same kill-switch + shape as the heartbeat path —
1479 - // see `desktop_mode_files_collect_heartbeat_delta()` for the
1540 + // see `openstation_files_collect_heartbeat_delta()` for the
1480 1541 // canonical builder.
1481 1542 $pending = array();
1482 - $sharing_enabled = function_exists( 'desktop_mode_files_sharing_enabled_for' )
1483 - ? desktop_mode_files_sharing_enabled_for( $user_id )
1543 + $sharing_enabled = function_exists( 'openstation_files_sharing_enabled_for' )
1544 + ? openstation_files_sharing_enabled_for( $user_id )
1484 1545 : true;
1485 1546 if (
1486 1547 $user_id > 0 &&
1487 1548 $sharing_enabled &&
1488 - function_exists( 'desktop_mode_files_get_pending_shares_for_user' )
1549 + function_exists( 'openstation_files_get_pending_shares_for_user' )
1489 1550 ) {
1490 - $rows = desktop_mode_files_get_pending_shares_for_user( $user_id, 0 );
1551 + $rows = openstation_files_get_pending_shares_for_user( $user_id, 0 );
1491 1552 foreach ( $rows as $row ) {
1492 - $shape = desktop_mode_files_shape_share( $row );
1493 - $folder = desktop_mode_files_get_folder( $row['folder_id'] );
1553 + $shape = openstation_files_shape_share( $row );
1554 + $folder = openstation_files_get_folder( $row['folder_id'] );
1494 1555 if ( $folder ) {
1495 1556 $shape['folderName'] = (string) $folder['name'];
1496 1557 $shape['ownerId'] = (int) $folder['owner_id'];
1497 1558 $owner_user = get_userdata( (int) $folder['owner_id'] );
1498 - $shape['ownerName'] = $owner_user ? $owner_user->display_name : '';
1559 + $shape['ownerName'] = $owner_user ? openstation_plain_text_title( $owner_user->display_name ) : '';
1499 1560 $shape['ownerAvatar'] = $owner_user ? get_avatar_url( $owner_user->ID, array( 'size' => 48 ) ) : '';
1500 1561 }
1501 1562 $pending[] = $shape;
1502 1563 }
@@ -1504,16 +1565,19 @@
1504 1565 $config['serverPendingShares'] = $pending;
1505 1566
1506 1567 return $config;
1507 1568 }
1508 -add_filter( 'desktop_mode_shell_config', 'desktop_mode_files_share_inject_shell_config', 20 );
1569 +add_filter( 'openstation_shell_config', 'openstation_files_share_inject_shell_config', 20 );
1509 1570
1510 1571 /**
1511 - * Place an icon at the next free row-major slot in a user's view
1512 - * of `$parent_id`. Internal helper used by share-accept and
1513 - * fan-out. Mirrors the grid math in `src/desktop-files/grid.ts`
1514 - * (padding 16 + col 96 + row 110).
1572 + * Place an icon at the next free slot in a user's view of
1573 + * `$parent_id`. Internal helper used by share-accept and fan-out.
1515 1574 *
1575 + * "Next free" follows the destination's own reading order — columns
1576 + * on the desktop, rows in a folder — so a shared thing arriving
1577 + * unannounced lands where the next tile the user created would have.
1578 + * Grid math lives in `includes/desktop-files/grid.php`.
1579 + *
1516 1580 * @param int $user_id Viewer.
1517 1581 * @param int $parent_id Folder id (0 = desktop root).
1518 1582 * @param string $type File-type slug.
1519 1583 * @param string $ref Entity reference.
@@ -1518,14 +1582,14 @@
1518 1582 * @param string $type File-type slug.
1519 1583 * @param string $ref Entity reference.
1520 1584 * @return int|WP_Error Placement id or error.
1521 1585 */
1522 -function desktop_mode_files_place_at_next_free_slot( $user_id, $parent_id, $type, $ref ) {
1586 +function openstation_files_place_at_next_free_slot( $user_id, $parent_id, $type, $ref ) {
1523 1587 global $wpdb;
1524 1588 $user_id = (int) $user_id;
1525 1589 $parent_id = max( 0, (int) $parent_id );
1526 1590
1527 - $tables = desktop_mode_files_table_names();
1591 + $tables = openstation_files_table_names();
1528 1592 $existing = $wpdb->get_results(
1529 1593 $wpdb->prepare(
1530 1594 "SELECT x, y FROM {$tables['placements']}
1531 1595 WHERE owner_id = %d
@@ -1535,34 +1599,19 @@
1535 1599 $parent_id
1536 1600 ),
1537 1601 ARRAY_A
1538 1602 );
1539 - $occupied = array();
1540 - foreach ( (array) $existing as $row ) {
1541 - $col = max( 0, (int) round( ( (int) $row['x'] - 16 ) / 96 ) );
1542 - $r = max( 0, (int) round( ( (int) $row['y'] - 16 ) / 110 ) );
1543 - $occupied[ "$col,$r" ] = true;
1544 - }
1603 + $occupied = openstation_files_grid_occupied( $existing );
1545 1604
1546 - $pick_col = 0;
1547 - $pick_row = 0;
1548 - for ( $r = 0; $r < 999; $r++ ) {
1549 - for ( $col = 0; $col < 999; $col++ ) {
1550 - if ( ! isset( $occupied[ "$col,$r" ] ) ) {
1551 - $pick_col = $col;
1552 - $pick_row = $r;
1553 - break 2;
1554 - }
1555 - }
1556 - }
1605 + list( $pick_col, $pick_row ) = openstation_files_grid_next_free(
1606 + $occupied,
1607 + openstation_files_grid_order( $parent_id )
1608 + );
1557 1609
1558 - return desktop_mode_files_place(
1610 + return openstation_files_place(
1559 1611 $user_id,
1560 1612 $parent_id,
1561 1613 $type,
1562 1614 $ref,
1563 - array(
1564 - 'x' => 16 + $pick_col * 96,
1565 - 'y' => 16 + $pick_row * 110,
1566 - )
1615 + openstation_files_grid_cell_to_point( $pick_col, $pick_row )
1567 1616 );
1568 1617 }