PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/desktop-files/rest.php +552 -361 0.9.11.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Files REST routes.
3 + * OpenStation — Files REST routes.
4 4 *
5 5 * Routes under `/desktop-mode/v1/files`:
6 6 *
7 7 * GET /placements?folder=<id> List the viewer's placements
@@ -14,29 +14,53 @@
14 14 * POST /folders Create a folder.
15 15 * PATCH /folders/(?P<id>\d+) Update a folder.
16 16 * DELETE /folders/(?P<id>\d+) Delete a folder.
17 17 *
18 + * GET /folders/(?P<id>\d+)/shares List a folder's shares
19 + * (owner only).
20 + * POST /folders/(?P<id>\d+)/shares Invite a user/role (owner only).
21 + * PATCH /folders/(?P<id>\d+)/shares/(?P<shareId>\d+)
22 + * Change a share's capability.
23 + * DELETE /folders/(?P<id>\d+)/shares/(?P<shareId>\d+)
24 + * Revoke a share.
25 + * POST /folders/(?P<id>\d+)/shares/(?P<shareId>\d+)/accept
26 + * Accept an invite (recipient).
27 + * POST /folders/(?P<id>\d+)/shares/(?P<shareId>\d+)/deny
28 + * Deny an invite (recipient).
29 + * POST /folders/(?P<id>\d+)/leave Leave a shared folder
30 + * (recipient).
31 + *
32 + * GET /users/search Share-picker autocomplete.
33 + * POST /folder-sharing-tables/purge
34 + * Drop the folder-sharing tables
35 + * (site admin only).
36 + *
18 37 * PUT /associations Replace the viewer's full
19 38 * `{ type => opener_id }` map.
20 39 *
21 40 * Permission: every route requires a logged-in user with desktop
22 - * mode enabled. Per-row gating happens inside the store.
41 + * mode enabled. Per-row gating happens inside the store. On top of
42 + * that base, the share/accept/deny/leave routes also gate on the
43 + * viewer's folder-sharing OS Setting via
44 + * `openstation_files_rest_share_permission`, `/users/search`
45 + * additionally requires `edit_posts`, and the sharing-tables purge
46 + * requires `manage_options`.
23 47 *
24 - * @package WPDesktopMode
25 - * @since 0.9.0
48 + * @package OpenStation
26 49 */
27 50
28 51 defined( 'ABSPATH' ) || exit;
29 52
30 53 /**
31 - * @since 0.9.0
54 + * Base permission check shared by the desktop-files REST routes:
55 + * requires a logged-in user with OpenStation enabled.
32 56 */
33 -function desktop_mode_files_rest_permission() {
57 +function openstation_files_rest_permission() {
34 58 if ( ! is_user_logged_in() ) {
35 - return new WP_Error( 'desktop_mode_files_unauthenticated', __( 'You must be logged in.', 'desktop-mode' ), array( 'status' => 401 ) );
59 + return new WP_Error( 'openstation_files_unauthenticated', __( 'You must be logged in.', 'desktop-mode' ), array( 'status' => 401 ) );
36 60 }
37 - if ( function_exists( 'desktop_mode_is_enabled' ) && ! desktop_mode_is_enabled( get_current_user_id() ) ) {
38 - return new WP_Error( 'desktop_mode_files_disabled', __( 'Desktop mode is not enabled for this user.', 'desktop-mode' ), array( 'status' => 403 ) );
61 + if ( function_exists( 'openstation_is_enabled' ) && ! openstation_is_enabled( get_current_user_id() ) ) {
62 + return new WP_Error( 'openstation_files_disabled', __( 'OpenStation is not enabled for this user.', 'desktop-mode' ), array( 'status' => 403 ) );
39 63 }
40 64 return true;
41 65 }
42 66
@@ -41,24 +65,22 @@
41 65 }
42 66
43 67 /**
44 68 * Permission callback layered ON TOP of
45 - * `desktop_mode_files_rest_permission` for every share-related
69 + * `openstation_files_rest_permission` for every share-related
46 70 * route. Returns a 404 (looks the same as a route that doesn't
47 71 * exist) when the viewer has the folder-sharing feature toggled
48 72 * off in OS Settings — no information leak about whether the
49 73 * feature is even installed.
50 - *
51 - * @since 0.18.x
52 74 */
53 -function desktop_mode_files_rest_share_permission() {
54 - $base = desktop_mode_files_rest_permission();
75 +function openstation_files_rest_share_permission() {
76 + $base = openstation_files_rest_permission();
55 77 if ( is_wp_error( $base ) ) {
56 78 return $base;
57 79 }
58 80 if (
59 - function_exists( 'desktop_mode_files_sharing_enabled_for' )
60 - && ! desktop_mode_files_sharing_enabled_for( get_current_user_id() )
81 + function_exists( 'openstation_files_sharing_enabled_for' )
82 + && ! openstation_files_sharing_enabled_for( get_current_user_id() )
61 83 ) {
62 84 return new WP_Error(
63 85 'rest_no_route',
64 86 __( 'No route was found matching the URL and request method.', 'desktop-mode' ),
@@ -72,15 +94,13 @@
72 94 * Permission callback for the destructive site-admin actions
73 95 * (currently: drop the folder-sharing tables). Requires
74 96 * `manage_options` — site-wide schema mutation should never be
75 97 * exposed below that capability.
76 - *
77 - * @since 0.18.x
78 98 */
79 -function desktop_mode_files_rest_admin_permission() {
99 +function openstation_files_rest_admin_permission() {
80 100 if ( ! current_user_can( 'manage_options' ) ) {
81 101 return new WP_Error(
82 - 'desktop_mode_files_forbidden',
102 + 'openstation_files_forbidden',
83 103 __( 'You do not have permission to perform this action.', 'desktop-mode' ),
84 104 array( 'status' => 403 )
85 105 );
86 106 }
@@ -88,211 +108,374 @@
88 108 }
89 109
90 110 /**
91 111 * Register the routes.
92 - *
93 - * @since 0.9.0
94 112 */
95 -function desktop_mode_files_register_rest_routes() {
113 +function openstation_files_register_rest_routes() {
96 114 $ns = 'desktop-mode/v1';
97 115
98 - register_rest_route( $ns, '/files/placements', array(
116 + register_rest_route(
117 + $ns,
118 + '/files/placements',
99 119 array(
100 - 'methods' => WP_REST_Server::READABLE,
101 - 'permission_callback' => 'desktop_mode_files_rest_permission',
102 - 'callback' => 'desktop_mode_files_rest_list_placements',
103 - 'args' => array(
104 - 'folder' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ),
120 + array(
121 + 'methods' => WP_REST_Server::READABLE,
122 + 'permission_callback' => 'openstation_files_rest_permission',
123 + 'callback' => 'openstation_files_rest_list_placements',
124 + 'args' => array(
125 + 'folder' => array(
126 + 'type' => 'integer',
127 + 'default' => 0,
128 + 'sanitize_callback' => 'absint',
129 + ),
130 + ),
105 131 ),
106 - ),
132 + array(
133 + 'methods' => WP_REST_Server::CREATABLE,
134 + 'permission_callback' => 'openstation_files_rest_permission',
135 + 'callback' => 'openstation_files_rest_create_placement',
136 + 'args' => array(
137 + 'parentId' => array(
138 + 'type' => 'integer',
139 + 'default' => 0,
140 + ),
141 + 'type' => array(
142 + 'type' => 'string',
143 + 'required' => true,
144 + ),
145 + 'ref' => array(
146 + 'type' => 'string',
147 + 'required' => true,
148 + ),
149 + 'x' => array(
150 + 'type' => 'integer',
151 + 'default' => 0,
152 + ),
153 + 'y' => array(
154 + 'type' => 'integer',
155 + 'default' => 0,
156 + ),
157 + 'sortOrder' => array(
158 + 'type' => 'integer',
159 + 'default' => 0,
160 + ),
161 + 'meta' => array(
162 + 'type' => 'object',
163 + 'required' => false,
164 + ),
165 + ),
166 + ),
167 + )
168 + );
169 +
170 + register_rest_route(
171 + $ns,
172 + '/files/placements/(?P<id>\d+)',
107 173 array(
108 - 'methods' => WP_REST_Server::CREATABLE,
109 - 'permission_callback' => 'desktop_mode_files_rest_permission',
110 - 'callback' => 'desktop_mode_files_rest_create_placement',
111 - 'args' => array(
112 - 'parentId' => array( 'type' => 'integer', 'default' => 0 ),
113 - 'type' => array( 'type' => 'string', 'required' => true ),
114 - 'ref' => array( 'type' => 'string', 'required' => true ),
115 - 'x' => array( 'type' => 'integer', 'default' => 0 ),
116 - 'y' => array( 'type' => 'integer', 'default' => 0 ),
117 - 'sortOrder' => array( 'type' => 'integer', 'default' => 0 ),
118 - 'meta' => array( 'type' => 'object', 'required' => false ),
174 + array(
175 + 'methods' => WP_REST_Server::EDITABLE,
176 + 'permission_callback' => 'openstation_files_rest_permission',
177 + 'callback' => 'openstation_files_rest_update_placement',
119 178 ),
120 - ),
121 - ) );
179 + array(
180 + 'methods' => WP_REST_Server::DELETABLE,
181 + 'permission_callback' => 'openstation_files_rest_permission',
182 + 'callback' => 'openstation_files_rest_delete_placement',
183 + ),
184 + )
185 + );
122 186
123 - register_rest_route( $ns, '/files/placements/(?P<id>\d+)', array(
187 + register_rest_route(
188 + $ns,
189 + '/files/folders',
124 190 array(
125 - 'methods' => WP_REST_Server::EDITABLE,
126 - 'permission_callback' => 'desktop_mode_files_rest_permission',
127 - 'callback' => 'desktop_mode_files_rest_update_placement',
128 - ),
191 + array(
192 + 'methods' => WP_REST_Server::READABLE,
193 + 'permission_callback' => 'openstation_files_rest_permission',
194 + 'callback' => 'openstation_files_rest_list_folders',
195 + ),
196 + array(
197 + 'methods' => WP_REST_Server::CREATABLE,
198 + 'permission_callback' => 'openstation_files_rest_permission',
199 + 'callback' => 'openstation_files_rest_create_folder',
200 + 'args' => array(
201 + 'name' => array(
202 + 'type' => 'string',
203 + 'required' => true,
204 + ),
205 + 'shareMode' => array(
206 + 'type' => 'string',
207 + 'default' => 'private',
208 + ),
209 + 'shareMeta' => array(
210 + 'type' => 'object',
211 + 'required' => false,
212 + ),
213 + ),
214 + ),
215 + )
216 + );
217 +
218 + register_rest_route(
219 + $ns,
220 + '/files/folders/(?P<id>\d+)',
129 221 array(
130 - 'methods' => WP_REST_Server::DELETABLE,
131 - 'permission_callback' => 'desktop_mode_files_rest_permission',
132 - 'callback' => 'desktop_mode_files_rest_delete_placement',
133 - ),
134 - ) );
222 + array(
223 + 'methods' => WP_REST_Server::EDITABLE,
224 + 'permission_callback' => 'openstation_files_rest_permission',
225 + 'callback' => 'openstation_files_rest_update_folder',
226 + ),
227 + array(
228 + 'methods' => WP_REST_Server::DELETABLE,
229 + 'permission_callback' => 'openstation_files_rest_permission',
230 + 'callback' => 'openstation_files_rest_delete_folder',
231 + ),
232 + )
233 + );
135 234
136 - register_rest_route( $ns, '/files/folders', array(
235 + register_rest_route(
236 + $ns,
237 + '/files/associations',
137 238 array(
138 - 'methods' => WP_REST_Server::READABLE,
139 - 'permission_callback' => 'desktop_mode_files_rest_permission',
140 - 'callback' => 'desktop_mode_files_rest_list_folders',
141 - ),
142 - array(
143 - 'methods' => WP_REST_Server::CREATABLE,
144 - 'permission_callback' => 'desktop_mode_files_rest_permission',
145 - 'callback' => 'desktop_mode_files_rest_create_folder',
239 + 'methods' => 'PUT',
240 + 'permission_callback' => 'openstation_files_rest_permission',
241 + 'callback' => 'openstation_files_rest_save_associations',
146 242 'args' => array(
147 - 'name' => array( 'type' => 'string', 'required' => true ),
148 - 'shareMode' => array( 'type' => 'string', 'default' => 'private' ),
149 - 'shareMeta' => array( 'type' => 'object', 'required' => false ),
243 + 'associations' => array(
244 + 'type' => 'object',
245 + 'required' => true,
246 + ),
150 247 ),
151 - ),
152 - ) );
248 + )
249 + );
153 250
154 - register_rest_route( $ns, '/files/folders/(?P<id>\d+)', array(
155 - array(
156 - 'methods' => WP_REST_Server::EDITABLE,
157 - 'permission_callback' => 'desktop_mode_files_rest_permission',
158 - 'callback' => 'desktop_mode_files_rest_update_folder',
159 - ),
160 - array(
161 - 'methods' => WP_REST_Server::DELETABLE,
162 - 'permission_callback' => 'desktop_mode_files_rest_permission',
163 - 'callback' => 'desktop_mode_files_rest_delete_folder',
164 - ),
165 - ) );
166 -
167 - register_rest_route( $ns, '/files/associations', array(
168 - 'methods' => 'PUT',
169 - 'permission_callback' => 'desktop_mode_files_rest_permission',
170 - 'callback' => 'desktop_mode_files_rest_save_associations',
171 - 'args' => array(
172 - 'associations' => array( 'type' => 'object', 'required' => true ),
173 - ),
174 - ) );
175 -
176 251 // Every share-related route gates on the user's
177 252 // `foldersSharingEnabled` OS Setting via
178 - // `desktop_mode_files_rest_share_permission` — when a user has
253 + // `openstation_files_rest_share_permission` — when a user has
179 254 // flipped sharing off, these routes return 404 (looks the same
180 255 // as a feature that isn't installed; no info leak about the
181 256 // kill switch's existence).
182 - register_rest_route( $ns, '/files/folders/(?P<id>\d+)/shares', array(
257 + register_rest_route(
258 + $ns,
259 + '/files/folders/(?P<id>\d+)/shares',
183 260 array(
184 - 'methods' => WP_REST_Server::READABLE,
185 - 'permission_callback' => 'desktop_mode_files_rest_share_permission',
186 - 'callback' => 'desktop_mode_files_rest_list_shares',
187 - ),
188 - array(
189 - 'methods' => WP_REST_Server::CREATABLE,
190 - 'permission_callback' => 'desktop_mode_files_rest_share_permission',
191 - 'callback' => 'desktop_mode_files_rest_create_share',
192 - 'args' => array(
193 - 'principalType' => array(
194 - 'type' => 'string',
195 - 'enum' => array( 'user', 'role' ),
196 - 'required' => true,
261 + array(
262 + 'methods' => WP_REST_Server::READABLE,
263 + 'permission_callback' => 'openstation_files_rest_share_permission',
264 + 'callback' => 'openstation_files_rest_list_shares',
265 + ),
266 + array(
267 + 'methods' => WP_REST_Server::CREATABLE,
268 + 'permission_callback' => 'openstation_files_rest_share_permission',
269 + 'callback' => 'openstation_files_rest_create_share',
270 + 'args' => array(
271 + 'principalType' => array(
272 + 'type' => 'string',
273 + 'enum' => array( 'user', 'role' ),
274 + 'required' => true,
275 + ),
276 + 'principalRef' => array(
277 + 'type' => 'string',
278 + 'required' => true,
279 + ),
280 + 'capability' => array(
281 + 'type' => 'string',
282 + 'enum' => array( 'read', 'write' ),
283 + 'default' => 'read',
284 + ),
197 285 ),
198 - 'principalRef' => array( 'type' => 'string', 'required' => true ),
199 - 'capability' => array(
200 - 'type' => 'string',
201 - 'enum' => array( 'read', 'write' ),
202 - 'default' => 'read',
203 - ),
204 286 ),
205 - ),
206 - ) );
287 + )
288 + );
207 289
208 - register_rest_route( $ns, '/files/folders/(?P<id>\d+)/shares/(?P<shareId>\d+)', array(
290 + register_rest_route(
291 + $ns,
292 + '/files/folders/(?P<id>\d+)/shares/(?P<shareId>\d+)',
209 293 array(
210 - 'methods' => WP_REST_Server::EDITABLE,
211 - 'permission_callback' => 'desktop_mode_files_rest_share_permission',
212 - 'callback' => 'desktop_mode_files_rest_update_share',
213 - 'args' => array(
214 - 'capability' => array(
215 - 'type' => 'string',
216 - 'enum' => array( 'read', 'write' ),
217 - 'required' => true,
294 + array(
295 + 'methods' => WP_REST_Server::EDITABLE,
296 + 'permission_callback' => 'openstation_files_rest_share_permission',
297 + 'callback' => 'openstation_files_rest_update_share',
298 + 'args' => array(
299 + 'capability' => array(
300 + 'type' => 'string',
301 + 'enum' => array( 'read', 'write' ),
302 + 'required' => true,
303 + ),
218 304 ),
219 305 ),
220 - ),
306 + array(
307 + 'methods' => WP_REST_Server::DELETABLE,
308 + 'permission_callback' => 'openstation_files_rest_share_permission',
309 + 'callback' => 'openstation_files_rest_delete_share',
310 + ),
311 + )
312 + );
313 +
314 + register_rest_route(
315 + $ns,
316 + '/files/folders/(?P<id>\d+)/shares/(?P<shareId>\d+)/accept',
221 317 array(
222 - 'methods' => WP_REST_Server::DELETABLE,
223 - 'permission_callback' => 'desktop_mode_files_rest_share_permission',
224 - 'callback' => 'desktop_mode_files_rest_delete_share',
225 - ),
226 - ) );
318 + 'methods' => WP_REST_Server::CREATABLE,
319 + 'permission_callback' => 'openstation_files_rest_share_permission',
320 + 'callback' => 'openstation_files_rest_accept_share',
321 + )
322 + );
227 323
228 - register_rest_route( $ns, '/files/folders/(?P<id>\d+)/shares/(?P<shareId>\d+)/accept', array(
229 - 'methods' => WP_REST_Server::CREATABLE,
230 - 'permission_callback' => 'desktop_mode_files_rest_share_permission',
231 - 'callback' => 'desktop_mode_files_rest_accept_share',
232 - ) );
324 + register_rest_route(
325 + $ns,
326 + '/files/folders/(?P<id>\d+)/shares/(?P<shareId>\d+)/deny',
327 + array(
328 + 'methods' => WP_REST_Server::CREATABLE,
329 + 'permission_callback' => 'openstation_files_rest_share_permission',
330 + 'callback' => 'openstation_files_rest_deny_share',
331 + )
332 + );
233 333
234 - register_rest_route( $ns, '/files/folders/(?P<id>\d+)/shares/(?P<shareId>\d+)/deny', array(
235 - 'methods' => WP_REST_Server::CREATABLE,
236 - 'permission_callback' => 'desktop_mode_files_rest_share_permission',
237 - 'callback' => 'desktop_mode_files_rest_deny_share',
238 - ) );
334 + register_rest_route(
335 + $ns,
336 + '/files/folders/(?P<id>\d+)/leave',
337 + array(
338 + 'methods' => WP_REST_Server::CREATABLE,
339 + 'permission_callback' => 'openstation_files_rest_share_permission',
340 + 'callback' => 'openstation_files_rest_leave_folder',
341 + )
342 + );
239 343
240 - register_rest_route( $ns, '/files/folders/(?P<id>\d+)/leave', array(
241 - 'methods' => WP_REST_Server::CREATABLE,
242 - 'permission_callback' => 'desktop_mode_files_rest_share_permission',
243 - 'callback' => 'desktop_mode_files_rest_leave_folder',
244 - ) );
344 + register_rest_route(
345 + $ns,
346 + '/files/users/search',
347 + array(
348 + 'methods' => WP_REST_Server::READABLE,
349 + 'permission_callback' => 'openstation_files_rest_search_users_permission',
350 + 'callback' => 'openstation_files_rest_search_users',
351 + 'args' => array(
352 + 'q' => array(
353 + 'type' => 'string',
354 + 'default' => '',
355 + ),
356 + 'exclude' => array(
357 + 'type' => 'string',
358 + 'default' => '',
359 + ),
360 + ),
361 + )
362 + );
245 363
246 - register_rest_route( $ns, '/files/users/search', array(
247 - 'methods' => WP_REST_Server::READABLE,
248 - 'permission_callback' => 'desktop_mode_files_rest_search_users_permission',
249 - 'callback' => 'desktop_mode_files_rest_search_users',
250 - 'args' => array(
251 - 'q' => array( 'type' => 'string', 'default' => '' ),
252 - 'exclude' => array( 'type' => 'string', 'default' => '' ),
253 - ),
254 - ) );
255 -
256 364 // Site-admin only: destructive cleanup that drops the folder-
257 365 // sharing tables outright (legacy + current). Surfaced from
258 366 // the OS Settings → Features → Advanced panel.
259 - register_rest_route( $ns, '/files/folder-sharing-tables/purge', array(
260 - 'methods' => WP_REST_Server::CREATABLE,
261 - 'permission_callback' => 'desktop_mode_files_rest_admin_permission',
262 - 'callback' => 'desktop_mode_files_rest_purge_sharing_tables',
263 - ) );
367 + register_rest_route(
368 + $ns,
369 + '/files/folder-sharing-tables/purge',
370 + array(
371 + 'methods' => WP_REST_Server::CREATABLE,
372 + 'permission_callback' => 'openstation_files_rest_admin_permission',
373 + 'callback' => 'openstation_files_rest_purge_sharing_tables',
374 + )
375 + );
264 376 }
265 -add_action( 'rest_api_init', 'desktop_mode_files_register_rest_routes' );
377 +add_action( 'rest_api_init', 'openstation_files_register_rest_routes' );
266 378
267 379 /**
380 + * Inline the root folder's placements into the boot-time shell
381 + * config so the desktop file grid hydrates without a REST
382 + * round-trip — this was the only REST call the shell had to await
383 + * before revealing the desktop. Mirrors the GET /placements handler
384 + * for `folder=0` exactly (same orphan backfill, same shape) so the
385 + * client store can't tell the difference; the JS consumer
386 + * (`src/desktop-files/layer.ts`) consumes the key one-shot, so any
387 + * later re-hydration still goes through REST for fresh state.
388 + *
389 + * The `openstation_shell_config` filter only runs while rendering
390 + * the shell for an enabled, logged-in user — the same gate the REST
391 + * permission callback enforces.
392 + *
393 + * @param array $config Shell config.
394 + * @return array
395 + */
396 +function openstation_files_inject_boot_placements( $config ) {
397 + $user_id = get_current_user_id();
398 + if ( $user_id <= 0 ) {
399 + return $config;
400 + }
401 + openstation_files_auto_place_orphans( $user_id );
402 + $rows = openstation_files_get_for_user_folder( $user_id, 0 );
403 + $out = array();
404 + foreach ( $rows as $row ) {
405 + $out[] = openstation_files_shape_placement( $row );
406 + }
407 + $config['filesBootPlacements'] = $out;
408 + return $config;
409 +}
410 +add_filter( 'openstation_shell_config', 'openstation_files_inject_boot_placements', 20 );
411 +
412 +/**
413 + * Inline the viewer's visible folder rows into the boot-time shell
414 + * config, the same way the root placements are.
415 + *
416 + * The client keeps a folders map alongside its placements, and
417 + * anything that needs to know a folder's OWNER reads it there —
418 + * notably the "Share folder" title-bar button, which is owner-only
419 + * and has nothing but a window id to work from. Nothing on the
420 + * normal boot path filled that map: placement hydration populates
421 + * placements, and `listFolders()` only ran after a create, a rename
422 + * or an untrash. So a plain reload left every folder ownerless, and
423 + * the owner of a folder lost the one control that manages its
424 + * sharing until something happened to repopulate the map.
425 + *
426 + * Mirrors GET /folders exactly (same visibility resolution — owned
427 + * folders plus accepted shares plus `share_mode='all'` — same
428 + * shape), and the client consumes it one-shot, so any later
429 + * re-hydration still goes through REST for fresh state.
430 + *
431 + * @param array $config Shell config.
432 + * @return array
433 + */
434 +function openstation_files_inject_boot_folders( $config ) {
435 + $user_id = get_current_user_id();
436 + if ( $user_id <= 0 ) {
437 + return $config;
438 + }
439 + $out = array();
440 + foreach ( openstation_files_get_visible_folders( $user_id ) as $row ) {
441 + $out[] = openstation_files_shape_folder( $row );
442 + }
443 + $config['filesBootFolders'] = $out;
444 + return $config;
445 +}
446 +add_filter( 'openstation_shell_config', 'openstation_files_inject_boot_folders', 20 );
447 +
448 +/**
268 449 * GET /placements
269 450 */
270 -function desktop_mode_files_rest_list_placements( WP_REST_Request $req ) {
451 +function openstation_files_rest_list_placements( WP_REST_Request $req ) {
271 452 $user_id = get_current_user_id();
272 453 $parent_id = (int) $req->get_param( 'folder' );
273 454 // Self-healing backfill — see
274 - // `desktop_mode_files_auto_place_orphan_folders` for the why.
455 + // `openstation_files_auto_place_orphan_folders` for the why.
275 456 // Only runs at the root because that's the only context where
276 457 // auto-placing an orphan folder as a tile is unambiguous.
277 458 if ( 0 === $parent_id ) {
278 - desktop_mode_files_auto_place_orphans( $user_id );
459 + openstation_files_auto_place_orphans( $user_id );
279 460 }
280 - $rows = desktop_mode_files_get_for_user_folder( $user_id, $parent_id );
281 - $out = array();
461 + $rows = openstation_files_get_for_user_folder( $user_id, $parent_id );
462 + $out = array();
282 463 foreach ( $rows as $row ) {
283 - $out[] = desktop_mode_files_shape_placement( $row );
464 + $out[] = openstation_files_shape_placement( $row );
284 465 }
285 - return rest_ensure_response( array(
286 - 'placements' => $out,
287 - 'folderId' => $parent_id,
288 - ) );
466 + return rest_ensure_response(
467 + array(
468 + 'placements' => $out,
469 + 'folderId' => $parent_id,
470 + )
471 + );
289 472 }
290 473
291 474 /**
292 475 * POST /placements
293 476 */
294 -function desktop_mode_files_rest_create_placement( WP_REST_Request $req ) {
477 +function openstation_files_rest_create_placement( WP_REST_Request $req ) {
295 478 $type = (string) $req->get_param( 'type' );
296 479 $ref = (string) $req->get_param( 'ref' );
297 480 $meta = $req->get_param( 'meta' );
298 481
@@ -300,17 +483,17 @@
300 483 // `meta.iconUrl` so the tile renderer can paint it without the
301 484 // browser making a third-party request on every render. Other
302 485 // types skip the resolver entirely (no extra fetch latency).
303 486 if ( 'link' === $type && '' !== $ref ) {
304 - $icon_data_uri = desktop_mode_resolve_favicon( $ref );
487 + $icon_data_uri = openstation_resolve_favicon( $ref );
305 488 if ( is_string( $icon_data_uri ) && '' !== $icon_data_uri ) {
306 - $meta_arr = is_array( $meta ) ? $meta : array();
307 - $meta_arr['iconUrl'] = $icon_data_uri;
308 - $meta = $meta_arr;
489 + $meta_arr = is_array( $meta ) ? $meta : array();
490 + $meta_arr['iconUrl'] = $icon_data_uri;
491 + $meta = $meta_arr;
309 492 }
310 493 }
311 494
312 - $id = desktop_mode_files_place(
495 + $id = openstation_files_place(
313 496 get_current_user_id(),
314 497 (int) $req->get_param( 'parentId' ),
315 498 $type,
316 499 $ref,
@@ -323,43 +506,50 @@
323 506 );
324 507 if ( is_wp_error( $id ) ) {
325 508 return $id;
326 509 }
327 - $row = desktop_mode_files_get_placement( $id );
328 - return rest_ensure_response( desktop_mode_files_shape_placement( $row ) );
510 + $row = openstation_files_get_placement( $id );
511 + return rest_ensure_response( openstation_files_shape_placement( $row ) );
329 512 }
330 513
331 514 /**
332 515 * PATCH /placements/<id>
333 516 */
334 -function desktop_mode_files_rest_update_placement( WP_REST_Request $req ) {
517 +function openstation_files_rest_update_placement( WP_REST_Request $req ) {
335 518 $id = (int) $req['id'];
336 - $body = $req->get_json_params() ?: $req->get_params();
337 - $current = desktop_mode_files_get_placement( $id );
519 + $json = $req->get_json_params();
520 + $body = $json ? $json : $req->get_params();
521 + $current = openstation_files_get_placement( $id );
338 522 if ( ! $current ) {
339 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) );
523 + return new WP_Error( 'openstation_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) );
340 524 }
341 - $conflict = desktop_mode_files_check_if_match( (int) $current['updated_at_ms'], $req, $current );
525 + $conflict = openstation_files_check_if_match( (int) $current['updated_at_ms'], $req, $current );
342 526 if ( is_wp_error( $conflict ) ) {
343 527 return $conflict;
344 528 }
345 529 $changes = array();
346 - foreach ( array( 'parentId' => 'parent_id', 'x' => 'x', 'y' => 'y', 'sortOrder' => 'sort_order', 'meta' => 'meta' ) as $in => $col ) {
530 + foreach ( array(
531 + 'parentId' => 'parent_id',
532 + 'x' => 'x',
533 + 'y' => 'y',
534 + 'sortOrder' => 'sort_order',
535 + 'meta' => 'meta',
536 + ) as $in => $col ) {
347 537 if ( array_key_exists( $in, $body ) ) {
348 538 $changes[ $col ] = $body[ $in ];
349 539 }
350 540 }
351 - $ok = desktop_mode_files_move( $id, get_current_user_id(), $changes );
541 + $ok = openstation_files_move( $id, get_current_user_id(), $changes );
352 542 if ( is_wp_error( $ok ) ) {
353 543 return $ok;
354 544 }
355 - return rest_ensure_response( desktop_mode_files_shape_placement( desktop_mode_files_get_placement( $id ) ) );
545 + return rest_ensure_response( openstation_files_shape_placement( openstation_files_get_placement( $id ) ) );
356 546 }
357 547
358 548 /**
359 549 * DELETE /placements/<id>
360 550 */
361 -function desktop_mode_files_rest_delete_placement( WP_REST_Request $req ) {
551 +function openstation_files_rest_delete_placement( WP_REST_Request $req ) {
362 552 $id = (int) $req['id'];
363 553 $user_id = get_current_user_id();
364 554 // `force=1` query param permanently deletes (purges the row).
365 555 // Default DELETE soft-trashes — the row lands in the recycle
@@ -367,10 +557,10 @@
367 557 // uses on every other resource.
368 558 $force = '1' === (string) $req->get_param( 'force' )
369 559 || true === $req->get_param( 'force' );
370 560 $ok = $force
371 - ? desktop_mode_files_purge_placement( $user_id, $id )
372 - : desktop_mode_files_trash_placement( $user_id, $id );
561 + ? openstation_files_purge_placement( $user_id, $id )
562 + : openstation_files_trash_placement( $user_id, $id );
373 563 if ( is_wp_error( $ok ) ) {
374 564 return $ok;
375 565 }
376 566 return rest_ensure_response(
@@ -383,13 +573,13 @@
383 573
384 574 /**
385 575 * GET /folders
386 576 */
387 -function desktop_mode_files_rest_list_folders() {
388 - $rows = desktop_mode_files_get_visible_folders( get_current_user_id() );
577 +function openstation_files_rest_list_folders() {
578 + $rows = openstation_files_get_visible_folders( get_current_user_id() );
389 579 $out = array();
390 580 foreach ( $rows as $row ) {
391 - $out[] = desktop_mode_files_shape_folder( $row );
581 + $out[] = openstation_files_shape_folder( $row );
392 582 }
393 583 return rest_ensure_response( array( 'folders' => $out ) );
394 584 }
395 585
@@ -395,10 +585,10 @@
395 585
396 586 /**
397 587 * POST /folders
398 588 */
399 -function desktop_mode_files_rest_create_folder( WP_REST_Request $req ) {
400 - $id = desktop_mode_files_create_folder(
589 +function openstation_files_rest_create_folder( WP_REST_Request $req ) {
590 + $id = openstation_files_create_folder(
401 591 get_current_user_id(),
402 592 array(
403 593 'name' => (string) $req->get_param( 'name' ),
404 594 'share_mode' => (string) $req->get_param( 'shareMode' ),
@@ -407,53 +597,58 @@
407 597 );
408 598 if ( is_wp_error( $id ) ) {
409 599 return $id;
410 600 }
411 - return rest_ensure_response( desktop_mode_files_shape_folder( desktop_mode_files_get_folder( $id ) ) );
601 + return rest_ensure_response( openstation_files_shape_folder( openstation_files_get_folder( $id ) ) );
412 602 }
413 603
414 604 /**
415 605 * PATCH /folders/<id>
416 606 */
417 -function desktop_mode_files_rest_update_folder( WP_REST_Request $req ) {
607 +function openstation_files_rest_update_folder( WP_REST_Request $req ) {
418 608 $id = (int) $req['id'];
419 - $body = $req->get_json_params() ?: $req->get_params();
420 - $current = desktop_mode_files_get_folder( $id );
609 + $json = $req->get_json_params();
610 + $body = $json ? $json : $req->get_params();
611 + $current = openstation_files_get_folder( $id );
421 612 if ( ! $current ) {
422 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
613 + return new WP_Error( 'openstation_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
423 614 }
424 - $conflict = desktop_mode_files_check_if_match( (int) $current['updated_at_ms'], $req, $current );
615 + $conflict = openstation_files_check_if_match( (int) $current['updated_at_ms'], $req, $current );
425 616 if ( is_wp_error( $conflict ) ) {
426 617 return $conflict;
427 618 }
428 619 $changes = array();
429 - foreach ( array( 'name' => 'name', 'shareMode' => 'share_mode', 'shareMeta' => 'share_meta' ) as $in => $col ) {
620 + foreach ( array(
621 + 'name' => 'name',
622 + 'shareMode' => 'share_mode',
623 + 'shareMeta' => 'share_meta',
624 + ) as $in => $col ) {
430 625 if ( array_key_exists( $in, $body ) ) {
431 626 $changes[ $col ] = $body[ $in ];
432 627 }
433 628 }
434 - $ok = desktop_mode_files_update_folder( $id, get_current_user_id(), $changes );
629 + $ok = openstation_files_update_folder( $id, get_current_user_id(), $changes );
435 630 if ( is_wp_error( $ok ) ) {
436 631 return $ok;
437 632 }
438 - return rest_ensure_response( desktop_mode_files_shape_folder( desktop_mode_files_get_folder( $id ) ) );
633 + return rest_ensure_response( openstation_files_shape_folder( openstation_files_get_folder( $id ) ) );
439 634 }
440 635
441 636 /**
442 637 * DELETE /folders/<id>
443 638 */
444 -function desktop_mode_files_rest_delete_folder( WP_REST_Request $req ) {
639 +function openstation_files_rest_delete_folder( WP_REST_Request $req ) {
445 640 $id = (int) $req['id'];
446 641 $user_id = get_current_user_id();
447 642 $force = '1' === (string) $req->get_param( 'force' )
448 643 || true === $req->get_param( 'force' );
449 644 // Default DELETE soft-trashes the folder + cascades to child
450 - // placements (see `desktop_mode_files_trash_folder`). `force=1`
645 + // placements (see `openstation_files_trash_folder`). `force=1`
451 646 // permanently deletes both the folder row AND every child
452 647 // placement that was trashed via the cascade.
453 648 $ok = $force
454 - ? desktop_mode_files_purge_folder( $user_id, $id )
455 - : desktop_mode_files_trash_folder( $user_id, $id );
649 + ? openstation_files_purge_folder( $user_id, $id )
650 + : openstation_files_trash_folder( $user_id, $id );
456 651 if ( is_wp_error( $ok ) ) {
457 652 return $ok;
458 653 }
459 654 return rest_ensure_response(
@@ -466,9 +661,9 @@
466 661
467 662 /**
468 663 * PUT /associations — replaces the entire user-association map.
469 664 */
470 -function desktop_mode_files_rest_save_associations( WP_REST_Request $req ) {
665 +function openstation_files_rest_save_associations( WP_REST_Request $req ) {
471 666 $assoc = (array) $req->get_param( 'associations' );
472 667 $clean = array();
473 668 foreach ( $assoc as $type => $opener_id ) {
474 669 $type = sanitize_key( (string) $type );
@@ -477,37 +672,55 @@
477 672 continue;
478 673 }
479 674 $clean[ $type ] = $opener_id;
480 675 }
481 - update_user_meta( get_current_user_id(), DESKTOP_MODE_FILE_ASSOCIATIONS_META, $clean );
482 - return rest_ensure_response( array(
483 - 'associations' => desktop_mode_get_user_file_associations( get_current_user_id() ),
484 - ) );
676 + update_user_meta( get_current_user_id(), OPENSTATION_FILE_ASSOCIATIONS_META, $clean );
677 + return rest_ensure_response(
678 + array(
679 + 'associations' => openstation_get_user_file_associations( get_current_user_id() ),
680 + )
681 + );
485 682 }
486 683
487 684 /**
488 685 * Shape a placement row for the wire — converts snake_case to
489 - * camelCase and merges in the resolved `Desktop_Mode_File`
686 + * camelCase and merges in the resolved `OpenStation_File`
490 687 * shape so the JS side can render without a second fetch.
491 688 *
492 - * @since 0.9.0
493 - *
494 689 * @param array|null $row Normalized placement row.
495 690 * @return array
496 691 */
497 -function desktop_mode_files_shape_placement( $row ) {
692 +function openstation_files_shape_placement( $row ) {
498 693 if ( ! is_array( $row ) ) {
499 694 return array();
500 695 }
501 - $file = desktop_mode_resolve_file( $row['file_type'], $row['file_ref'] );
502 - $shape = $file ? $file->serialize() : array(
503 - 'type' => $row['file_type'],
504 - 'ref' => $row['file_ref'],
505 - 'title' => '',
506 - 'icon' => 'dashicons-warning',
507 - 'previewUrl' => '',
508 - 'exists' => false,
509 - );
696 + // Access-gated rows (shared-folder view, viewer lacks read on
697 + // the underlying entity) get a redacted shape — the viewer may
698 + // learn THAT the owner placed something here, not WHAT it is.
699 + // Skipping the resolver keeps entity metadata (title, permalink,
700 + // status, roles, …) from crossing the read-access boundary; the
701 + // tile renderer paints the lock overlay off `accessGated`.
702 + $access_gated = ! empty( $row['access_gated'] );
703 + if ( $access_gated ) {
704 + $shape = array(
705 + 'type' => $row['file_type'],
706 + 'ref' => $row['file_ref'],
707 + 'title' => __( 'Restricted item', 'desktop-mode' ),
708 + 'icon' => 'dashicons-lock',
709 + 'previewUrl' => '',
710 + 'exists' => true,
711 + );
712 + } else {
713 + $file = openstation_resolve_file( $row['file_type'], $row['file_ref'] );
714 + $shape = $file ? $file->serialize() : array(
715 + 'type' => $row['file_type'],
716 + 'ref' => $row['file_ref'],
717 + 'title' => '',
718 + 'icon' => 'dashicons-warning',
719 + 'previewUrl' => '',
720 + 'exists' => false,
721 + );
722 + }
510 723 // `canTrash` carries the server's answer to "can the viewer
511 724 // move this placement to the recycle bin?" so the client can
512 725 // proactively suppress the trash affordance — both the tile's
513 726 // right-click "Move to recycle bin" menu item and the trash
@@ -516,63 +729,53 @@
516 729 // user staring at a tile that wouldn't move. Falls back to
517 730 // `false` when the helper isn't loaded (defensive — early-boot
518 731 // REST calls before trash.php is required can't grant permission
519 732 // they don't know about).
520 - $viewer_id = (int) get_current_user_id();
521 - $can_trash = false;
522 - if ( $viewer_id > 0 && function_exists( 'desktop_mode_files_user_can_trash_placement' ) ) {
523 - $can_trash = desktop_mode_files_user_can_trash_placement( $viewer_id, $row );
733 + $viewer_id = (int) get_current_user_id();
734 + $can_trash = false;
735 + if ( $viewer_id > 0 && function_exists( 'openstation_files_user_can_trash_placement' ) ) {
736 + $can_trash = openstation_files_user_can_trash_placement( $viewer_id, $row );
524 737 }
525 738
526 739 return array(
527 - 'id' => (int) $row['id'],
528 - 'parentId' => (int) $row['parent_id'],
529 - 'x' => (int) $row['x'],
530 - 'y' => (int) $row['y'],
531 - 'sortOrder' => (int) $row['sort_order'],
532 - 'updatedAtMs' => (int) $row['updated_at_ms'],
533 - 'meta' => isset( $row['meta'] ) ? $row['meta'] : null,
534 - 'file' => $shape,
740 + 'id' => (int) $row['id'],
741 + 'parentId' => (int) $row['parent_id'],
742 + 'x' => (int) $row['x'],
743 + 'y' => (int) $row['y'],
744 + 'sortOrder' => (int) $row['sort_order'],
745 + 'updatedAtMs' => (int) $row['updated_at_ms'],
746 + 'meta' => isset( $row['meta'] ) ? $row['meta'] : null,
747 + 'file' => $shape,
535 748 // `accessGated` is true when the viewer can't read the
536 749 // underlying entity but the placement is shown anyway (the
537 750 // shared-folder-view UX). Tile renderer surfaces it as a
538 - // lock overlay + tooltip.
539 - 'accessGated' => ! empty( $row['access_gated'] ),
540 - 'canTrash' => $can_trash,
751 + // lock overlay + tooltip; the `file` shape above is redacted.
752 + 'accessGated' => $access_gated,
753 + 'canTrash' => $can_trash,
541 754 );
542 755 }
543 756
544 757 /**
545 - * @since 0.9.0
546 - *
547 758 * @param array|null $row Folder row.
548 759 * @return array
549 760 */
550 -function desktop_mode_files_shape_folder( $row ) {
761 +function openstation_files_shape_folder( $row ) {
551 762 if ( ! is_array( $row ) ) {
552 763 return array();
553 764 }
554 765 $shape = array(
555 - 'id' => (int) $row['id'],
556 - 'ownerId' => (int) $row['owner_id'],
557 - 'name' => (string) $row['name'],
558 - 'shareMode' => (string) $row['share_mode'],
559 - 'shareMeta' => isset( $row['share_meta'] ) ? $row['share_meta'] : null,
560 - 'updatedAtMs' => (int) $row['updated_at_ms'],
766 + 'id' => (int) $row['id'],
767 + 'ownerId' => (int) $row['owner_id'],
768 + 'name' => (string) $row['name'],
769 + 'shareMode' => (string) $row['share_mode'],
770 + 'shareMeta' => isset( $row['share_meta'] ) ? $row['share_meta'] : null,
771 + 'updatedAtMs' => (int) $row['updated_at_ms'],
561 772 );
562 - if ( function_exists( 'desktop_mode_files_get_folder_shares' ) ) {
563 - $shares = desktop_mode_files_get_folder_shares( (int) $row['id'] );
564 - $accepted_count = 0;
565 - $has_all = 'all' === (string) $row['share_mode'];
566 - foreach ( $shares as $s ) {
567 - if ( 'accepted' === $s['state'] ) {
568 - $accepted_count++;
569 - }
570 - }
571 - $shape['shareSummary'] = array(
572 - 'shared' => $has_all || $accepted_count > 0,
573 - 'recipientCount' => $accepted_count + ( $has_all ? 1 : 0 ),
574 - );
773 + // Same summary `OpenStation_Folder_File::serialize()` puts on a
774 + // folder PLACEMENT, so a tile paints identically whichever
775 + // response it came from.
776 + if ( function_exists( 'openstation_files_folder_share_summary' ) ) {
777 + $shape['shareSummary'] = openstation_files_folder_share_summary( $row );
575 778 }
576 779 return $shape;
577 780 }
578 781
@@ -586,16 +789,14 @@
586 789 * The 409 body carries a structured `data` payload the client
587 790 * surfaces as a toast: `{ reason, actor: { id,name,avatar },
588 791 * current: { parentId, parentName, updatedAtMs } }`.
589 792 *
590 - * @since 0.18.0
591 - *
592 793 * @param int $current_ms Current `updated_at_ms` on the row.
593 794 * @param WP_REST_Request $req Inbound request.
594 795 * @param array $row Normalized row (placement or folder).
595 796 * @return WP_Error|null
596 797 */
597 -function desktop_mode_files_check_if_match( $current_ms, WP_REST_Request $req, $row ) {
798 +function openstation_files_check_if_match( $current_ms, WP_REST_Request $req, $row ) {
598 799 $header = $req->get_header( 'if_match' );
599 800 if ( null === $header || '' === $header ) {
600 801 return null;
601 802 }
@@ -604,14 +805,15 @@
604 805 return null;
605 806 }
606 807 // Prefer `updated_by` (v10+) so the conflict toast attributes
607 808 // the change to the SESSION that won the race. Falls back to
608 - // `user_id` (placement creator) or `owner_id` (folder owner)
609 - // for legacy rows from before the column was added — in a
610 - // non-shared-write workflow those still happen to be the right
611 - // person; in shared-write the toast may be slightly misleading
612 - // for the lifetime of pre-v10 rows. New mutations stamp the
613 - // column accurately. See `desktop_mode_files_ensure_updated_by_column`.
809 + // `owner_id` (placement creator / folder owner) for legacy
810 + // rows from before the v10 `updated_by` column was added — in
811 + // a non-shared-write workflow that still happens to be the
812 + // right person; in shared-write the toast may be slightly
813 + // misleading for the lifetime of pre-v10 rows. New mutations
814 + // stamp the column accurately. See
815 + // `openstation_files_ensure_updated_by_column`.
614 816 $actor_id = 0;
615 817 if ( isset( $row['updated_by'] ) && (int) $row['updated_by'] > 0 ) {
616 818 $actor_id = (int) $row['updated_by'];
617 819 } elseif ( isset( $row['owner_id'] ) ) {
@@ -621,9 +823,9 @@
621 823
622 824 $parent_id = isset( $row['parent_id'] ) ? (int) $row['parent_id'] : 0;
623 825 $parent_name = '';
624 826 if ( $parent_id > 0 ) {
625 - $parent_folder = desktop_mode_files_get_folder( $parent_id );
827 + $parent_folder = openstation_files_get_folder( $parent_id );
626 828 $parent_name = $parent_folder ? (string) $parent_folder['name'] : '';
627 829 }
628 830
629 831 $reason = 'parent_changed';
@@ -631,23 +833,26 @@
631 833 $reason = 'trashed';
632 834 }
633 835
634 836 // PII gate. The conflict toast names the actor (display name +
635 - // avatar) only when the requesting viewer is in the same
636 - // collaboration scope as the actor — i.e. owns the row, owns
637 - // the parent folder, or has at least read access to the parent
638 - // folder via the shares table. For any other viewer the actor
639 - // degrades to a generic "another session" — `id: 0`, empty
640 - // name + avatar — so a write attempt can't be used to enumerate
641 - // other users' display names.
642 - $viewer_id = (int) get_current_user_id();
643 - $viewer_owns_row = isset( $row['owner_id'] ) && (int) $row['owner_id'] === $viewer_id;
644 - $viewer_can_see = $viewer_owns_row;
837 + // avatar) and the row's parent folder only when the requesting
838 + // viewer is in the same collaboration scope as the actor — i.e.
839 + // owns the row, owns the parent folder, or has at least read
840 + // access to the parent folder via the shares table. For any
841 + // other viewer the actor degrades to a generic "another
842 + // session" — `id: 0`, empty name + avatar — and `current`
843 + // drops the parent id/name, so a write attempt can't be used
844 + // to enumerate other users' display names or folder names
845 + // (this check runs BEFORE the store's ownership gate, so the
846 + // 409 body must not leak what the later 403 would protect).
847 + $viewer_id = (int) get_current_user_id();
848 + $viewer_owns_row = isset( $row['owner_id'] ) && (int) $row['owner_id'] === $viewer_id;
849 + $viewer_can_see = $viewer_owns_row;
645 850 if ( ! $viewer_can_see && $parent_id > 0 && isset( $parent_folder ) && $parent_folder ) {
646 851 if ( (int) $parent_folder['owner_id'] === $viewer_id ) {
647 852 $viewer_can_see = true;
648 - } elseif ( function_exists( 'desktop_mode_folder_share_user_capability' ) ) {
649 - $viewer_can_see = 'none' !== desktop_mode_folder_share_user_capability( $parent_id, $viewer_id );
853 + } elseif ( function_exists( 'openstation_folder_share_user_capability' ) ) {
854 + $viewer_can_see = 'none' !== openstation_folder_share_user_capability( $parent_id, $viewer_id );
650 855 }
651 856 }
652 857 $actor_payload = array(
653 858 'id' => $viewer_can_see ? $actor_id : 0,
@@ -655,9 +860,9 @@
655 860 'avatar' => $viewer_can_see && $actor ? get_avatar_url( $actor->ID, array( 'size' => 32 ) ) : '',
656 861 );
657 862
658 863 return new WP_Error(
659 - 'desktop_mode_files_conflict',
864 + 'openstation_files_conflict',
660 865 __( 'This row was changed by another session.', 'desktop-mode' ),
661 866 array(
662 867 'status' => 409,
663 868 'data' => array(
@@ -663,10 +868,10 @@
663 868 'data' => array(
664 869 'reason' => $reason,
665 870 'actor' => $actor_payload,
666 871 'current' => array(
667 - 'parentId' => $parent_id,
668 - 'parentName' => $parent_name,
872 + 'parentId' => $viewer_can_see ? $parent_id : 0,
873 + 'parentName' => $viewer_can_see ? $parent_name : '',
669 874 'updatedAtMs' => (int) $current_ms,
670 875 ),
671 876 ),
672 877 )
@@ -675,14 +880,12 @@
675 880
676 881 /**
677 882 * Shape a share row for the wire.
678 883 *
679 - * @since 0.18.0
680 - *
681 884 * @param array|null $row Normalized share row.
682 885 * @return array
683 886 */
684 -function desktop_mode_files_shape_share( $row ) {
887 +function openstation_files_shape_share( $row ) {
685 888 if ( ! is_array( $row ) ) {
686 889 return array();
687 890 }
688 891 $shape = array(
@@ -696,15 +899,15 @@
696 899 'invitedAtMs' => (int) $row['invited_at_ms'],
697 900 'decidedAtMs' => isset( $row['decided_at_ms'] ) ? $row['decided_at_ms'] : null,
698 901 );
699 902 if ( 'user' === $row['principal_type'] ) {
700 - $uid = (int) $row['principal_ref'];
701 - $user = $uid > 0 ? get_userdata( $uid ) : null;
903 + $uid = (int) $row['principal_ref'];
904 + $user = $uid > 0 ? get_userdata( $uid ) : null;
702 905 $shape['displayName'] = $user ? $user->display_name : '';
703 906 $shape['avatarUrl'] = $user ? get_avatar_url( $uid, array( 'size' => 48 ) ) : '';
704 907 } else {
705 - $roles = wp_roles();
706 - $info = $roles && isset( $roles->roles[ $row['principal_ref'] ] ) ? $roles->roles[ $row['principal_ref'] ] : null;
908 + $roles = wp_roles();
909 + $info = $roles && isset( $roles->roles[ $row['principal_ref'] ] ) ? $roles->roles[ $row['principal_ref'] ] : null;
707 910 $shape['displayName'] = $info ? translate_user_role( (string) $info['name'] ) : (string) $row['principal_ref'];
708 911 $shape['avatarUrl'] = '';
709 912 }
710 913 return $shape;
@@ -712,22 +915,22 @@
712 915
713 916 /**
714 917 * GET /folders/<id>/shares — owner only.
715 918 */
716 -function desktop_mode_files_rest_list_shares( WP_REST_Request $req ) {
919 +function openstation_files_rest_list_shares( WP_REST_Request $req ) {
717 920 $folder_id = (int) $req['id'];
718 921 $user_id = get_current_user_id();
719 - if ( ! desktop_mode_files_share_can_manage( $folder_id, $user_id ) ) {
720 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot view shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
922 + if ( ! openstation_files_share_can_manage( $folder_id, $user_id ) ) {
923 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot view shares for this folder.', 'desktop-mode' ), array( 'status' => 403 ) );
721 924 }
722 - $folder = desktop_mode_files_get_folder( $folder_id );
925 + $folder = openstation_files_get_folder( $folder_id );
723 926 if ( ! $folder ) {
724 - return new WP_Error( 'desktop_mode_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
927 + return new WP_Error( 'openstation_files_not_found', __( 'Folder not found.', 'desktop-mode' ), array( 'status' => 404 ) );
725 928 }
726 - $rows = desktop_mode_files_get_folder_shares( $folder_id );
727 - $out = array();
929 + $rows = openstation_files_get_folder_shares( $folder_id );
930 + $out = array();
728 931 foreach ( $rows as $row ) {
729 - $out[] = desktop_mode_files_shape_share( $row );
932 + $out[] = openstation_files_shape_share( $row );
730 933 }
731 934 return rest_ensure_response(
732 935 array(
733 936 'shares' => $out,
@@ -739,12 +942,12 @@
739 942
740 943 /**
741 944 * POST /folders/<id>/shares — owner only.
742 945 */
743 -function desktop_mode_files_rest_create_share( WP_REST_Request $req ) {
946 +function openstation_files_rest_create_share( WP_REST_Request $req ) {
744 947 $folder_id = (int) $req['id'];
745 948 $actor_id = get_current_user_id();
746 - $id = desktop_mode_folder_share_invite(
949 + $id = openstation_folder_share_invite(
747 950 $folder_id,
748 951 $actor_id,
749 952 (string) $req->get_param( 'principalType' ),
750 953 (string) $req->get_param( 'principalRef' ),
@@ -752,9 +955,9 @@
752 955 );
753 956 if ( is_wp_error( $id ) ) {
754 957 return $id;
755 958 }
756 - return rest_ensure_response( desktop_mode_files_shape_share( desktop_mode_files_get_share( $id ) ) );
959 + return rest_ensure_response( openstation_files_shape_share( openstation_files_get_share( $id ) ) );
757 960 }
758 961
759 962 /**
760 963 * Verify that the share id in the URL actually belongs to the
@@ -764,20 +967,18 @@
764 967 * mismatched URL never escalates permission — but the routes are
765 968 * hierarchical (`/folders/{id}/shares/{shareId}/…`), so honoring
766 969 * both path segments is the contract callers expect.
767 970 *
768 - * @since 0.18.x
769 - *
770 971 * @param WP_REST_Request $req Request.
771 972 * @return array|WP_Error
772 973 */
773 -function desktop_mode_files_rest_resolve_share_in_folder( WP_REST_Request $req ) {
974 +function openstation_files_rest_resolve_share_in_folder( WP_REST_Request $req ) {
774 975 $folder_id = (int) $req['id'];
775 976 $share_id = (int) $req['shareId'];
776 - $share = desktop_mode_files_get_share( $share_id );
977 + $share = openstation_files_get_share( $share_id );
777 978 if ( ! $share ) {
778 979 return new WP_Error(
779 - 'desktop_mode_files_not_found',
980 + 'openstation_files_not_found',
780 981 __( 'Share not found.', 'desktop-mode' ),
781 982 array( 'status' => 404 )
782 983 );
783 984 }
@@ -782,9 +983,9 @@
782 983 );
783 984 }
784 985 if ( (int) $share['folder_id'] !== $folder_id ) {
785 986 return new WP_Error(
786 - 'desktop_mode_files_not_found',
987 + 'openstation_files_not_found',
787 988 __( 'Share not found in this folder.', 'desktop-mode' ),
788 989 array( 'status' => 404 )
789 990 );
790 991 }
@@ -793,30 +994,30 @@
793 994
794 995 /**
795 996 * PATCH /folders/<id>/shares/<shareId> — owner only.
796 997 */
797 -function desktop_mode_files_rest_update_share( WP_REST_Request $req ) {
798 - $share = desktop_mode_files_rest_resolve_share_in_folder( $req );
998 +function openstation_files_rest_update_share( WP_REST_Request $req ) {
999 + $share = openstation_files_rest_resolve_share_in_folder( $req );
799 1000 if ( is_wp_error( $share ) ) {
800 1001 return $share;
801 1002 }
802 1003 $share_id = (int) $share['id'];
803 - $ok = desktop_mode_folder_share_update_capability( $share_id, get_current_user_id(), (string) $req->get_param( 'capability' ) );
1004 + $ok = openstation_folder_share_update_capability( $share_id, get_current_user_id(), (string) $req->get_param( 'capability' ) );
804 1005 if ( is_wp_error( $ok ) ) {
805 1006 return $ok;
806 1007 }
807 - return rest_ensure_response( desktop_mode_files_shape_share( desktop_mode_files_get_share( $share_id ) ) );
1008 + return rest_ensure_response( openstation_files_shape_share( openstation_files_get_share( $share_id ) ) );
808 1009 }
809 1010
810 1011 /**
811 1012 * DELETE /folders/<id>/shares/<shareId> — owner only.
812 1013 */
813 -function desktop_mode_files_rest_delete_share( WP_REST_Request $req ) {
814 - $share = desktop_mode_files_rest_resolve_share_in_folder( $req );
1014 +function openstation_files_rest_delete_share( WP_REST_Request $req ) {
1015 + $share = openstation_files_rest_resolve_share_in_folder( $req );
815 1016 if ( is_wp_error( $share ) ) {
816 1017 return $share;
817 1018 }
818 - $ok = desktop_mode_folder_share_revoke( (int) $share['id'], get_current_user_id() );
1019 + $ok = openstation_folder_share_revoke( (int) $share['id'], get_current_user_id() );
819 1020 if ( is_wp_error( $ok ) ) {
820 1021 return $ok;
821 1022 }
822 1023 return rest_ensure_response( array( 'deleted' => true ) );
@@ -824,33 +1025,33 @@
824 1025
825 1026 /**
826 1027 * POST /folders/<id>/shares/<shareId>/accept — recipient only.
827 1028 */
828 -function desktop_mode_files_rest_accept_share( WP_REST_Request $req ) {
829 - $share = desktop_mode_files_rest_resolve_share_in_folder( $req );
1029 +function openstation_files_rest_accept_share( WP_REST_Request $req ) {
1030 + $share = openstation_files_rest_resolve_share_in_folder( $req );
830 1031 if ( is_wp_error( $share ) ) {
831 1032 return $share;
832 1033 }
833 - $row = desktop_mode_folder_share_accept( (int) $share['id'], get_current_user_id() );
1034 + $row = openstation_folder_share_accept( (int) $share['id'], get_current_user_id() );
834 1035 if ( is_wp_error( $row ) ) {
835 1036 return $row;
836 1037 }
837 - return rest_ensure_response( desktop_mode_files_shape_share( $row ) );
1038 + return rest_ensure_response( openstation_files_shape_share( $row ) );
838 1039 }
839 1040
840 1041 /**
841 1042 * POST /folders/<id>/shares/<shareId>/deny — recipient only.
842 1043 */
843 -function desktop_mode_files_rest_deny_share( WP_REST_Request $req ) {
844 - $share = desktop_mode_files_rest_resolve_share_in_folder( $req );
1044 +function openstation_files_rest_deny_share( WP_REST_Request $req ) {
1045 + $share = openstation_files_rest_resolve_share_in_folder( $req );
845 1046 if ( is_wp_error( $share ) ) {
846 1047 return $share;
847 1048 }
848 - $row = desktop_mode_folder_share_deny( (int) $share['id'], get_current_user_id() );
1049 + $row = openstation_folder_share_deny( (int) $share['id'], get_current_user_id() );
849 1050 if ( is_wp_error( $row ) ) {
850 1051 return $row;
851 1052 }
852 - return rest_ensure_response( desktop_mode_files_shape_share( $row ) );
1053 + return rest_ensure_response( openstation_files_shape_share( $row ) );
853 1054 }
854 1055
855 1056 /**
856 1057 * POST /folders/<id>/leave — recipient-initiated leave.
@@ -860,11 +1061,11 @@
860 1061 * see the folder (user-principal or role-principal) and removes
861 1062 * their access — for role shares without affecting other role
862 1063 * members, via the per-user decisions table.
863 1064 */
864 -function desktop_mode_files_rest_leave_folder( WP_REST_Request $req ) {
1065 +function openstation_files_rest_leave_folder( WP_REST_Request $req ) {
865 1066 $folder_id = (int) $req['id'];
866 - $ok = desktop_mode_folder_share_leave( $folder_id, get_current_user_id() );
1067 + $ok = openstation_folder_share_leave( $folder_id, get_current_user_id() );
867 1068 if ( is_wp_error( $ok ) ) {
868 1069 return $ok;
869 1070 }
870 1071 return rest_ensure_response( array( 'left' => true ) );
@@ -874,9 +1075,9 @@
874 1075 * POST /files/folder-sharing-tables/purge — destructive cleanup
875 1076 * that drops every table the folder-sharing feature ever created
876 1077 * (current `folder_shares` + `share_user_decisions`, plus any
877 1078 * future variants enumerated via the
878 - * `desktop_mode_files_sharing_tables_for_purge` filter).
1079 + * `openstation_files_sharing_tables_for_purge` filter).
879 1080 *
880 1081 * Restricted to `manage_options` by the permission callback. The
881 1082 * schema-version option is cleared so the next admin-init runs
882 1083 * `install_schema` and recreates the empty tables — keeps the
@@ -881,14 +1082,12 @@
881 1082 * schema-version option is cleared so the next admin-init runs
882 1083 * `install_schema` and recreates the empty tables — keeps the
883 1084 * code path that ASSUMES the tables exist (e.g. heartbeat
884 1085 * delivery queries) working even after a purge.
885 - *
886 - * @since 0.18.x
887 1086 */
888 -function desktop_mode_files_rest_purge_sharing_tables() {
1087 +function openstation_files_rest_purge_sharing_tables() {
889 1088 global $wpdb;
890 - $tables = desktop_mode_files_table_names();
1089 + $tables = openstation_files_table_names();
891 1090
892 1091 $to_drop = array( $tables['shares'], $tables['decisions'] );
893 1092 /**
894 1093 * Filter the list of table names dropped by the
@@ -893,13 +1092,11 @@
893 1092 /**
894 1093 * Filter the list of table names dropped by the
895 1094 * "Delete folder sharing data" admin action.
896 1095 *
897 - * @since 0.18.x
898 - *
899 1096 * @param string[] $tables Default = shares + decisions.
900 1097 */
901 - $to_drop = (array) apply_filters( 'desktop_mode_files_sharing_tables_for_purge', $to_drop );
1098 + $to_drop = (array) apply_filters( 'openstation_files_sharing_tables_for_purge', $to_drop );
902 1099
903 1100 $dropped = array();
904 1101 $skipped = array();
905 1102 $prefix = (string) $wpdb->prefix;
@@ -911,14 +1108,14 @@
911 1108 // Defense-in-depth: a misbehaving filter could push any
912 1109 // string into `$to_drop` and we're about to interpolate
913 1110 // the value directly into a `DROP TABLE` statement (wpdb
914 1111 // has no placeholder for identifiers). Two gates:
915 - // 1. Must match the `[A-Za-z0-9_]+` identifier pattern —
916 - // keeps quotes/backticks/spaces out of the SQL even
917 - // if a filter author smuggled them in.
918 - // 2. Must start with the wpdb prefix — keeps a malicious
919 - // filter from dropping system tables (`wp_users`,
920 - // `wp_options`, …) on a multi-prefix install.
1112 + // 1. Must match the `[A-Za-z0-9_]+` identifier pattern —
1113 + // keeps quotes/backticks/spaces out of the SQL even
1114 + // if a filter author smuggled them in.
1115 + // 2. Must start with the wpdb prefix — keeps a malicious
1116 + // filter from dropping system tables (`wp_users`,
1117 + // `wp_options`, …) on a multi-prefix install.
921 1118 if (
922 1119 ! preg_match( '/^[A-Za-z0-9_]+$/', $tbl ) ||
923 1120 0 !== strpos( $tbl, $prefix )
924 1121 ) {
@@ -936,9 +1133,9 @@
936 1133 // `install_schema` so the tables are recreated empty. Code
937 1134 // paths that JOIN against them (heartbeat, sharing.php
938 1135 // visibility) keep working without a per-request existence
939 1136 // check.
940 - delete_option( DESKTOP_MODE_FILES_SCHEMA_OPTION );
1137 + delete_option( OPENSTATION_FILES_SCHEMA_OPTION );
941 1138
942 1139 /**
943 1140 * Fires after the folder-sharing tables are purged. Plugins
944 1141 * that mirror share state into their own storage can react
@@ -943,35 +1140,33 @@
943 1140 * Fires after the folder-sharing tables are purged. Plugins
944 1141 * that mirror share state into their own storage can react
945 1142 * here.
946 1143 *
947 - * @since 0.18.x
948 - *
949 1144 * @param string[] $dropped Table names that were dropped.
950 1145 */
951 - do_action( 'desktop_mode_files_sharing_tables_purged', $dropped );
1146 + do_action( 'openstation_files_sharing_tables_purged', $dropped );
952 1147
953 - return rest_ensure_response( array(
954 - 'dropped' => $dropped,
955 - 'skipped' => $skipped,
956 - ) );
1148 + return rest_ensure_response(
1149 + array(
1150 + 'dropped' => $dropped,
1151 + 'skipped' => $skipped,
1152 + )
1153 + );
957 1154 }
958 1155
959 1156 /**
960 1157 * Permission gate for /users/search. Requires `edit_posts` —
961 - * `desktop_mode_files_rest_permission` would let any logged-in
962 - * desktop-mode user pull the directory, which is too broad for an
1158 + * `openstation_files_rest_permission` would let any logged-in
1159 + * openstation user pull the directory, which is too broad for an
963 1160 * autocomplete that exposes display names + emails.
964 - *
965 - * @since 0.18.0
966 1161 */
967 -function desktop_mode_files_rest_search_users_permission() {
968 - $base = desktop_mode_files_rest_permission();
1162 +function openstation_files_rest_search_users_permission() {
1163 + $base = openstation_files_rest_permission();
969 1164 if ( is_wp_error( $base ) ) {
970 1165 return $base;
971 1166 }
972 1167 if ( ! current_user_can( 'edit_posts' ) ) {
973 - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot search users.', 'desktop-mode' ), array( 'status' => 403 ) );
1168 + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot search users.', 'desktop-mode' ), array( 'status' => 403 ) );
974 1169 }
975 1170 return true;
976 1171 }
977 1172
@@ -977,12 +1172,10 @@
977 1172
978 1173 /**
979 1174 * GET /files/users/search?q=<>&exclude=<csv> — autocomplete for the
980 1175 * folder share picker.
981 - *
982 - * @since 0.18.0
983 1176 */
984 -function desktop_mode_files_rest_search_users( WP_REST_Request $req ) {
1177 +function openstation_files_rest_search_users( WP_REST_Request $req ) {
985 1178 $q = trim( (string) $req->get_param( 'q' ) );
986 1179 $exclude = array_filter( array_map( 'intval', explode( ',', (string) $req->get_param( 'exclude' ) ) ) );
987 1180
988 1181 // Always exclude the current viewer — sharing with yourself is
@@ -1009,14 +1202,12 @@
1009 1202
1010 1203 /**
1011 1204 * Filter the WP_User_Query args used by the share picker.
1012 1205 *
1013 - * @since 0.18.0
1014 - *
1015 1206 * @param array $args Default args.
1016 1207 * @param array $req Request params (`q`, `exclude`).
1017 1208 */
1018 - $args = (array) apply_filters( 'desktop_mode_files_share_user_query_args', $args, $req->get_params() );
1209 + $args = (array) apply_filters( 'openstation_files_share_user_query_args', $args, $req->get_params() );
1019 1210
1020 1211 $query = new WP_User_Query( $args );
1021 1212 $users = $query->get_results();
1022 1213 $out = array();