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