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 +266 -294 0.9.7 → 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,10 +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
23 - * @since 0.8.5
22 + * @package OpenStation
24 23 */
25 24
26 25 defined( 'ABSPATH' ) || exit;
27 26
@@ -37,39 +36,35 @@
37 36 * plugin that doesn't ship the feature, no information leak).
38 37 * - The client suppresses every share UI surface.
39 38 *
40 39 * Plugins can short-circuit via the
41 - * `desktop_mode_files_sharing_enabled_for` filter (e.g. force-off
40 + * `openstation_files_sharing_enabled_for` filter (e.g. force-off
42 41 * on a multisite subsite, gate by capability, etc.).
43 42 *
44 - * @since 0.8.5
45 - *
46 43 * @param int $user_id Viewer id. `0` is treated as disabled.
47 44 * @return bool
48 45 */
49 -function desktop_mode_files_sharing_enabled_for( $user_id ) {
46 +function openstation_files_sharing_enabled_for( $user_id ) {
50 47 $user_id = (int) $user_id;
51 48 if ( $user_id <= 0 ) {
52 49 return false;
53 50 }
54 51 $enabled = true;
55 - if ( function_exists( 'desktop_mode_get_os_settings' ) ) {
56 - $settings = desktop_mode_get_os_settings( $user_id );
52 + if ( function_exists( 'openstation_get_os_settings' ) ) {
53 + $settings = openstation_get_os_settings( $user_id );
57 54 $enabled = ! empty( $settings['foldersSharingEnabled'] );
58 55 }
59 56 /**
60 57 * Filter the per-user folder-sharing kill switch.
61 58 *
62 - * @since 0.8.5
63 - *
64 59 * @param bool $enabled Default reads from OS Settings.
65 60 * @param int $user_id Viewer.
66 61 */
67 - 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 );
68 63 }
69 64
70 65 /** Allowed principal-type values. */
71 -function desktop_mode_files_share_principal_types() {
66 +function openstation_files_share_principal_types() {
72 67 return array( 'user', 'role' );
73 68 }
74 69
75 70 /**
@@ -74,9 +69,9 @@
74 69
75 70 /**
76 71 * Target types that support sharing. The shares table carries a
77 72 * `target_type` column on every row; this filter declares which
78 - * values the framework will accept on `desktop_mode_share_invite`
73 + * values the framework will accept on `openstation_share_invite`
79 74 * + friends.
80 75 *
81 76 * Default ships with `'folder'`. A plugin that wants to add
82 77 * shareable posts (or any other entity) registers their type plus
@@ -82,13 +77,13 @@
82 77 * shareable posts (or any other entity) registers their type plus
83 78 * an owner resolver:
84 79 *
85 80 * ```php
86 - * add_filter( 'desktop_mode_files_shareable_types', function ( $types ) {
81 + * add_filter( 'openstation_files_shareable_types', function ( $types ) {
87 82 * $types[] = 'post';
88 83 * return $types;
89 84 * } );
90 - * 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 ) {
91 86 * if ( 'post' === $type ) {
92 87 * return (int) get_post_field( 'post_author', (int) $ref );
93 88 * }
94 89 * return $owner_id;
@@ -99,22 +94,18 @@
99 94 * REST routes live at `/folders/{id}/shares`. A future modal
100 95 * generalisation (or a per-type opener) can hit the same store
101 96 * functions with a different `$target_type`.
102 97 *
103 - * @since 0.8.5
104 - *
105 98 * @return string[]
106 99 */
107 -function desktop_mode_files_shareable_types() {
100 +function openstation_files_shareable_types() {
108 101 /**
109 102 * Filter the list of target types that support sharing.
110 103 *
111 - * @since 0.8.5
112 - *
113 104 * @param string[] $types Default `[ 'folder', 'file' ]` ('file'
114 - * = stored uploads, since 0.9.6).
105 + * = stored uploads).
115 106 */
116 - $types = (array) apply_filters( 'desktop_mode_files_shareable_types', array( 'folder', 'file' ) );
107 + $types = (array) apply_filters( 'openstation_files_shareable_types', array( 'folder', 'file' ) );
117 108 return array_values( array_unique( array_filter( array_map( 'strval', $types ) ) ) );
118 109 }
119 110
120 111 /**
@@ -121,10 +112,8 @@
121 112 * Owner of a shareable target. Defaults to the folder owner when
122 113 * `$target_type === 'folder'`. Plugins extending the system to a
123 114 * new type register a filter that returns the correct owner id.
124 115 *
125 - * @since 0.8.5
126 - *
127 116 * @param string $target_type Target type slug.
128 117 * @param string $target_id Target id (stringified — folder ids
129 118 * are integers, but custom types may
130 119 * use slugs).
@@ -129,17 +118,17 @@
129 118 * are integers, but custom types may
130 119 * use slugs).
131 120 * @return int Owner user id, or 0 if unknown.
132 121 */
133 -function desktop_mode_files_share_target_owner( $target_type, $target_id ) {
122 +function openstation_files_share_target_owner( $target_type, $target_id ) {
134 123 $owner = 0;
135 124 if ( 'folder' === $target_type ) {
136 - $folder = desktop_mode_files_get_folder( (int) $target_id );
125 + $folder = openstation_files_get_folder( (int) $target_id );
137 126 if ( $folder ) {
138 127 $owner = (int) $folder['owner_id'];
139 128 }
140 - } elseif ( 'file' === $target_type && function_exists( 'desktop_mode_stored_files_get' ) ) {
141 - $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 );
142 131 if ( $file ) {
143 132 $owner = (int) $file['owner_id'];
144 133 }
145 134 }
@@ -145,24 +134,22 @@
145 134 }
146 135 /**
147 136 * Filter the owner of a shareable target.
148 137 *
149 - * @since 0.8.5
150 - *
151 138 * @param int $owner Default owner. 0 = unknown.
152 139 * @param string $target_type Target type slug.
153 140 * @param string $target_id Target id.
154 141 */
155 - 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 );
156 143 }
157 144
158 145 /** Allowed capability values. */
159 -function desktop_mode_files_share_capabilities() {
146 +function openstation_files_share_capabilities() {
160 147 return array( 'read', 'write' );
161 148 }
162 149
163 150 /** Allowed state values. */
164 -function desktop_mode_files_share_states() {
151 +function openstation_files_share_states() {
165 152 return array( 'pending', 'accepted', 'denied' );
166 153 }
167 154
168 155 /**
@@ -167,16 +154,14 @@
167 154
168 155 /**
169 156 * Roles eligible to appear in the share picker. Defaults to every
170 157 * role on the site that carries `edit_posts`. Plugins can override
171 - * via the `desktop_mode_files_share_eligible_roles` filter — site
158 + * via the `openstation_files_share_eligible_roles` filter — site
172 159 * owners typically use this to whitelist a custom team role.
173 160 *
174 - * @since 0.8.5
175 - *
176 161 * @return array<int, array{ slug:string, name:string }>
177 162 */
178 -function desktop_mode_files_share_eligible_roles() {
163 +function openstation_files_share_eligible_roles() {
179 164 $out = array();
180 165 $roles = wp_roles();
181 166 if ( $roles && is_array( $roles->roles ) ) {
182 167 foreach ( $roles->roles as $slug => $info ) {
@@ -191,13 +176,11 @@
191 176 }
192 177 /**
193 178 * Filter the roles eligible to appear in the folder share picker.
194 179 *
195 - * @since 0.8.5
196 - *
197 180 * @param array<int, array{ slug:string, name:string }> $roles Default = roles with `edit_posts`.
198 181 */
199 - $out = (array) apply_filters( 'desktop_mode_files_share_eligible_roles', $out );
182 + $out = (array) apply_filters( 'openstation_files_share_eligible_roles', $out );
200 183 return $out;
201 184 }
202 185
203 186 /**
@@ -204,40 +187,35 @@
204 187 * Whether `$user_id` may manage the share rules of `$folder_id`.
205 188 * Default: only the folder's owner. Plugins (e.g. a team-admin
206 189 * extension) can broaden this via the filter.
207 190 *
208 - * @since 0.8.5
209 - *
210 191 * @param int $folder_id Folder id.
211 192 * @param int $user_id Viewer.
212 193 * @return bool
213 194 */
214 -function desktop_mode_files_share_can_manage( $folder_id, $user_id ) {
215 - $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 );
216 197 $can = $folder && (int) $folder['owner_id'] === (int) $user_id;
217 198 /**
218 199 * Filter who can manage a folder's share rules.
219 200 *
220 - * @since 0.8.5
221 - *
222 201 * @param bool $can Default: owner-only.
223 202 * @param int $folder_id Folder id.
224 203 * @param int $user_id Viewer.
225 204 * @param array|null $folder Normalized folder row (null when missing).
226 205 */
227 - 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 );
228 207 }
229 208
230 209 /**
231 210 * Coerce a raw wpdb shares row to typed values.
232 211 *
233 - * @since 0.8.5
234 212 * @internal
235 213 *
236 214 * @param array $row Raw wpdb row.
237 215 * @return array
238 216 */
239 -function desktop_mode_files_normalize_share_row( $row ) {
217 +function openstation_files_normalize_share_row( $row ) {
240 218 return array(
241 219 'id' => (int) $row['id'],
242 220 // `target_type` defaults to 'folder' for rows that predate
243 221 // the column. The `folder_id` column carries the TARGET id —
@@ -261,16 +239,14 @@
261 239
262 240 /**
263 241 * Read a single share row by id.
264 242 *
265 - * @since 0.8.5
266 - *
267 243 * @param int $share_id Share id.
268 244 * @return array|null
269 245 */
270 -function desktop_mode_files_get_share( $share_id ) {
246 +function openstation_files_get_share( $share_id ) {
271 247 global $wpdb;
272 - $tables = desktop_mode_files_table_names();
248 + $tables = openstation_files_table_names();
273 249 $row = $wpdb->get_row(
274 250 $wpdb->prepare( "SELECT * FROM {$tables['shares']} WHERE id = %d", (int) $share_id ),
275 251 ARRAY_A
276 252 );
@@ -276,22 +252,72 @@
276 252 );
277 253 if ( ! $row ) {
278 254 return null;
279 255 }
280 - return desktop_mode_files_normalize_share_row( $row );
256 + return openstation_files_normalize_share_row( $row );
281 257 }
282 258
283 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 +/**
284 312 * Every share row for a folder. Owner-internal view.
285 313 *
286 - * @since 0.8.5
287 - *
288 314 * @param int $folder_id Folder id.
289 315 * @return array[]
290 316 */
291 -function desktop_mode_files_get_folder_shares( $folder_id ) {
317 +function openstation_files_get_folder_shares( $folder_id ) {
292 318 global $wpdb;
293 - $tables = desktop_mode_files_table_names();
319 + $tables = openstation_files_table_names();
294 320 $rows = $wpdb->get_results(
295 321 $wpdb->prepare(
296 322 "SELECT * FROM {$tables['shares']} WHERE target_type = 'folder' AND folder_id = %d ORDER BY invited_at_ms ASC, id ASC",
297 323 (int) $folder_id
@@ -297,11 +323,11 @@
297 323 (int) $folder_id
298 324 ),
299 325 ARRAY_A
300 326 );
301 - $out = array();
327 + $out = array();
302 328 foreach ( (array) $rows as $row ) {
303 - $out[] = desktop_mode_files_normalize_share_row( $row );
329 + $out[] = openstation_files_normalize_share_row( $row );
304 330 }
305 331 return $out;
306 332 }
307 333
@@ -307,10 +333,8 @@
307 333
308 334 /**
309 335 * Invite a principal to a folder.
310 336 *
311 - * @since 0.8.5
312 - *
313 337 * @param int $folder_id Folder id.
314 338 * @param int $actor_id Actor (must be able to manage the folder).
315 339 * @param string $principal_type 'user' | 'role'.
316 340 * @param string $principal_ref User id (stringified) or role slug.
@@ -316,9 +340,9 @@
316 340 * @param string $principal_ref User id (stringified) or role slug.
317 341 * @param string $capability 'read' | 'write'.
318 342 * @return int|WP_Error Share id on success.
319 343 */
320 -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' ) {
321 345 global $wpdb;
322 346 $folder_id = (int) $folder_id;
323 347 $actor_id = (int) $actor_id;
324 348 $principal_type = (string) $principal_type;
@@ -324,16 +348,16 @@
324 348 $principal_type = (string) $principal_type;
325 349 $principal_ref = (string) $principal_ref;
326 350 $capability = (string) $capability;
327 351
328 - if ( ! in_array( $principal_type, desktop_mode_files_share_principal_types(), true ) ) {
329 - 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 ) );
330 354 }
331 - if ( ! in_array( $capability, desktop_mode_files_share_capabilities(), true ) ) {
332 - 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 ) );
333 357 }
334 - if ( ! desktop_mode_files_share_can_manage( $folder_id, $actor_id ) ) {
335 - 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 ) );
336 360 }
337 361
338 362 // Eligibility gate. Users must have `edit_posts`; roles must
339 363 // appear in the eligible-roles list. This is the only place
@@ -341,32 +365,32 @@
341 365 // is computed downstream from accepted rows on this table.
342 366 if ( 'user' === $principal_type ) {
343 367 $uid = (int) $principal_ref;
344 368 if ( $uid <= 0 ) {
345 - 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 ) );
346 370 }
347 371 $user = get_userdata( $uid );
348 372 if ( ! $user ) {
349 - 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 ) );
350 374 }
351 - $folder = desktop_mode_files_get_folder( $folder_id );
352 - $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;
353 377 if ( $uid === $owner_id ) {
354 - 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 ) );
355 379 }
356 380 if ( ! user_can( $user, 'edit_posts' ) ) {
357 - 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 ) );
358 382 }
359 383 $principal_ref = (string) $uid;
360 384 } else {
361 - $eligible = wp_list_pluck( desktop_mode_files_share_eligible_roles(), 'slug' );
385 + $eligible = wp_list_pluck( openstation_files_share_eligible_roles(), 'slug' );
362 386 if ( ! in_array( $principal_ref, $eligible, true ) ) {
363 - 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 ) );
364 388 }
365 389 }
366 390
367 - $tables = desktop_mode_files_table_names();
368 - $now = desktop_mode_files_now_ms();
391 + $tables = openstation_files_table_names();
392 + $now = openstation_files_now_ms();
369 393
370 394 // Idempotent invite: a pre-existing row with state='denied'
371 395 // becomes 'pending' again (owner re-inviting after a no);
372 396 // a pre-existing 'pending' or 'accepted' row keeps its state
@@ -381,24 +405,24 @@
381 405 ),
382 406 ARRAY_A
383 407 );
384 408 if ( $existing ) {
385 - $id = (int) $existing['id'];
386 - $next_state = 'denied' === $existing['state'] ? 'pending' : $existing['state'];
387 - $next_cap = $capability;
388 - $set = array(
409 + $id = (int) $existing['id'];
410 + $next_state = 'denied' === $existing['state'] ? 'pending' : $existing['state'];
411 + $next_cap = $capability;
412 + $set = array(
389 413 'capability' => $next_cap,
390 414 'state' => $next_state,
391 415 'invited_by' => $actor_id,
392 416 'invited_at_ms' => $now,
393 417 );
394 - $fmt = array( '%s', '%s', '%d', '%d' );
418 + $fmt = array( '%s', '%s', '%d', '%d' );
395 419 if ( 'denied' === $existing['state'] ) {
396 420 $set['decided_at_ms'] = null;
397 421 $fmt[] = '%s';
398 422 }
399 423 $wpdb->update( $tables['shares'], $set, array( 'id' => $id ), $fmt, array( '%d' ) );
400 - $row = desktop_mode_files_get_share( $id );
424 + $row = openstation_files_get_share( $id );
401 425 } else {
402 426 $ok = $wpdb->insert(
403 427 $tables['shares'],
404 428 array(
@@ -413,26 +437,24 @@
413 437 ),
414 438 array( '%s', '%d', '%s', '%s', '%s', '%s', '%d', '%d' )
415 439 );
416 440 if ( false === $ok ) {
417 - 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 ) );
418 442 }
419 443 $id = (int) $wpdb->insert_id;
420 - $row = desktop_mode_files_get_share( $id );
444 + $row = openstation_files_get_share( $id );
421 445 }
422 446
423 - desktop_mode_files_bump_folder_updated_at( $folder_id );
447 + openstation_files_bump_folder_updated_at( $folder_id );
424 448
425 449 /**
426 450 * Fires after a share is invited (or re-invited).
427 451 *
428 - * @since 0.8.5
429 - *
430 452 * @param int $share_id Share id.
431 453 * @param array $row Share row.
432 454 * @param int $actor_id Acting user.
433 455 */
434 - do_action( 'desktop_mode_files_share_invited', $id, $row, $actor_id );
456 + do_action( 'openstation_files_share_invited', $id, $row, $actor_id );
435 457
436 458 return $id;
437 459 }
438 460
@@ -438,33 +460,31 @@
438 460
439 461 /**
440 462 * Revoke a share. Owner-side action.
441 463 *
442 - * @since 0.8.5
443 - *
444 464 * @param int $share_id Share id.
445 465 * @param int $actor_id Actor.
446 466 * @return true|WP_Error
447 467 */
448 -function desktop_mode_folder_share_revoke( $share_id, $actor_id ) {
468 +function openstation_folder_share_revoke( $share_id, $actor_id ) {
449 469 global $wpdb;
450 470 $share_id = (int) $share_id;
451 471 $actor_id = (int) $actor_id;
452 - $row = desktop_mode_files_get_share( $share_id );
472 + $row = openstation_files_get_share( $share_id );
453 473 if ( ! $row ) {
454 - 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 ) );
455 475 }
456 - if ( ! desktop_mode_files_share_can_manage( $row['folder_id'], $actor_id ) ) {
457 - 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 ) );
458 478 }
459 479
460 - $tables = desktop_mode_files_table_names();
480 + $tables = openstation_files_table_names();
461 481 $wpdb->delete( $tables['shares'], array( 'id' => $share_id ), array( '%d' ) );
462 482 // Drop any per-user decision rows attached to this share so
463 483 // they don't leak past the row deletion.
464 484 $wpdb->delete( $tables['decisions'], array( 'share_id' => $share_id ), array( '%d' ) );
465 485
466 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
486 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
467 487
468 488 // Scrub recipient's local view. For user-principal grants
469 489 // that's the single recipient; for role-principal grants we
470 490 // scrub every user who had a decision row (i.e. interacted
@@ -472,9 +492,9 @@
472 492 // also lose visibility but have no local placement to trash.
473 493 if ( 'user' === $row['principal_type'] && 'accepted' === $row['state'] ) {
474 494 $uid = (int) $row['principal_ref'];
475 495 if ( $uid > 0 ) {
476 - desktop_mode_files_trash_folder_for_user( $row['folder_id'], $uid );
496 + openstation_files_trash_folder_for_user( $row['folder_id'], $uid );
477 497 }
478 498 } elseif ( 'role' === $row['principal_type'] ) {
479 499 $decided_users = $wpdb->get_col(
480 500 $wpdb->prepare(
@@ -482,9 +502,9 @@
482 502 $share_id
483 503 )
484 504 );
485 505 foreach ( (array) $decided_users as $uid ) {
486 - desktop_mode_files_trash_folder_for_user( $row['folder_id'], (int) $uid );
506 + openstation_files_trash_folder_for_user( $row['folder_id'], (int) $uid );
487 507 }
488 508 }
489 509
490 510 /**
@@ -489,15 +509,13 @@
489 509
490 510 /**
491 511 * Fires after a share is revoked.
492 512 *
493 - * @since 0.8.5
494 - *
495 513 * @param int $share_id Share id.
496 514 * @param array $row Share row (last-known state).
497 515 * @param int $actor_id Acting user.
498 516 */
499 - do_action( 'desktop_mode_files_share_revoked', $share_id, $row, $actor_id );
517 + do_action( 'openstation_files_share_revoked', $share_id, $row, $actor_id );
500 518
501 519 return true;
502 520 }
503 521
@@ -503,50 +521,46 @@
503 521
504 522 /**
505 523 * Update the capability on a share. Owner-side action.
506 524 *
507 - * @since 0.8.5
508 - *
509 525 * @param int $share_id Share id.
510 526 * @param int $actor_id Actor.
511 527 * @param string $capability New capability.
512 528 * @return true|WP_Error
513 529 */
514 -function desktop_mode_folder_share_update_capability( $share_id, $actor_id, $capability ) {
530 +function openstation_folder_share_update_capability( $share_id, $actor_id, $capability ) {
515 531 global $wpdb;
516 532 $share_id = (int) $share_id;
517 533 $actor_id = (int) $actor_id;
518 534 $capability = (string) $capability;
519 535
520 - if ( ! in_array( $capability, desktop_mode_files_share_capabilities(), true ) ) {
521 - 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 ) );
522 538 }
523 - $row = desktop_mode_files_get_share( $share_id );
539 + $row = openstation_files_get_share( $share_id );
524 540 if ( ! $row ) {
525 - 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 ) );
526 542 }
527 - if ( ! desktop_mode_files_share_can_manage( $row['folder_id'], $actor_id ) ) {
528 - 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 ) );
529 545 }
530 546
531 - $tables = desktop_mode_files_table_names();
547 + $tables = openstation_files_table_names();
532 548 $wpdb->update( $tables['shares'], array( 'capability' => $capability ), array( 'id' => $share_id ), array( '%s' ), array( '%d' ) );
533 549
534 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
550 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
535 551
536 - $next = desktop_mode_files_get_share( $share_id );
552 + $next = openstation_files_get_share( $share_id );
537 553
538 554 /**
539 555 * Fires after a share's capability is changed.
540 556 *
541 - * @since 0.8.5
542 - *
543 557 * @param int $share_id Share id.
544 558 * @param array $next Row after.
545 559 * @param array $prev Row before.
546 560 * @param int $actor_id Acting user.
547 561 */
548 - 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 );
549 563
550 564 return true;
551 565 }
552 566
@@ -552,9 +566,8 @@
552 566
553 567 /**
554 568 * Read this user's decision row for a share (role-principal only).
555 569 *
556 - * @since 0.8.5
557 570 * @internal
558 571 *
559 572 * @param int $share_id Share id.
560 573 * @param int $user_id User.
@@ -559,11 +572,11 @@
559 572 * @param int $share_id Share id.
560 573 * @param int $user_id User.
561 574 * @return array|null Normalized decision row or null.
562 575 */
563 -function desktop_mode_files_get_user_decision( $share_id, $user_id ) {
576 +function openstation_files_get_user_decision( $share_id, $user_id ) {
564 577 global $wpdb;
565 - $tables = desktop_mode_files_table_names();
578 + $tables = openstation_files_table_names();
566 579 $row = $wpdb->get_row(
567 580 $wpdb->prepare(
568 581 "SELECT * FROM {$tables['decisions']} WHERE share_id = %d AND user_id = %d",
569 582 (int) $share_id,
@@ -585,9 +598,8 @@
585 598
586 599 /**
587 600 * Upsert a per-user decision (role-principal opt-in state).
588 601 *
589 - * @since 0.8.5
590 602 * @internal
591 603 *
592 604 * @param int $share_id Share id.
593 605 * @param int $user_id User.
@@ -592,12 +604,12 @@
592 604 * @param int $share_id Share id.
593 605 * @param int $user_id User.
594 606 * @param string $state 'pending' | 'accepted' | 'denied'.
595 607 */
596 -function desktop_mode_files_upsert_user_decision( $share_id, $user_id, $state ) {
608 +function openstation_files_upsert_user_decision( $share_id, $user_id, $state ) {
597 609 global $wpdb;
598 - $tables = desktop_mode_files_table_names();
599 - $now = desktop_mode_files_now_ms();
610 + $tables = openstation_files_table_names();
611 + $now = openstation_files_now_ms();
600 612 $wpdb->query(
601 613 $wpdb->prepare(
602 614 "INSERT INTO {$tables['decisions']}
603 615 (share_id, user_id, state, decided_at_ms)
@@ -617,20 +629,18 @@
617 629 * - user-principal: state lives on the share row itself.
618 630 * - role-principal: state lives on the per-user decisions
619 631 * table. Absence = 'pending' (user hasn't decided yet).
620 632 *
621 - * @since 0.8.5
622 - *
623 633 * @param array $share_row Normalized share row.
624 634 * @param int $user_id Viewer.
625 635 * @return string 'pending' | 'accepted' | 'denied'
626 636 */
627 -function desktop_mode_files_share_user_state( $share_row, $user_id ) {
637 +function openstation_files_share_user_state( $share_row, $user_id ) {
628 638 if ( 'user' === $share_row['principal_type'] ) {
629 639 return (string) $share_row['state'];
630 640 }
631 641 if ( 'role' === $share_row['principal_type'] ) {
632 - $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 );
633 643 if ( $dec ) {
634 644 return (string) $dec['state'];
635 645 }
636 646 return 'pending';
@@ -641,66 +651,65 @@
641 651 /**
642 652 * Recipient accepts a share. Creates the recipient's placement of
643 653 * the folder at their desktop root.
644 654 *
645 - * @since 0.8.5
646 - *
647 655 * @param int $share_id Share id.
648 656 * @param int $user_id Acting user (must be the share's principal).
649 657 * @return array|WP_Error Share row on success.
650 658 */
651 -function desktop_mode_folder_share_accept( $share_id, $user_id ) {
659 +function openstation_folder_share_accept( $share_id, $user_id ) {
652 660 global $wpdb;
653 661 $share_id = (int) $share_id;
654 662 $user_id = (int) $user_id;
655 - $row = desktop_mode_files_get_share( $share_id );
663 + $row = openstation_files_get_share( $share_id );
656 664 if ( ! $row || 'folder' !== $row['target_type'] ) {
657 - 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 ) );
658 666 }
659 - if ( ! desktop_mode_files_share_principal_matches_user( $row, $user_id ) ) {
660 - 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 ) );
661 669 }
662 - $state = desktop_mode_files_share_user_state( $row, $user_id );
670 + $state = openstation_files_share_user_state( $row, $user_id );
663 671 if ( 'accepted' === $state ) {
664 672 return $row;
665 673 }
666 674 if ( 'denied' === $state && 'user' === $row['principal_type'] ) {
667 - 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 ) );
668 676 }
669 677
670 - $tables = desktop_mode_files_table_names();
671 - $now = desktop_mode_files_now_ms();
678 + $tables = openstation_files_table_names();
679 + $now = openstation_files_now_ms();
672 680
673 681 if ( 'user' === $row['principal_type'] ) {
674 682 $wpdb->update(
675 683 $tables['shares'],
676 - array( 'state' => 'accepted', 'decided_at_ms' => $now ),
684 + array(
685 + 'state' => 'accepted',
686 + 'decided_at_ms' => $now,
687 + ),
677 688 array( 'id' => $share_id ),
678 689 array( '%s', '%d' ),
679 690 array( '%d' )
680 691 );
681 692 } else {
682 - desktop_mode_files_upsert_user_decision( $share_id, $user_id, 'accepted' );
693 + openstation_files_upsert_user_decision( $share_id, $user_id, 'accepted' );
683 694 }
684 695
685 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
696 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
686 697
687 698 // Place the folder on the recipient's desktop root.
688 - $parent_id = (int) apply_filters( 'desktop_mode_folder_share_accept_default_parent', 0, $row['folder_id'], $user_id, $row );
689 - 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'] );
690 701
691 - $next = desktop_mode_files_get_share( $share_id );
702 + $next = openstation_files_get_share( $share_id );
692 703
693 704 /**
694 705 * Fires after a share is accepted by its recipient.
695 706 *
696 - * @since 0.8.5
697 - *
698 707 * @param int $share_id Share id.
699 708 * @param array $row Updated share row.
700 709 * @param int $user_id Acting user (recipient).
701 710 */
702 - do_action( 'desktop_mode_files_share_accepted', $share_id, $next, $user_id );
711 + do_action( 'openstation_files_share_accepted', $share_id, $next, $user_id );
703 712
704 713 return $next;
705 714 }
706 715
@@ -706,37 +715,38 @@
706 715
707 716 /**
708 717 * Recipient denies a share.
709 718 *
710 - * @since 0.8.5
711 - *
712 719 * @param int $share_id Share id.
713 720 * @param int $user_id Recipient.
714 721 * @return array|WP_Error Share row on success.
715 722 */
716 -function desktop_mode_folder_share_deny( $share_id, $user_id ) {
723 +function openstation_folder_share_deny( $share_id, $user_id ) {
717 724 global $wpdb;
718 725 $share_id = (int) $share_id;
719 726 $user_id = (int) $user_id;
720 - $row = desktop_mode_files_get_share( $share_id );
727 + $row = openstation_files_get_share( $share_id );
721 728 if ( ! $row || 'folder' !== $row['target_type'] ) {
722 - 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 ) );
723 730 }
724 - if ( ! desktop_mode_files_share_principal_matches_user( $row, $user_id ) ) {
725 - 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 ) );
726 733 }
727 - $state = desktop_mode_files_share_user_state( $row, $user_id );
734 + $state = openstation_files_share_user_state( $row, $user_id );
728 735 if ( 'denied' === $state ) {
729 736 return $row;
730 737 }
731 738
732 - $tables = desktop_mode_files_table_names();
733 - $now = desktop_mode_files_now_ms();
739 + $tables = openstation_files_table_names();
740 + $now = openstation_files_now_ms();
734 741
735 742 if ( 'user' === $row['principal_type'] ) {
736 743 $wpdb->update(
737 744 $tables['shares'],
738 - array( 'state' => 'denied', 'decided_at_ms' => $now ),
745 + array(
746 + 'state' => 'denied',
747 + 'decided_at_ms' => $now,
748 + ),
739 749 array( 'id' => $share_id ),
740 750 array( '%s', '%d' ),
741 751 array( '%d' )
742 752 );
@@ -741,12 +751,12 @@
741 751 array( '%d' )
742 752 );
743 753 } else {
744 754 // Role-principal: per-user decision keeps other role members untouched.
745 - desktop_mode_files_upsert_user_decision( $share_id, $user_id, 'denied' );
755 + openstation_files_upsert_user_decision( $share_id, $user_id, 'denied' );
746 756 }
747 757
748 - desktop_mode_files_bump_folder_updated_at( $row['folder_id'] );
758 + openstation_files_bump_folder_updated_at( $row['folder_id'] );
749 759
750 760 // If the recipient had previously accepted and is now denying
751 761 // (e.g. they hit deny on a placeholder they already opened),
752 762 // scrub their local placement too. Works for BOTH user- and
@@ -751,23 +761,21 @@
751 761 // (e.g. they hit deny on a placeholder they already opened),
752 762 // scrub their local placement too. Works for BOTH user- and
753 763 // role-principals since the trash helper is user-scoped.
754 764 if ( 'accepted' === $state ) {
755 - desktop_mode_files_trash_folder_for_user( $row['folder_id'], $user_id );
765 + openstation_files_trash_folder_for_user( $row['folder_id'], $user_id );
756 766 }
757 767
758 - $next = desktop_mode_files_get_share( $share_id );
768 + $next = openstation_files_get_share( $share_id );
759 769
760 770 /**
761 771 * Fires after a share is denied.
762 772 *
763 - * @since 0.8.5
764 - *
765 773 * @param int $share_id Share id.
766 774 * @param array $row Updated share row.
767 775 * @param int $user_id Acting user (recipient).
768 776 */
769 - do_action( 'desktop_mode_files_share_denied', $share_id, $next, $user_id );
777 + do_action( 'openstation_files_share_denied', $share_id, $next, $user_id );
770 778
771 779 return $next;
772 780 }
773 781
@@ -776,34 +784,32 @@
776 784 * the user access to `$folder_id` (user-principal or matching
777 785 * role-principal) and marks them as denied, then scrubs their
778 786 * local placements. Idempotent — no-op if the user has no share.
779 787 *
780 - * @since 0.8.5
781 - *
782 788 * @param int $folder_id Folder id.
783 789 * @param int $user_id Recipient leaving.
784 790 * @return true|WP_Error
785 791 */
786 -function desktop_mode_folder_share_leave( $folder_id, $user_id ) {
792 +function openstation_folder_share_leave( $folder_id, $user_id ) {
787 793 global $wpdb;
788 794 $folder_id = (int) $folder_id;
789 795 $user_id = (int) $user_id;
790 796 if ( $folder_id <= 0 || $user_id <= 0 ) {
791 - 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 ) );
792 798 }
793 - $folder = desktop_mode_files_get_folder( $folder_id );
799 + $folder = openstation_files_get_folder( $folder_id );
794 800 if ( ! $folder ) {
795 - 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 ) );
796 802 }
797 803 if ( (int) $folder['owner_id'] === $user_id ) {
798 - 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 ) );
799 805 }
800 806
801 807 $user = get_userdata( $user_id );
802 808 $roles = $user ? (array) $user->roles : array();
803 809
804 - $tables = desktop_mode_files_table_names();
805 - $rows = $wpdb->get_results(
810 + $tables = openstation_files_table_names();
811 + $rows = $wpdb->get_results(
806 812 $wpdb->prepare(
807 813 "SELECT * FROM {$tables['shares']} WHERE target_type = 'folder' AND folder_id = %d",
808 814 $folder_id
809 815 ),
@@ -810,46 +816,47 @@
810 816 ARRAY_A
811 817 );
812 818 $touched = 0;
813 819 foreach ( (array) $rows as $raw ) {
814 - $row = desktop_mode_files_normalize_share_row( $raw );
815 - 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 ) ) {
816 822 continue;
817 823 }
818 824 if ( 'user' === $row['principal_type'] ) {
819 825 $wpdb->update(
820 826 $tables['shares'],
821 - 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 + ),
822 831 array( 'id' => $row['id'] ),
823 832 array( '%s', '%d' ),
824 833 array( '%d' )
825 834 );
826 835 } else {
827 - desktop_mode_files_upsert_user_decision( $row['id'], $user_id, 'denied' );
836 + openstation_files_upsert_user_decision( $row['id'], $user_id, 'denied' );
828 837 }
829 - $touched++;
838 + ++$touched;
830 839 /**
831 840 * Fires after a recipient leaves a shared folder. Distinct
832 841 * from `_denied` (owner-side audit) because this is always
833 842 * recipient-initiated, after acceptance.
834 843 *
835 - * @since 0.8.5
836 - *
837 844 * @param int $share_id Share id.
838 845 * @param array $row Share row (last-known).
839 846 * @param int $user_id Recipient leaving.
840 847 */
841 - do_action( 'desktop_mode_files_share_left', $row['id'], $row, $user_id );
848 + do_action( 'openstation_files_share_left', $row['id'], $row, $user_id );
842 849 }
843 850
844 851 // Scrub the recipient's view regardless of whether a share row
845 852 // matched (the user might have a placement from a previously
846 853 // revoked share that lingered).
847 - desktop_mode_files_trash_folder_for_user( $folder_id, $user_id );
848 - 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 );
849 856
850 857 if ( 0 === $touched ) {
851 - 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 ) );
852 859 }
853 860 return true;
854 861 }
855 862
@@ -855,9 +862,8 @@
855 862
856 863 /**
857 864 * Does `$share_row` target `$user_id` (directly or by role)?
858 865 *
859 - * @since 0.8.5
860 866 * @internal
861 867 *
862 868 * @param array $share_row Normalized share row.
863 869 * @param int $user_id User to test.
@@ -862,9 +868,9 @@
862 868 * @param array $share_row Normalized share row.
863 869 * @param int $user_id User to test.
864 870 * @return bool
865 871 */
866 -function desktop_mode_files_share_principal_matches_user( $share_row, $user_id ) {
872 +function openstation_files_share_principal_matches_user( $share_row, $user_id ) {
867 873 $user_id = (int) $user_id;
868 874 if ( $user_id <= 0 ) {
869 875 return false;
870 876 }
@@ -885,15 +891,13 @@
885 891 * Capability `$user_id` holds on `$folder_id`. `write` beats `read`
886 892 * beats `none`. Owner always returns `'write'`. `share_mode='all'`
887 893 * yields a default of `'read'` (filterable).
888 894 *
889 - * @since 0.8.5
890 - *
891 895 * @param int $folder_id Folder id.
892 896 * @param int $user_id Viewer.
893 897 * @return string 'none' | 'read' | 'write'
894 898 */
895 -function desktop_mode_folder_share_user_capability( $folder_id, $user_id ) {
899 +function openstation_folder_share_user_capability( $folder_id, $user_id ) {
896 900 $folder_id = (int) $folder_id;
897 901 $user_id = (int) $user_id;
898 902 if ( $folder_id <= 0 || $user_id <= 0 ) {
899 903 return 'none';
@@ -898,9 +902,9 @@
898 902 if ( $folder_id <= 0 || $user_id <= 0 ) {
899 903 return 'none';
900 904 }
901 905
902 - $folder = desktop_mode_files_get_folder( $folder_id );
906 + $folder = openstation_files_get_folder( $folder_id );
903 907 if ( ! $folder ) {
904 908 return 'none';
905 909 }
906 910 if ( (int) $folder['owner_id'] === $user_id ) {
@@ -909,9 +913,9 @@
909 913
910 914 // Cascade — walk the folder's ancestor chain. A folder nested
911 915 // inside a shared folder inherits the share. The most permissive
912 916 // ancestor cap wins. Bail out as soon as we hit 'write'.
913 - $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 );
914 918 if ( 'write' === $cascade_cap ) {
915 919 return 'write';
916 920 }
917 921
@@ -919,15 +923,13 @@
919 923 if ( 'all' === $folder['share_mode'] ) {
920 924 /**
921 925 * Filter the default capability for `share_mode='all'`.
922 926 *
923 - * @since 0.8.5
924 - *
925 927 * @param string $cap Default 'read'.
926 928 * @param int $folder_id Folder id.
927 929 * @param int $user_id Viewer.
928 930 */
929 - $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 );
930 932 }
931 933
932 934 $user_roles = array();
933 935 $user = get_userdata( $user_id );
@@ -935,9 +937,9 @@
935 937 $user_roles = (array) $user->roles;
936 938 }
937 939
938 940 global $wpdb;
939 - $tables = desktop_mode_files_table_names();
941 + $tables = openstation_files_table_names();
940 942 // User-principal grants — state lives on the shares row.
941 943 $rows = $wpdb->get_results(
942 944 $wpdb->prepare(
943 945 "SELECT id, principal_type, principal_ref, capability FROM {$tables['shares']}
@@ -959,9 +961,9 @@
959 961 $folder_id
960 962 ),
961 963 ARRAY_A
962 964 );
963 - $rows = array_merge( (array) $rows, (array) $role_rows );
965 + $rows = array_merge( (array) $rows, (array) $role_rows );
964 966 foreach ( $rows as $row ) {
965 967 $matches = false;
966 968 if ( 'user' === $row['principal_type'] && (int) $row['principal_ref'] === $user_id ) {
967 969 $matches = true;
@@ -990,22 +992,20 @@
990 992
991 993 /**
992 994 * Filter the resolved capability.
993 995 *
994 - * @since 0.8.5
995 - *
996 996 * @param string $cap 'none' | 'read' | 'write'.
997 997 * @param int $folder_id Folder id.
998 998 * @param int $user_id Viewer.
999 999 * @param array $folder Normalized folder row.
1000 1000 */
1001 - 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 );
1002 1002 }
1003 1003
1004 1004 /**
1005 1005 * Walk the ancestor chain of `$folder_id` and return the most
1006 1006 * permissive DIRECT share cap any ancestor has for `$user_id`.
1007 - * Used by `desktop_mode_folder_share_user_capability` to cascade
1007 + * Used by `openstation_folder_share_user_capability` to cascade
1008 1008 * a share grant from a parent folder into every folder nested
1009 1009 * inside it.
1010 1010 *
1011 1011 * "Direct" means the share row exists for that ancestor — we
@@ -1019,23 +1019,21 @@
1019 1019 * `shares IN (…) JOIN decisions` for role-principal accepted
1020 1020 * rows. Replaces the previous loop that fired up to two queries
1021 1021 * per ancestor (32 on cold caches at the 16-level cap).
1022 1022 *
1023 - * @since 0.8.5
1024 - *
1025 1023 * @param int $folder_id Folder whose ancestors to walk.
1026 1024 * @param int $user_id Viewer.
1027 1025 * @return string 'none' | 'read' | 'write'
1028 1026 */
1029 -function desktop_mode_folder_share_user_capability_cascade( $folder_id, $user_id ) {
1027 +function openstation_folder_share_user_capability_cascade( $folder_id, $user_id ) {
1030 1028 $user_id = (int) $user_id;
1031 - $ancestors = desktop_mode_folder_ancestors( (int) $folder_id );
1029 + $ancestors = openstation_folder_ancestors( (int) $folder_id );
1032 1030 if ( empty( $ancestors ) || $user_id <= 0 ) {
1033 1031 return 'none';
1034 1032 }
1035 1033
1036 1034 global $wpdb;
1037 - $tables = desktop_mode_files_table_names();
1035 + $tables = openstation_files_table_names();
1038 1036
1039 1037 // Coerce + dedupe to keep the IN clause small and safe to
1040 1038 // interpolate. Every value is an int by the time it lands
1041 1039 // in the SQL.
@@ -1056,9 +1054,9 @@
1056 1054 return 'write';
1057 1055 }
1058 1056 if ( 'all' === $f['share_mode'] ) {
1059 1057 $all_cap = (string) apply_filters(
1060 - 'desktop_mode_files_share_all_default_capability',
1058 + 'openstation_files_share_all_default_capability',
1061 1059 'read',
1062 1060 (int) $f['id'],
1063 1061 $user_id
1064 1062 );
@@ -1131,27 +1129,26 @@
1131 1129 }
1132 1130
1133 1131 /**
1134 1132 * Direct (non-cascading) capability resolver. Same logic as
1135 - * `desktop_mode_folder_share_user_capability` minus the cascade
1133 + * `openstation_folder_share_user_capability` minus the cascade
1136 1134 * walk — owner / share rows / role decisions only.
1137 1135 *
1138 1136 * Currently uncalled: the cascade resolver
1139 - * (`desktop_mode_folder_share_user_capability_cascade`) resolves
1137 + * (`openstation_folder_share_user_capability_cascade`) resolves
1140 1138 * every ancestor via three batched `IN (…)` queries instead of
1141 1139 * querying ancestors one at a time. Kept as a single-folder,
1142 1140 * non-cascading resolver.
1143 1141 *
1144 - * @since 0.8.5
1145 1142 * @internal
1146 1143 */
1147 -function desktop_mode_folder_share_user_capability_direct( $folder_id, $user_id ) {
1144 +function openstation_folder_share_user_capability_direct( $folder_id, $user_id ) {
1148 1145 $folder_id = (int) $folder_id;
1149 1146 $user_id = (int) $user_id;
1150 1147 if ( $folder_id <= 0 || $user_id <= 0 ) {
1151 1148 return 'none';
1152 1149 }
1153 - $folder = desktop_mode_files_get_folder( $folder_id );
1150 + $folder = openstation_files_get_folder( $folder_id );
1154 1151 if ( ! $folder ) {
1155 1152 return 'none';
1156 1153 }
1157 1154 if ( (int) $folder['owner_id'] === $user_id ) {
@@ -1158,9 +1155,9 @@
1158 1155 return 'write';
1159 1156 }
1160 1157 $cap = 'none';
1161 1158 if ( 'all' === $folder['share_mode'] ) {
1162 - $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 );
1163 1160 }
1164 1161
1165 1162 $user_roles = array();
1166 1163 $user = get_userdata( $user_id );
@@ -1168,10 +1165,10 @@
1168 1165 $user_roles = (array) $user->roles;
1169 1166 }
1170 1167
1171 1168 global $wpdb;
1172 - $tables = desktop_mode_files_table_names();
1173 - $rows = $wpdb->get_results(
1169 + $tables = openstation_files_table_names();
1170 + $rows = $wpdb->get_results(
1174 1171 $wpdb->prepare(
1175 1172 "SELECT id, principal_type, principal_ref, capability FROM {$tables['shares']}
1176 1173 WHERE target_type = 'folder' AND folder_id = %d AND principal_type = 'user' AND state = 'accepted'",
1177 1174 $folder_id
@@ -1220,21 +1217,19 @@
1220 1217 *
1221 1218 * Hard-capped at 16 levels deep + a visited set to make pathological
1222 1219 * inputs (cycles, deep nests) bounded.
1223 1220 *
1224 - * @since 0.8.5
1225 - *
1226 1221 * @param int $folder_id Folder whose ancestors to walk.
1227 1222 * @param int $limit Max ancestor count (default 16).
1228 1223 * @return int[]
1229 1224 */
1230 -function desktop_mode_folder_ancestors( $folder_id, $limit = 16 ) {
1225 +function openstation_folder_ancestors( $folder_id, $limit = 16 ) {
1231 1226 global $wpdb;
1232 1227 $folder_id = (int) $folder_id;
1233 1228 if ( $folder_id <= 0 ) {
1234 1229 return array();
1235 1230 }
1236 - $tables = desktop_mode_files_table_names();
1231 + $tables = openstation_files_table_names();
1237 1232 $ancestors = array();
1238 1233 $current = $folder_id;
1239 1234 $visited = array();
1240 1235 $depth = 0;
@@ -1242,9 +1237,9 @@
1242 1237 if ( isset( $visited[ $current ] ) ) {
1243 1238 break;
1244 1239 }
1245 1240 $visited[ $current ] = true;
1246 - $folder = desktop_mode_files_get_folder( $current );
1241 + $folder = openstation_files_get_folder( $current );
1247 1242 if ( ! $folder ) {
1248 1243 break;
1249 1244 }
1250 1245 $owner = (int) $folder['owner_id'];
@@ -1270,9 +1265,9 @@
1270 1265 break;
1271 1266 }
1272 1267 $ancestors[] = $parent_id;
1273 1268 $current = $parent_id;
1274 - $depth++;
1269 + ++$depth;
1275 1270 }
1276 1271 return $ancestors;
1277 1272 }
1278 1273
@@ -1279,15 +1274,13 @@
1279 1274 /**
1280 1275 * Pending invites for `$user_id` across every folder. Used by the
1281 1276 * heartbeat `shares.pending` payload.
1282 1277 *
1283 - * @since 0.8.5
1284 - *
1285 1278 * @param int $user_id Viewer.
1286 1279 * @param int $since_ms Optional. Only include rows with `invited_at_ms > since`.
1287 1280 * @return array[]
1288 1281 */
1289 -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 ) {
1290 1283 global $wpdb;
1291 1284 $user_id = (int) $user_id;
1292 1285 $since_ms = (int) $since_ms;
1293 1286 if ( $user_id <= 0 ) {
@@ -1298,9 +1291,9 @@
1298 1291 return array();
1299 1292 }
1300 1293 $roles = (array) $user->roles;
1301 1294
1302 - $tables = desktop_mode_files_table_names();
1295 + $tables = openstation_files_table_names();
1303 1296
1304 1297 // User-principal: state lives on the share row. Surface where
1305 1298 // state='pending' AND principal_ref matches the user.
1306 1299 $user_pending = $wpdb->get_results(
@@ -1345,9 +1338,9 @@
1345 1338 }
1346 1339
1347 1340 $out = array();
1348 1341 foreach ( array_merge( (array) $user_pending, (array) $role_pending ) as $row ) {
1349 - $out[] = desktop_mode_files_normalize_share_row( $row );
1342 + $out[] = openstation_files_normalize_share_row( $row );
1350 1343 }
1351 1344 return $out;
1352 1345 }
1353 1346
@@ -1355,19 +1348,18 @@
1355 1348 * Bump a folder's `updated_at_ms`. Internal helper — every share
1356 1349 * mutation should bump the parent folder so heartbeat clients pick
1357 1350 * up the change in the same delta window.
1358 1351 *
1359 - * @since 0.8.5
1360 1352 * @internal
1361 1353 *
1362 1354 * @param int $folder_id Folder id.
1363 1355 */
1364 -function desktop_mode_files_bump_folder_updated_at( $folder_id ) {
1356 +function openstation_files_bump_folder_updated_at( $folder_id ) {
1365 1357 global $wpdb;
1366 - $tables = desktop_mode_files_table_names();
1358 + $tables = openstation_files_table_names();
1367 1359 $wpdb->update(
1368 1360 $tables['folders'],
1369 - array( 'updated_at_ms' => desktop_mode_files_now_ms() ),
1361 + array( 'updated_at_ms' => openstation_files_now_ms() ),
1370 1362 array( 'id' => (int) $folder_id ),
1371 1363 array( '%d' ),
1372 1364 array( '%d' )
1373 1365 );
@@ -1377,15 +1369,13 @@
1377 1369 * Recipient-scoped trash. Removes the recipient's placement of the
1378 1370 * folder + their placements INSIDE the folder. Does NOT cascade
1379 1371 * into the shared icon namespace (other users' placements survive).
1380 1372 *
1381 - * @since 0.8.5
1382 - *
1383 1373 * @param int $folder_id Folder id.
1384 1374 * @param int $user_id Recipient.
1385 1375 * @return int Number of placement rows trashed.
1386 1376 */
1387 -function desktop_mode_files_trash_folder_for_user( $folder_id, $user_id ) {
1377 +function openstation_files_trash_folder_for_user( $folder_id, $user_id ) {
1388 1378 global $wpdb;
1389 1379 $folder_id = (int) $folder_id;
1390 1380 $user_id = (int) $user_id;
1391 1381 if ( $folder_id <= 0 || $user_id <= 0 ) {
@@ -1390,15 +1380,15 @@
1390 1380 $user_id = (int) $user_id;
1391 1381 if ( $folder_id <= 0 || $user_id <= 0 ) {
1392 1382 return 0;
1393 1383 }
1394 - $tables = desktop_mode_files_table_names();
1395 - $now = desktop_mode_files_now_ms();
1384 + $tables = openstation_files_table_names();
1385 + $now = openstation_files_now_ms();
1396 1386
1397 1387 // The recipient's folder-shortcut placement (parent_id=0,
1398 1388 // file_type='folder', file_ref=$folder_id) + every placement
1399 1389 // they own INSIDE this folder (parent_id=$folder_id).
1400 - $rows = $wpdb->get_results(
1390 + $rows = $wpdb->get_results(
1401 1391 $wpdb->prepare(
1402 1392 "SELECT id FROM {$tables['placements']}
1403 1393 WHERE owner_id = %d
1404 1394 AND trashed_at_ms IS NULL
@@ -1428,9 +1418,9 @@
1428 1418 // Soft-trash only — DO NOT write a tombstone here.
1429 1419 // Tombstones represent permanent removal (hard delete); the
1430 1420 // heartbeat already surfaces soft-trashed rows via the
1431 1421 // `trashed_at_ms IS NOT NULL` query in
1432 - // `desktop_mode_files_compute_heartbeat_delta`. Writing a
1422 + // `openstation_files_compute_heartbeat_delta`. Writing a
1433 1423 // tombstone on every soft-trash conflates the two states and,
1434 1424 // when the row is later restored (e.g. the recipient re-
1435 1425 // accepts the same share), the lingering tombstone keeps
1436 1426 // telling clients "this is gone" while the same placement
@@ -1436,10 +1426,10 @@
1436 1426 // telling clients "this is gone" while the same placement
1437 1427 // row is also being upserted as alive — causing the row to
1438 1428 // disappear from the desktop on every heartbeat tick. See
1439 1429 // the user-reported "shared folder vanishes after refresh"
1440 - // bug fixed in 0.8.5.
1441 - $count++;
1430 + // bug.
1431 + ++$count;
1442 1432 }
1443 1433 return $count;
1444 1434 }
1445 1435
@@ -1448,16 +1438,14 @@
1448 1438 * placements they "own" inside a shared folder. The ownership
1449 1439 * check at the placement level passes (each user has their own
1450 1440 * placement row), so the gate needs an extra read-only veto.
1451 1441 *
1452 - * @since 0.8.5
1453 - *
1454 1442 * @param bool $can Default decision (ownership match).
1455 1443 * @param int $user_id Acting user.
1456 1444 * @param array $row Placement row.
1457 1445 * @return bool
1458 1446 */
1459 -function desktop_mode_files_share_gate_trash( $can, $user_id, $row ) {
1447 +function openstation_files_share_gate_trash( $can, $user_id, $row ) {
1460 1448 $parent_id = isset( $row['parent_id'] ) ? (int) $row['parent_id'] : 0;
1461 1449 $user_id = (int) $user_id;
1462 1450
1463 1451 // Root-level placement of a SHARED FOLDER (the recipient's
@@ -1478,9 +1466,9 @@
1478 1466 isset( $row['file_ref'] )
1479 1467 ) {
1480 1468 $folder_ref = (int) $row['file_ref'];
1481 1469 if ( $folder_ref > 0 ) {
1482 - $folder_row = desktop_mode_files_get_folder( $folder_ref );
1470 + $folder_row = openstation_files_get_folder( $folder_ref );
1483 1471 if (
1484 1472 $folder_row &&
1485 1473 (int) $folder_row['owner_id'] !== $user_id
1486 1474 ) {
@@ -1491,9 +1479,9 @@
1491 1479 // a writer's destructive intent should be expressed
1492 1480 // via the leave flow (which scrubs their own
1493 1481 // placement) instead of via Move to Trash (which is
1494 1482 // reserved for the owner's destructive cascade).
1495 - $root_cap = desktop_mode_folder_share_user_capability( $folder_ref, $user_id );
1483 + $root_cap = openstation_folder_share_user_capability( $folder_ref, $user_id );
1496 1484 if ( 'none' !== $root_cap ) {
1497 1485 return false;
1498 1486 }
1499 1487 }
@@ -1505,14 +1493,14 @@
1505 1493 // Any other root placement (not a shared-folder tile) —
1506 1494 // default ownership rule stands.
1507 1495 return $can;
1508 1496 }
1509 - $folder = desktop_mode_files_get_folder( $parent_id );
1497 + $folder = openstation_files_get_folder( $parent_id );
1510 1498 if ( ! $folder ) {
1511 1499 return $can;
1512 1500 }
1513 1501 $is_owner = (int) $folder['owner_id'] === $user_id;
1514 - $cap = desktop_mode_folder_share_user_capability( $parent_id, $user_id );
1502 + $cap = openstation_folder_share_user_capability( $parent_id, $user_id );
1515 1503
1516 1504 // Folder owner can always trash anything inside their folder.
1517 1505 if ( $is_owner ) {
1518 1506 return true;
@@ -1522,9 +1510,9 @@
1522 1510 // writers can trash any icon in the shared folder. Readers
1523 1511 // can't trash anything (even their own placement in this folder).
1524 1512 return 'write' === $cap;
1525 1513 }
1526 -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 );
1527 1515
1528 1516 /**
1529 1517 * Inject share-related state into the shell config so the share
1530 1518 * settings modal + role picker have what they need without
@@ -1529,18 +1517,16 @@
1529 1517 * Inject share-related state into the shell config so the share
1530 1518 * settings modal + role picker have what they need without
1531 1519 * round-tripping.
1532 1520 *
1533 - * @since 0.8.5
1534 - *
1535 1521 * @param array $config Shell config.
1536 1522 * @return array
1537 1523 */
1538 -function desktop_mode_files_share_inject_shell_config( $config ) {
1524 +function openstation_files_share_inject_shell_config( $config ) {
1539 1525 if ( ! is_array( $config ) ) {
1540 1526 $config = array();
1541 1527 }
1542 - $config['shareEligibleRoles'] = desktop_mode_files_share_eligible_roles();
1528 + $config['shareEligibleRoles'] = openstation_files_share_eligible_roles();
1543 1529 $config['filesUsersSearchUrl'] = esc_url_raw( rest_url( 'desktop-mode/v1/files/users/search' ) );
1544 1530 $config['folderSharesUrl'] = esc_url_raw( rest_url( 'desktop-mode/v1/files/folders' ) );
1545 1531 $user_id = (int) get_current_user_id();
1546 1532 if ( ! isset( $config['currentUserId'] ) ) {
@@ -1550,28 +1536,28 @@
1550 1536 // Seed the shares store with the viewer's current pending invites
1551 1537 // on the first paint, so the accept/deny modal opens immediately
1552 1538 // on refresh instead of waiting for the first heartbeat tick to
1553 1539 // deliver them. Same kill-switch + shape as the heartbeat path —
1554 - // see `desktop_mode_files_collect_heartbeat_delta()` for the
1540 + // see `openstation_files_collect_heartbeat_delta()` for the
1555 1541 // canonical builder.
1556 1542 $pending = array();
1557 - $sharing_enabled = function_exists( 'desktop_mode_files_sharing_enabled_for' )
1558 - ? 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 )
1559 1545 : true;
1560 1546 if (
1561 1547 $user_id > 0 &&
1562 1548 $sharing_enabled &&
1563 - function_exists( 'desktop_mode_files_get_pending_shares_for_user' )
1549 + function_exists( 'openstation_files_get_pending_shares_for_user' )
1564 1550 ) {
1565 - $rows = desktop_mode_files_get_pending_shares_for_user( $user_id, 0 );
1551 + $rows = openstation_files_get_pending_shares_for_user( $user_id, 0 );
1566 1552 foreach ( $rows as $row ) {
1567 - $shape = desktop_mode_files_shape_share( $row );
1568 - $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'] );
1569 1555 if ( $folder ) {
1570 1556 $shape['folderName'] = (string) $folder['name'];
1571 1557 $shape['ownerId'] = (int) $folder['owner_id'];
1572 1558 $owner_user = get_userdata( (int) $folder['owner_id'] );
1573 - $shape['ownerName'] = $owner_user ? $owner_user->display_name : '';
1559 + $shape['ownerName'] = $owner_user ? openstation_plain_text_title( $owner_user->display_name ) : '';
1574 1560 $shape['ownerAvatar'] = $owner_user ? get_avatar_url( $owner_user->ID, array( 'size' => 48 ) ) : '';
1575 1561 }
1576 1562 $pending[] = $shape;
1577 1563 }
@@ -1579,17 +1565,18 @@
1579 1565 $config['serverPendingShares'] = $pending;
1580 1566
1581 1567 return $config;
1582 1568 }
1583 -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 );
1584 1570
1585 1571 /**
1586 - * Place an icon at the next free row-major slot in a user's view
1587 - * of `$parent_id`. Internal helper used by share-accept and
1588 - * fan-out. Mirrors the grid math in `src/desktop-files/grid.ts`
1589 - * (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.
1590 1574 *
1591 - * @since 0.8.5
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`.
1592 1579 *
1593 1580 * @param int $user_id Viewer.
1594 1581 * @param int $parent_id Folder id (0 = desktop root).
1595 1582 * @param string $type File-type slug.
@@ -1595,14 +1582,14 @@
1595 1582 * @param string $type File-type slug.
1596 1583 * @param string $ref Entity reference.
1597 1584 * @return int|WP_Error Placement id or error.
1598 1585 */
1599 -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 ) {
1600 1587 global $wpdb;
1601 1588 $user_id = (int) $user_id;
1602 1589 $parent_id = max( 0, (int) $parent_id );
1603 1590
1604 - $tables = desktop_mode_files_table_names();
1591 + $tables = openstation_files_table_names();
1605 1592 $existing = $wpdb->get_results(
1606 1593 $wpdb->prepare(
1607 1594 "SELECT x, y FROM {$tables['placements']}
1608 1595 WHERE owner_id = %d
@@ -1612,34 +1599,19 @@
1612 1599 $parent_id
1613 1600 ),
1614 1601 ARRAY_A
1615 1602 );
1616 - $occupied = array();
1617 - foreach ( (array) $existing as $row ) {
1618 - $col = max( 0, (int) round( ( (int) $row['x'] - 16 ) / 96 ) );
1619 - $r = max( 0, (int) round( ( (int) $row['y'] - 16 ) / 110 ) );
1620 - $occupied[ "$col,$r" ] = true;
1621 - }
1603 + $occupied = openstation_files_grid_occupied( $existing );
1622 1604
1623 - $pick_col = 0;
1624 - $pick_row = 0;
1625 - for ( $r = 0; $r < 999; $r++ ) {
1626 - for ( $col = 0; $col < 999; $col++ ) {
1627 - if ( ! isset( $occupied[ "$col,$r" ] ) ) {
1628 - $pick_col = $col;
1629 - $pick_row = $r;
1630 - break 2;
1631 - }
1632 - }
1633 - }
1605 + list( $pick_col, $pick_row ) = openstation_files_grid_next_free(
1606 + $occupied,
1607 + openstation_files_grid_order( $parent_id )
1608 + );
1634 1609
1635 - return desktop_mode_files_place(
1610 + return openstation_files_place(
1636 1611 $user_id,
1637 1612 $parent_id,
1638 1613 $type,
1639 1614 $ref,
1640 - array(
1641 - 'x' => 16 + $pick_col * 96,
1642 - 'y' => 16 + $pick_row * 110,
1643 - )
1615 + openstation_files_grid_cell_to_point( $pick_col, $pick_row )
1644 1616 );
1645 1617 }