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 -363 0.8.81.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,19 +805,18 @@
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 - } elseif ( isset( $row['user_id'] ) ) {
618 - $actor_id = (int) $row['user_id'];
619 819 } elseif ( isset( $row['owner_id'] ) ) {
620 820 $actor_id = (int) $row['owner_id'];
621 821 }
622 822 $actor = $actor_id ? get_userdata( $actor_id ) : null;
@@ -623,9 +823,9 @@
623 823
624 824 $parent_id = isset( $row['parent_id'] ) ? (int) $row['parent_id'] : 0;
625 825 $parent_name = '';
626 826 if ( $parent_id > 0 ) {
627 - $parent_folder = desktop_mode_files_get_folder( $parent_id );
827 + $parent_folder = openstation_files_get_folder( $parent_id );
628 828 $parent_name = $parent_folder ? (string) $parent_folder['name'] : '';
629 829 }
630 830
631 831 $reason = 'parent_changed';
@@ -633,23 +833,26 @@
633 833 $reason = 'trashed';
634 834 }
635 835
636 836 // PII gate. The conflict toast names the actor (display name +
637 - // avatar) only when the requesting viewer is in the same
638 - // collaboration scope as the actor — i.e. owns the row, owns
639 - // the parent folder, or has at least read access to the parent
640 - // folder via the shares table. For any other viewer the actor
641 - // degrades to a generic "another session" — `id: 0`, empty
642 - // name + avatar — so a write attempt can't be used to enumerate
643 - // other users' display names.
644 - $viewer_id = (int) get_current_user_id();
645 - $viewer_owns_row = isset( $row['user_id'] ) && (int) $row['user_id'] === $viewer_id;
646 - $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;
647 850 if ( ! $viewer_can_see && $parent_id > 0 && isset( $parent_folder ) && $parent_folder ) {
648 851 if ( (int) $parent_folder['owner_id'] === $viewer_id ) {
649 852 $viewer_can_see = true;
650 - } elseif ( function_exists( 'desktop_mode_folder_share_user_capability' ) ) {
651 - $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 );
652 855 }
653 856 }
654 857 $actor_payload = array(
655 858 'id' => $viewer_can_see ? $actor_id : 0,
@@ -657,9 +860,9 @@
657 860 'avatar' => $viewer_can_see && $actor ? get_avatar_url( $actor->ID, array( 'size' => 32 ) ) : '',
658 861 );
659 862
660 863 return new WP_Error(
661 - 'desktop_mode_files_conflict',
864 + 'openstation_files_conflict',
662 865 __( 'This row was changed by another session.', 'desktop-mode' ),
663 866 array(
664 867 'status' => 409,
665 868 'data' => array(
@@ -665,10 +868,10 @@
665 868 'data' => array(
666 869 'reason' => $reason,
667 870 'actor' => $actor_payload,
668 871 'current' => array(
669 - 'parentId' => $parent_id,
670 - 'parentName' => $parent_name,
872 + 'parentId' => $viewer_can_see ? $parent_id : 0,
873 + 'parentName' => $viewer_can_see ? $parent_name : '',
671 874 'updatedAtMs' => (int) $current_ms,
672 875 ),
673 876 ),
674 877 )
@@ -677,14 +880,12 @@
677 880
678 881 /**
679 882 * Shape a share row for the wire.
680 883 *
681 - * @since 0.18.0
682 - *
683 884 * @param array|null $row Normalized share row.
684 885 * @return array
685 886 */
686 -function desktop_mode_files_shape_share( $row ) {
887 +function openstation_files_shape_share( $row ) {
687 888 if ( ! is_array( $row ) ) {
688 889 return array();
689 890 }
690 891 $shape = array(
@@ -698,15 +899,15 @@
698 899 'invitedAtMs' => (int) $row['invited_at_ms'],
699 900 'decidedAtMs' => isset( $row['decided_at_ms'] ) ? $row['decided_at_ms'] : null,
700 901 );
701 902 if ( 'user' === $row['principal_type'] ) {
702 - $uid = (int) $row['principal_ref'];
703 - $user = $uid > 0 ? get_userdata( $uid ) : null;
903 + $uid = (int) $row['principal_ref'];
904 + $user = $uid > 0 ? get_userdata( $uid ) : null;
704 905 $shape['displayName'] = $user ? $user->display_name : '';
705 906 $shape['avatarUrl'] = $user ? get_avatar_url( $uid, array( 'size' => 48 ) ) : '';
706 907 } else {
707 - $roles = wp_roles();
708 - $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;
709 910 $shape['displayName'] = $info ? translate_user_role( (string) $info['name'] ) : (string) $row['principal_ref'];
710 911 $shape['avatarUrl'] = '';
711 912 }
712 913 return $shape;
@@ -714,22 +915,22 @@
714 915
715 916 /**
716 917 * GET /folders/<id>/shares — owner only.
717 918 */
718 -function desktop_mode_files_rest_list_shares( WP_REST_Request $req ) {
919 +function openstation_files_rest_list_shares( WP_REST_Request $req ) {
719 920 $folder_id = (int) $req['id'];
720 921 $user_id = get_current_user_id();
721 - if ( ! desktop_mode_files_share_can_manage( $folder_id, $user_id ) ) {
722 - 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 ) );
723 924 }
724 - $folder = desktop_mode_files_get_folder( $folder_id );
925 + $folder = openstation_files_get_folder( $folder_id );
725 926 if ( ! $folder ) {
726 - 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 ) );
727 928 }
728 - $rows = desktop_mode_files_get_folder_shares( $folder_id );
729 - $out = array();
929 + $rows = openstation_files_get_folder_shares( $folder_id );
930 + $out = array();
730 931 foreach ( $rows as $row ) {
731 - $out[] = desktop_mode_files_shape_share( $row );
932 + $out[] = openstation_files_shape_share( $row );
732 933 }
733 934 return rest_ensure_response(
734 935 array(
735 936 'shares' => $out,
@@ -741,12 +942,12 @@
741 942
742 943 /**
743 944 * POST /folders/<id>/shares — owner only.
744 945 */
745 -function desktop_mode_files_rest_create_share( WP_REST_Request $req ) {
946 +function openstation_files_rest_create_share( WP_REST_Request $req ) {
746 947 $folder_id = (int) $req['id'];
747 948 $actor_id = get_current_user_id();
748 - $id = desktop_mode_folder_share_invite(
949 + $id = openstation_folder_share_invite(
749 950 $folder_id,
750 951 $actor_id,
751 952 (string) $req->get_param( 'principalType' ),
752 953 (string) $req->get_param( 'principalRef' ),
@@ -754,9 +955,9 @@
754 955 );
755 956 if ( is_wp_error( $id ) ) {
756 957 return $id;
757 958 }
758 - 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 ) ) );
759 960 }
760 961
761 962 /**
762 963 * Verify that the share id in the URL actually belongs to the
@@ -766,20 +967,18 @@
766 967 * mismatched URL never escalates permission — but the routes are
767 968 * hierarchical (`/folders/{id}/shares/{shareId}/…`), so honoring
768 969 * both path segments is the contract callers expect.
769 970 *
770 - * @since 0.18.x
771 - *
772 971 * @param WP_REST_Request $req Request.
773 972 * @return array|WP_Error
774 973 */
775 -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 ) {
776 975 $folder_id = (int) $req['id'];
777 976 $share_id = (int) $req['shareId'];
778 - $share = desktop_mode_files_get_share( $share_id );
977 + $share = openstation_files_get_share( $share_id );
779 978 if ( ! $share ) {
780 979 return new WP_Error(
781 - 'desktop_mode_files_not_found',
980 + 'openstation_files_not_found',
782 981 __( 'Share not found.', 'desktop-mode' ),
783 982 array( 'status' => 404 )
784 983 );
785 984 }
@@ -784,9 +983,9 @@
784 983 );
785 984 }
786 985 if ( (int) $share['folder_id'] !== $folder_id ) {
787 986 return new WP_Error(
788 - 'desktop_mode_files_not_found',
987 + 'openstation_files_not_found',
789 988 __( 'Share not found in this folder.', 'desktop-mode' ),
790 989 array( 'status' => 404 )
791 990 );
792 991 }
@@ -795,30 +994,30 @@
795 994
796 995 /**
797 996 * PATCH /folders/<id>/shares/<shareId> — owner only.
798 997 */
799 -function desktop_mode_files_rest_update_share( WP_REST_Request $req ) {
800 - $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 );
801 1000 if ( is_wp_error( $share ) ) {
802 1001 return $share;
803 1002 }
804 1003 $share_id = (int) $share['id'];
805 - $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' ) );
806 1005 if ( is_wp_error( $ok ) ) {
807 1006 return $ok;
808 1007 }
809 - 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 ) ) );
810 1009 }
811 1010
812 1011 /**
813 1012 * DELETE /folders/<id>/shares/<shareId> — owner only.
814 1013 */
815 -function desktop_mode_files_rest_delete_share( WP_REST_Request $req ) {
816 - $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 );
817 1016 if ( is_wp_error( $share ) ) {
818 1017 return $share;
819 1018 }
820 - $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() );
821 1020 if ( is_wp_error( $ok ) ) {
822 1021 return $ok;
823 1022 }
824 1023 return rest_ensure_response( array( 'deleted' => true ) );
@@ -826,33 +1025,33 @@
826 1025
827 1026 /**
828 1027 * POST /folders/<id>/shares/<shareId>/accept — recipient only.
829 1028 */
830 -function desktop_mode_files_rest_accept_share( WP_REST_Request $req ) {
831 - $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 );
832 1031 if ( is_wp_error( $share ) ) {
833 1032 return $share;
834 1033 }
835 - $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() );
836 1035 if ( is_wp_error( $row ) ) {
837 1036 return $row;
838 1037 }
839 - return rest_ensure_response( desktop_mode_files_shape_share( $row ) );
1038 + return rest_ensure_response( openstation_files_shape_share( $row ) );
840 1039 }
841 1040
842 1041 /**
843 1042 * POST /folders/<id>/shares/<shareId>/deny — recipient only.
844 1043 */
845 -function desktop_mode_files_rest_deny_share( WP_REST_Request $req ) {
846 - $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 );
847 1046 if ( is_wp_error( $share ) ) {
848 1047 return $share;
849 1048 }
850 - $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() );
851 1050 if ( is_wp_error( $row ) ) {
852 1051 return $row;
853 1052 }
854 - return rest_ensure_response( desktop_mode_files_shape_share( $row ) );
1053 + return rest_ensure_response( openstation_files_shape_share( $row ) );
855 1054 }
856 1055
857 1056 /**
858 1057 * POST /folders/<id>/leave — recipient-initiated leave.
@@ -862,11 +1061,11 @@
862 1061 * see the folder (user-principal or role-principal) and removes
863 1062 * their access — for role shares without affecting other role
864 1063 * members, via the per-user decisions table.
865 1064 */
866 -function desktop_mode_files_rest_leave_folder( WP_REST_Request $req ) {
1065 +function openstation_files_rest_leave_folder( WP_REST_Request $req ) {
867 1066 $folder_id = (int) $req['id'];
868 - $ok = desktop_mode_folder_share_leave( $folder_id, get_current_user_id() );
1067 + $ok = openstation_folder_share_leave( $folder_id, get_current_user_id() );
869 1068 if ( is_wp_error( $ok ) ) {
870 1069 return $ok;
871 1070 }
872 1071 return rest_ensure_response( array( 'left' => true ) );
@@ -876,9 +1075,9 @@
876 1075 * POST /files/folder-sharing-tables/purge — destructive cleanup
877 1076 * that drops every table the folder-sharing feature ever created
878 1077 * (current `folder_shares` + `share_user_decisions`, plus any
879 1078 * future variants enumerated via the
880 - * `desktop_mode_files_sharing_tables_for_purge` filter).
1079 + * `openstation_files_sharing_tables_for_purge` filter).
881 1080 *
882 1081 * Restricted to `manage_options` by the permission callback. The
883 1082 * schema-version option is cleared so the next admin-init runs
884 1083 * `install_schema` and recreates the empty tables — keeps the
@@ -883,14 +1082,12 @@
883 1082 * schema-version option is cleared so the next admin-init runs
884 1083 * `install_schema` and recreates the empty tables — keeps the
885 1084 * code path that ASSUMES the tables exist (e.g. heartbeat
886 1085 * delivery queries) working even after a purge.
887 - *
888 - * @since 0.18.x
889 1086 */
890 -function desktop_mode_files_rest_purge_sharing_tables() {
1087 +function openstation_files_rest_purge_sharing_tables() {
891 1088 global $wpdb;
892 - $tables = desktop_mode_files_table_names();
1089 + $tables = openstation_files_table_names();
893 1090
894 1091 $to_drop = array( $tables['shares'], $tables['decisions'] );
895 1092 /**
896 1093 * Filter the list of table names dropped by the
@@ -895,13 +1092,11 @@
895 1092 /**
896 1093 * Filter the list of table names dropped by the
897 1094 * "Delete folder sharing data" admin action.
898 1095 *
899 - * @since 0.18.x
900 - *
901 1096 * @param string[] $tables Default = shares + decisions.
902 1097 */
903 - $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 );
904 1099
905 1100 $dropped = array();
906 1101 $skipped = array();
907 1102 $prefix = (string) $wpdb->prefix;
@@ -913,14 +1108,14 @@
913 1108 // Defense-in-depth: a misbehaving filter could push any
914 1109 // string into `$to_drop` and we're about to interpolate
915 1110 // the value directly into a `DROP TABLE` statement (wpdb
916 1111 // has no placeholder for identifiers). Two gates:
917 - // 1. Must match the `[A-Za-z0-9_]+` identifier pattern —
918 - // keeps quotes/backticks/spaces out of the SQL even
919 - // if a filter author smuggled them in.
920 - // 2. Must start with the wpdb prefix — keeps a malicious
921 - // filter from dropping system tables (`wp_users`,
922 - // `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.
923 1118 if (
924 1119 ! preg_match( '/^[A-Za-z0-9_]+$/', $tbl ) ||
925 1120 0 !== strpos( $tbl, $prefix )
926 1121 ) {
@@ -938,9 +1133,9 @@
938 1133 // `install_schema` so the tables are recreated empty. Code
939 1134 // paths that JOIN against them (heartbeat, sharing.php
940 1135 // visibility) keep working without a per-request existence
941 1136 // check.
942 - delete_option( DESKTOP_MODE_FILES_SCHEMA_OPTION );
1137 + delete_option( OPENSTATION_FILES_SCHEMA_OPTION );
943 1138
944 1139 /**
945 1140 * Fires after the folder-sharing tables are purged. Plugins
946 1141 * that mirror share state into their own storage can react
@@ -945,35 +1140,33 @@
945 1140 * Fires after the folder-sharing tables are purged. Plugins
946 1141 * that mirror share state into their own storage can react
947 1142 * here.
948 1143 *
949 - * @since 0.18.x
950 - *
951 1144 * @param string[] $dropped Table names that were dropped.
952 1145 */
953 - do_action( 'desktop_mode_files_sharing_tables_purged', $dropped );
1146 + do_action( 'openstation_files_sharing_tables_purged', $dropped );
954 1147
955 - return rest_ensure_response( array(
956 - 'dropped' => $dropped,
957 - 'skipped' => $skipped,
958 - ) );
1148 + return rest_ensure_response(
1149 + array(
1150 + 'dropped' => $dropped,
1151 + 'skipped' => $skipped,
1152 + )
1153 + );
959 1154 }
960 1155
961 1156 /**
962 1157 * Permission gate for /users/search. Requires `edit_posts` —
963 - * `desktop_mode_files_rest_permission` would let any logged-in
964 - * 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
965 1160 * autocomplete that exposes display names + emails.
966 - *
967 - * @since 0.18.0
968 1161 */
969 -function desktop_mode_files_rest_search_users_permission() {
970 - $base = desktop_mode_files_rest_permission();
1162 +function openstation_files_rest_search_users_permission() {
1163 + $base = openstation_files_rest_permission();
971 1164 if ( is_wp_error( $base ) ) {
972 1165 return $base;
973 1166 }
974 1167 if ( ! current_user_can( 'edit_posts' ) ) {
975 - 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 ) );
976 1169 }
977 1170 return true;
978 1171 }
979 1172
@@ -979,12 +1172,10 @@
979 1172
980 1173 /**
981 1174 * GET /files/users/search?q=<>&exclude=<csv> — autocomplete for the
982 1175 * folder share picker.
983 - *
984 - * @since 0.18.0
985 1176 */
986 -function desktop_mode_files_rest_search_users( WP_REST_Request $req ) {
1177 +function openstation_files_rest_search_users( WP_REST_Request $req ) {
987 1178 $q = trim( (string) $req->get_param( 'q' ) );
988 1179 $exclude = array_filter( array_map( 'intval', explode( ',', (string) $req->get_param( 'exclude' ) ) ) );
989 1180
990 1181 // Always exclude the current viewer — sharing with yourself is
@@ -1011,14 +1202,12 @@
1011 1202
1012 1203 /**
1013 1204 * Filter the WP_User_Query args used by the share picker.
1014 1205 *
1015 - * @since 0.18.0
1016 - *
1017 1206 * @param array $args Default args.
1018 1207 * @param array $req Request params (`q`, `exclude`).
1019 1208 */
1020 - $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() );
1021 1210
1022 1211 $query = new WP_User_Query( $args );
1023 1212 $users = $query->get_results();
1024 1213 $out = array();