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-uploads.php +221 -141 0.9.71.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — upload REST intake.
3 + * OpenStation — upload REST intake.
4 4 *
5 5 * One route:
6 6 *
7 7 * POST /desktop-mode/v1/files/uploads
@@ -18,10 +18,9 @@
18 18 * `post_max_size` aggregates. Internally the handler is split into
19 19 * receive (bytes → disk) and register (row + placement) so a future
20 20 * resumable-upload layer can feed the same register step.
21 21 *
22 - * @package WPDesktopMode
23 - * @since 0.9.6
22 + * @package OpenStation
24 23 */
25 24
26 25 defined( 'ABSPATH' ) || exit;
27 26
@@ -33,38 +32,45 @@
33 32 * OWASP double-extension guidance. Belt and suspenders: the WP
34 33 * MIME policy would reject most of these anyway, and the stored
35 34 * disk name is an extensionless UUID that no handler dispatches.
36 35 *
37 - * @since 0.9.6
38 - *
39 36 * @return string[]
40 37 */
41 -function desktop_mode_stored_files_denied_extensions() {
38 +function openstation_stored_files_denied_extensions() {
42 39 $denied = array(
43 - 'php', 'php3', 'php4', 'php5', 'php7', 'php8',
44 - 'phtml', 'phar', 'pht', 'phps',
45 - 'cgi', 'pl', 'asp', 'aspx', 'jsp', 'shtml',
40 + 'php',
41 + 'php3',
42 + 'php4',
43 + 'php5',
44 + 'php7',
45 + 'php8',
46 + 'phtml',
47 + 'phar',
48 + 'pht',
49 + 'phps',
50 + 'cgi',
51 + 'pl',
52 + 'asp',
53 + 'aspx',
54 + 'jsp',
55 + 'shtml',
46 56 );
47 57 /**
48 58 * Filters the hard-denied extension list. Narrowing this below
49 59 * the shipped set is strongly discouraged.
50 60 *
51 - * @since 0.9.6
52 - *
53 61 * @param string[] $denied Lowercase extensions.
54 62 */
55 - return (array) apply_filters( 'desktop_mode_stored_files_denied_extensions', $denied );
63 + return (array) apply_filters( 'openstation_stored_files_denied_extensions', $denied );
56 64 }
57 65
58 66 /**
59 67 * Whole-filename denylist (dotfiles / server config).
60 68 *
61 - * @since 0.9.6
62 - *
63 69 * @param string $name Client filename.
64 70 * @return bool True when the name is forbidden.
65 71 */
66 -function desktop_mode_stored_files_is_denied_filename( $name ) {
72 +function openstation_stored_files_is_denied_filename( $name ) {
67 73 $name = strtolower( trim( (string) $name ) );
68 74 if ( in_array( $name, array( '.htaccess', '.user.ini', 'web.config' ), true ) ) {
69 75 return true;
70 76 }
@@ -69,9 +75,9 @@
69 75 return true;
70 76 }
71 77 $segments = explode( '.', $name );
72 78 array_shift( $segments ); // Everything after the first dot is an "extension" segment.
73 - $denied = desktop_mode_stored_files_denied_extensions();
79 + $denied = openstation_stored_files_denied_extensions();
74 80 foreach ( $segments as $segment ) {
75 81 if ( in_array( $segment, $denied, true ) ) {
76 82 return true;
77 83 }
@@ -81,14 +87,12 @@
81 87
82 88 /**
83 89 * Effective per-file size cap in bytes.
84 90 *
85 - * @since 0.9.6
86 - *
87 91 * @param int $user_id User.
88 92 * @return int
89 93 */
90 -function desktop_mode_stored_files_max_upload_bytes( $user_id ) {
94 +function openstation_stored_files_max_upload_bytes( $user_id ) {
91 95 $max = (int) wp_max_upload_size();
92 96 /**
93 97 * Filters the per-file upload cap for desktop storage. May only
94 98 * effectively lower it below the server limits — PHP discards
@@ -93,29 +97,25 @@
93 97 * Filters the per-file upload cap for desktop storage. May only
94 98 * effectively lower it below the server limits — PHP discards
95 99 * larger bodies before WordPress runs.
96 100 *
97 - * @since 0.9.6
98 - *
99 101 * @param int $max Cap in bytes. Default `wp_max_upload_size()`.
100 102 * @param int $user_id User.
101 103 */
102 - return max( 0, (int) apply_filters( 'desktop_mode_stored_files_max_upload_bytes', $max, (int) $user_id ) );
104 + return max( 0, (int) apply_filters( 'openstation_stored_files_max_upload_bytes', $max, (int) $user_id ) );
103 105 }
104 106
105 107 /**
106 108 * Permission: base files gate + the upload capability.
107 - *
108 - * @since 0.9.6
109 109 */
110 -function desktop_mode_files_rest_uploads_permission() {
111 - $base = desktop_mode_files_rest_permission();
110 +function openstation_files_rest_uploads_permission() {
111 + $base = openstation_files_rest_permission();
112 112 if ( is_wp_error( $base ) ) {
113 113 return $base;
114 114 }
115 - if ( ! current_user_can( desktop_mode_stored_files_upload_capability() ) ) {
115 + if ( ! current_user_can( openstation_stored_files_upload_capability() ) ) {
116 116 return new WP_Error(
117 - 'desktop_mode_stored_files_cannot_upload',
117 + 'openstation_stored_files_cannot_upload',
118 118 __( 'You are not allowed to upload files.', 'desktop-mode' ),
119 119 array( 'status' => 403 )
120 120 );
121 121 }
@@ -123,46 +123,79 @@
123 123 }
124 124
125 125 /**
126 126 * Register the upload route.
127 - *
128 - * @since 0.9.6
129 127 */
130 -function desktop_mode_files_register_upload_rest_routes() {
131 - register_rest_route( 'desktop-mode/v1', '/files/uploads', array(
132 - // POST only — PHP parses multipart into $_FILES for real
133 - // POST requests exclusively.
134 - 'methods' => WP_REST_Server::CREATABLE,
135 - 'permission_callback' => 'desktop_mode_files_rest_uploads_permission',
136 - 'callback' => 'desktop_mode_files_rest_upload',
137 - 'args' => array(
138 - 'parentId' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ),
139 - 'relativePath' => array( 'type' => 'string', 'default' => '' ),
140 - // No defaults on x/y on purpose: absent coords mean
141 - // "server picks the next free grid slot".
142 - 'x' => array( 'type' => 'integer', 'required' => false ),
143 - 'y' => array( 'type' => 'integer', 'required' => false ),
144 - ),
145 - ) );
128 +function openstation_files_register_upload_rest_routes() {
129 + register_rest_route(
130 + 'desktop-mode/v1',
131 + '/files/uploads',
132 + array(
133 + // POST only — PHP parses multipart into $_FILES for real
134 + // POST requests exclusively.
135 + 'methods' => WP_REST_Server::CREATABLE,
136 + 'permission_callback' => 'openstation_files_rest_uploads_permission',
137 + 'callback' => 'openstation_files_rest_upload',
138 + 'args' => array(
139 + 'parentId' => array(
140 + 'type' => 'integer',
141 + 'default' => 0,
142 + 'sanitize_callback' => 'absint',
143 + ),
144 + 'relativePath' => array(
145 + 'type' => 'string',
146 + 'default' => '',
147 + ),
148 + // No defaults on x/y on purpose: absent coords mean
149 + // "server picks the next free grid slot".
150 + 'x' => array(
151 + 'type' => 'integer',
152 + 'required' => false,
153 + ),
154 + 'y' => array(
155 + 'type' => 'integer',
156 + 'required' => false,
157 + ),
158 + ),
159 + )
160 + );
146 161
147 - register_rest_route( 'desktop-mode/v1', '/files/uploads/paths', array(
148 - 'methods' => WP_REST_Server::CREATABLE,
149 - 'permission_callback' => 'desktop_mode_files_rest_uploads_permission',
150 - 'callback' => 'desktop_mode_files_rest_ensure_upload_path',
151 - 'args' => array(
152 - 'parentId' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ),
153 - 'relativePath' => array( 'type' => 'string', 'required' => true ),
154 - ),
155 - ) );
162 + register_rest_route(
163 + 'desktop-mode/v1',
164 + '/files/uploads/paths',
165 + array(
166 + 'methods' => WP_REST_Server::CREATABLE,
167 + 'permission_callback' => 'openstation_files_rest_uploads_permission',
168 + 'callback' => 'openstation_files_rest_ensure_upload_path',
169 + 'args' => array(
170 + 'parentId' => array(
171 + 'type' => 'integer',
172 + 'default' => 0,
173 + 'sanitize_callback' => 'absint',
174 + ),
175 + 'relativePath' => array(
176 + 'type' => 'string',
177 + 'required' => true,
178 + ),
179 + ),
180 + )
181 + );
156 182
157 - register_rest_route( 'desktop-mode/v1', '/files/uploads/(?P<id>\d+)', array(
158 - 'methods' => WP_REST_Server::EDITABLE,
159 - 'permission_callback' => 'desktop_mode_files_rest_permission',
160 - 'callback' => 'desktop_mode_files_rest_rename_upload',
161 - 'args' => array(
162 - 'name' => array( 'type' => 'string', 'required' => true ),
163 - ),
164 - ) );
183 + register_rest_route(
184 + 'desktop-mode/v1',
185 + '/files/uploads/(?P<id>\d+)',
186 + array(
187 + 'methods' => WP_REST_Server::EDITABLE,
188 + 'permission_callback' => 'openstation_files_rest_permission',
189 + 'callback' => 'openstation_files_rest_rename_upload',
190 + 'args' => array(
191 + 'name' => array(
192 + 'type' => 'string',
193 + 'required' => true,
194 + ),
195 + ),
196 + )
197 + );
165 198 }
166 199
167 200 /**
168 201 * POST /files/uploads/paths — mkdir-p a directory path with no
@@ -169,30 +202,35 @@
169 202 * file attached. Used by folder-tree drops to preserve EMPTY
170 203 * directories (the drag-drop Entries API sees them; files-only
171 204 * transports lose them).
172 205 *
173 - * @since 0.9.6
174 - *
175 206 * @param WP_REST_Request $req Request.
176 207 * @return WP_REST_Response|WP_Error
177 208 */
178 -function desktop_mode_files_rest_ensure_upload_path( WP_REST_Request $req ) {
209 +function openstation_files_rest_ensure_upload_path( WP_REST_Request $req ) {
179 210 $rel = (string) $req->get_param( 'relativePath' );
180 211 if ( '' === trim( $rel, " \t/" ) ) {
181 - return new WP_Error( 'desktop_mode_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
212 + return new WP_Error( 'openstation_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
182 213 }
183 214 if ( '/' !== substr( $rel, -1 ) ) {
184 215 $rel .= '/'; // Whole string is a directory path.
185 216 }
186 - $folder_id = desktop_mode_files_resolve_relative_path(
217 + $created = array();
218 + $folder_id = openstation_files_resolve_relative_path(
187 219 get_current_user_id(),
188 220 (int) $req->get_param( 'parentId' ),
189 - $rel
221 + $rel,
222 + $created
190 223 );
191 224 if ( is_wp_error( $folder_id ) ) {
192 225 return $folder_id;
193 226 }
194 - return rest_ensure_response( array( 'folderId' => (int) $folder_id ) );
227 + return rest_ensure_response(
228 + array(
229 + 'folderId' => (int) $folder_id,
230 + 'createdFolders' => openstation_files_shape_created_folders( $created ),
231 + )
232 + );
195 233 }
196 234
197 235 /**
198 236 * PATCH /files/uploads/<id> — rename (owner only). Not-found and
@@ -197,25 +235,23 @@
197 235 /**
198 236 * PATCH /files/uploads/<id> — rename (owner only). Not-found and
199 237 * not-owner are both 404 (existence masking, same as downloads).
200 238 *
201 - * @since 0.9.6
202 - *
203 239 * @param WP_REST_Request $req Request.
204 240 * @return WP_REST_Response|WP_Error
205 241 */
206 -function desktop_mode_files_rest_rename_upload( WP_REST_Request $req ) {
242 +function openstation_files_rest_rename_upload( WP_REST_Request $req ) {
207 243 $file_id = (int) $req['id'];
208 244 $user_id = get_current_user_id();
209 - $row = desktop_mode_stored_files_get( $file_id );
245 + $row = openstation_stored_files_get( $file_id );
210 246 if ( ! $row || (int) $row['owner_id'] !== $user_id ) {
211 - return desktop_mode_files_download_not_found();
247 + return openstation_files_download_not_found();
212 248 }
213 - $ok = desktop_mode_stored_files_rename( $file_id, (string) $req->get_param( 'name' ) );
249 + $ok = openstation_stored_files_rename( $file_id, (string) $req->get_param( 'name' ) );
214 250 if ( is_wp_error( $ok ) ) {
215 251 return $ok;
216 252 }
217 - $row = desktop_mode_stored_files_get( $file_id );
253 + $row = openstation_stored_files_get( $file_id );
218 254 return rest_ensure_response(
219 255 array(
220 256 'id' => (int) $row['id'],
221 257 'name' => (string) $row['display_name'],
@@ -223,19 +259,17 @@
223 259 'mime' => (string) $row['mime'],
224 260 )
225 261 );
226 262 }
227 -add_action( 'rest_api_init', 'desktop_mode_files_register_upload_rest_routes' );
263 +add_action( 'rest_api_init', 'openstation_files_register_upload_rest_routes' );
228 264
229 265 /**
230 266 * POST /files/uploads
231 267 *
232 - * @since 0.9.6
233 - *
234 268 * @param WP_REST_Request $req Request.
235 269 * @return WP_REST_Response|WP_Error
236 270 */
237 -function desktop_mode_files_rest_upload( WP_REST_Request $req ) {
271 +function openstation_files_rest_upload( WP_REST_Request $req ) {
238 272 $user_id = get_current_user_id();
239 273 $files = $req->get_file_params();
240 274
241 275 // A body larger than `post_max_size` reaches PHP as a paramless
@@ -245,15 +279,15 @@
245 279 if ( empty( $files ) ) {
246 280 $content_length = isset( $_SERVER['CONTENT_LENGTH'] ) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
247 281 if ( $content_length > 0 ) {
248 282 return new WP_Error(
249 - 'desktop_mode_stored_files_too_large',
283 + 'openstation_stored_files_too_large',
250 284 __( 'That file is larger than this server accepts.', 'desktop-mode' ),
251 285 array( 'status' => 413 )
252 286 );
253 287 }
254 288 return new WP_Error(
255 - 'desktop_mode_stored_files_no_file',
289 + 'openstation_stored_files_no_file',
256 290 __( 'No file was uploaded.', 'desktop-mode' ),
257 291 array( 'status' => 400 )
258 292 );
259 293 }
@@ -258,15 +292,15 @@
258 292 );
259 293 }
260 294 if ( empty( $files['file'] ) || ! is_array( $files['file'] ) ) {
261 295 return new WP_Error(
262 - 'desktop_mode_stored_files_no_file',
296 + 'openstation_stored_files_no_file',
263 297 __( 'No file was uploaded.', 'desktop-mode' ),
264 298 array( 'status' => 400 )
265 299 );
266 300 }
267 301
268 - $received = desktop_mode_files_upload_receive( $files['file'], $user_id );
302 + $received = openstation_files_upload_receive( $files['file'], $user_id );
269 303 if ( is_wp_error( $received ) ) {
270 304 return $received;
271 305 }
272 306
@@ -278,9 +312,9 @@
278 312 'y' => (int) $y,
279 313 )
280 314 : null;
281 315
282 - $registered = desktop_mode_files_upload_register(
316 + $registered = openstation_files_upload_register(
283 317 $user_id,
284 318 $received,
285 319 (int) $req->get_param( 'parentId' ),
286 320 (string) $req->get_param( 'relativePath' ),
@@ -293,24 +327,58 @@
293 327 }
294 328 return $registered;
295 329 }
296 330
297 - $row = desktop_mode_files_get_placement( $registered['placement_id'] );
331 + $row = openstation_files_get_placement( $registered['placement_id'] );
298 332 return rest_ensure_response(
299 333 array(
300 - 'placement' => desktop_mode_files_shape_placement( $row ),
301 - 'storedFileId' => (int) $registered['file_id'],
334 + 'placement' => openstation_files_shape_placement( $row ),
335 + 'storedFileId' => (int) $registered['file_id'],
336 + 'createdFolders' => openstation_files_shape_created_folders( $registered['created_folders'] ),
302 337 )
303 338 );
304 339 }
305 340
306 341 /**
342 + * Shape the folders a request created mkdir-p style so the client
343 + * can paint their tiles the moment they exist. Each entry carries
344 + * the folder row AND its placement in the parent the client is
345 + * looking at — the per-file `placement` in the same response only
346 + * describes the file inside the leaf folder, which is why the
347 + * wallpaper tile used to wait for the end-of-batch resync (or the
348 + * next Heartbeat delta) after a folder-tree drop.
349 + *
350 + * @internal
351 + *
352 + * @param array $created `{ folder_id, placement_id }` rows from
353 + * `openstation_files_resolve_relative_path()`.
354 + * @return array<int,array{folder:array,placement:array}>
355 + */
356 +function openstation_files_shape_created_folders( $created ) {
357 + $out = array();
358 + foreach ( (array) $created as $entry ) {
359 + if ( empty( $entry['folder_id'] ) || empty( $entry['placement_id'] ) ) {
360 + continue;
361 + }
362 + $folder = openstation_files_get_folder( (int) $entry['folder_id'] );
363 + $placement = openstation_files_get_placement( (int) $entry['placement_id'] );
364 + if ( ! $folder || ! $placement ) {
365 + continue;
366 + }
367 + $out[] = array(
368 + 'folder' => openstation_files_shape_folder( $folder ),
369 + 'placement' => openstation_files_shape_placement( $placement ),
370 + );
371 + }
372 + return $out;
373 +}
374 +
375 +/**
307 376 * Receive step: validate and move the bytes into the owner's
308 377 * storage dir under a fresh UUID disk name. Returns
309 378 * `{ path, disk_name, display_name, size_bytes, mime }` or an
310 379 * error. No DB writes happen here.
311 380 *
312 - * @since 0.9.6
313 381 * @internal
314 382 *
315 383 * @param array $file Single `$_FILES`-shaped entry.
316 384 * @param int $user_id Uploader.
@@ -315,15 +383,15 @@
315 383 * @param array $file Single `$_FILES`-shaped entry.
316 384 * @param int $user_id Uploader.
317 385 * @return array|WP_Error
318 386 */
319 -function desktop_mode_files_upload_receive( $file, $user_id ) {
387 +function openstation_files_upload_receive( $file, $user_id ) {
320 388 $user_id = (int) $user_id;
321 389 $client_name = isset( $file['name'] ) ? (string) $file['name'] : '';
322 390
323 - if ( desktop_mode_stored_files_is_denied_filename( $client_name ) ) {
391 + if ( openstation_stored_files_is_denied_filename( $client_name ) ) {
324 392 return new WP_Error(
325 - 'desktop_mode_stored_files_forbidden_type',
393 + 'openstation_stored_files_forbidden_type',
326 394 __( 'This file type is not allowed.', 'desktop-mode' ),
327 395 array( 'status' => 400 )
328 396 );
329 397 }
@@ -328,12 +396,12 @@
328 396 );
329 397 }
330 398
331 399 $size = isset( $file['size'] ) ? (int) $file['size'] : 0;
332 - $max = desktop_mode_stored_files_max_upload_bytes( $user_id );
400 + $max = openstation_stored_files_max_upload_bytes( $user_id );
333 401 if ( $max > 0 && $size > $max ) {
334 402 return new WP_Error(
335 - 'desktop_mode_stored_files_too_large',
403 + 'openstation_stored_files_too_large',
336 404 sprintf(
337 405 /* translators: %s: formatted maximum file size. */
338 406 __( 'That file is larger than the allowed maximum of %s.', 'desktop-mode' ),
339 407 size_format( $max )
@@ -341,18 +409,18 @@
341 409 array( 'status' => 413 )
342 410 );
343 411 }
344 412
345 - $quota = desktop_mode_stored_files_user_quota_bytes( $user_id );
346 - if ( $quota > 0 && ( desktop_mode_stored_files_total_bytes( $user_id ) + $size ) > $quota ) {
413 + $quota = openstation_stored_files_user_quota_bytes( $user_id );
414 + if ( $quota > 0 && ( openstation_stored_files_total_bytes( $user_id ) + $size ) > $quota ) {
347 415 return new WP_Error(
348 - 'desktop_mode_stored_files_quota_exceeded',
416 + 'openstation_stored_files_quota_exceeded',
349 417 __( 'Your desktop storage is full.', 'desktop-mode' ),
350 418 array( 'status' => 403 )
351 419 );
352 420 }
353 421
354 - $dir = desktop_mode_stored_files_ensure_dir( $user_id );
422 + $dir = openstation_stored_files_ensure_dir( $user_id );
355 423 if ( is_wp_error( $dir ) ) {
356 424 return $dir;
357 425 }
358 426
@@ -364,15 +432,13 @@
364 432 * Additions here genuinely widen the policy (the scoped
365 433 * `upload_mimes` hook below keeps core's re-check in
366 434 * `wp_check_filetype_and_ext()` in agreement).
367 435 *
368 - * @since 0.9.6
369 - *
370 436 * @param array<string,string> $mimes Allowed map.
371 437 * @param int $user_id Uploader.
372 438 */
373 439 $mimes = (array) apply_filters(
374 - 'desktop_mode_stored_files_allowed_mimes',
440 + 'openstation_stored_files_allowed_mimes',
375 441 get_allowed_mime_types( $user_id ),
376 442 $user_id
377 443 );
378 444
@@ -383,13 +449,13 @@
383 449 $name_cb = static function ( $dir_unused, $name_unused, $ext_unused ) use ( $disk_name ) {
384 450 return $disk_name;
385 451 };
386 452 // Resolve the target paths BEFORE hooking `upload_dir` and close
387 - // over plain strings — `desktop_mode_stored_files_dir()` calls
453 + // over plain strings — `openstation_stored_files_dir()` calls
388 454 // `wp_get_upload_dir()`, which applies the `upload_dir` filter,
389 455 // so calling it from inside the closure would recurse infinitely.
390 - $target_base = desktop_mode_stored_files_dir();
391 - $target_dir = desktop_mode_stored_files_dir( $user_id );
456 + $target_base = openstation_stored_files_dir();
457 + $target_dir = openstation_stored_files_dir( $user_id );
392 458 $redirect = static function ( $dirs ) use ( $user_id, $target_base, $target_dir ) {
393 459 $dirs['subdir'] = '/' . $user_id;
394 460 $dirs['path'] = $target_dir;
395 461 $dirs['url'] = $dirs['baseurl'] . '/desktop-mode-files/' . $user_id;
@@ -403,15 +469,13 @@
403 469 * uploads. Exists mainly so tests (and future resumable layers
404 470 * feeding pre-staged files) can switch `action` to the sideload
405 471 * variant — never remove `test_form => false`.
406 472 *
407 - * @since 0.9.6
408 - *
409 473 * @param array $overrides Overrides array.
410 474 * @param int $user_id Uploader.
411 475 */
412 476 $overrides = (array) apply_filters(
413 - 'desktop_mode_stored_files_upload_overrides',
477 + 'openstation_stored_files_upload_overrides',
414 478 array(
415 479 'test_form' => false,
416 480 'mimes' => $mimes,
417 481 'unique_filename_callback' => $name_cb,
@@ -426,9 +490,9 @@
426 490 remove_filter( 'upload_dir', $redirect );
427 491
428 492 if ( isset( $result['error'] ) ) {
429 493 return new WP_Error(
430 - 'desktop_mode_stored_files_upload_failed',
494 + 'openstation_stored_files_upload_failed',
431 495 (string) $result['error'],
432 496 array( 'status' => 400 )
433 497 );
434 498 }
@@ -445,9 +509,8 @@
445 509
446 510 /**
447 511 * Register step: stored-file row + folder resolution + placement.
448 512 *
449 - * @since 0.9.6
450 513 * @internal
451 514 *
452 515 * @param int $user_id Uploader.
453 516 * @param array $received Return value of the receive step.
@@ -455,15 +518,20 @@
455 518 * @param string $relative_path Optional `a/b/c.ext` path; directory
456 519 * segments are resolved under `$parent_id`.
457 520 * @param array|null $coords `x`, `y` for the placement, or null
458 521 * to auto-place at the next free slot.
459 - * @return array|WP_Error `{ file_id, placement_id }`.
522 + * @return array|WP_Error `{ file_id, placement_id, created_folders }` —
523 + * `created_folders` lists the `{ folder_id,
524 + * placement_id }` pairs the relative path
525 + * created (empty for flat uploads and for
526 + * segments that already existed).
460 527 */
461 -function desktop_mode_files_upload_register( $user_id, $received, $parent_id, $relative_path = '', $coords = null ) {
528 +function openstation_files_upload_register( $user_id, $received, $parent_id, $relative_path = '', $coords = null ) {
462 529 $parent_id = max( 0, (int) $parent_id );
530 + $created = array();
463 531
464 532 if ( '' !== (string) $relative_path ) {
465 - $resolved = desktop_mode_files_resolve_relative_path( (int) $user_id, $parent_id, (string) $relative_path );
533 + $resolved = openstation_files_resolve_relative_path( (int) $user_id, $parent_id, (string) $relative_path, $created );
466 534 if ( is_wp_error( $resolved ) ) {
467 535 return $resolved;
468 536 }
469 537 $parent_id = $resolved;
@@ -468,9 +536,9 @@
468 536 }
469 537 $parent_id = $resolved;
470 538 }
471 539
472 - $file_id = desktop_mode_stored_files_create(
540 + $file_id = openstation_stored_files_create(
473 541 (int) $user_id,
474 542 array(
475 543 'display_name' => $received['display_name'],
476 544 'disk_name' => $received['disk_name'],
@@ -482,9 +550,9 @@
482 550 return $file_id;
483 551 }
484 552
485 553 if ( is_array( $coords ) && isset( $coords['x'], $coords['y'] ) ) {
486 - $placement_id = desktop_mode_files_place(
554 + $placement_id = openstation_files_place(
487 555 (int) $user_id,
488 556 $parent_id,
489 557 'upload',
490 558 (string) $file_id,
@@ -496,9 +564,9 @@
496 564 } else {
497 565 // No coords sent (folder-tree members, batch files after
498 566 // the first) — the server picks the next free grid slot so
499 567 // tiles never stack at the origin.
500 - $placement_id = desktop_mode_files_place_at_next_free_slot(
568 + $placement_id = openstation_files_place_at_next_free_slot(
501 569 (int) $user_id,
502 570 $parent_id,
503 571 'upload',
504 572 (string) $file_id
@@ -505,12 +573,12 @@
505 573 );
506 574 }
507 575 if ( is_wp_error( $placement_id ) ) {
508 576 // Roll back through the store primitive so the documented
509 - // `desktop_mode_stored_file_created` / `_deleted` action pair
577 + // `openstation_stored_file_created` / `_deleted` action pair
510 578 // stays balanced for subscribers (and the bytes go with the
511 579 // row — the caller's outer cleanup guard becomes a no-op).
512 - desktop_mode_stored_files_delete( (int) $file_id );
580 + openstation_stored_files_delete( (int) $file_id );
513 581 return $placement_id;
514 582 }
515 583
516 584 /**
@@ -516,19 +584,18 @@
516 584 /**
517 585 * Fires after an upload lands (bytes, row, and placement all
518 586 * exist).
519 587 *
520 - * @since 0.9.6
521 - *
522 588 * @param int $file_id Stored-file id.
523 589 * @param int $placement_id Placement id.
524 590 * @param int $user_id Uploader.
525 591 */
526 - do_action( 'desktop_mode_stored_file_uploaded', (int) $file_id, (int) $placement_id, (int) $user_id );
592 + do_action( 'openstation_stored_file_uploaded', (int) $file_id, (int) $placement_id, (int) $user_id );
527 593
528 594 return array(
529 - 'file_id' => (int) $file_id,
530 - 'placement_id' => (int) $placement_id,
595 + 'file_id' => (int) $file_id,
596 + 'placement_id' => (int) $placement_id,
597 + 'created_folders' => $created,
531 598 );
532 599 }
533 600
534 601 /**
@@ -539,18 +606,23 @@
539 606 * uploads of one tree share segments instead of racing).
540 607 *
541 608 * The final path segment is the FILE name and is ignored here.
542 609 *
543 - * @since 0.9.6
544 - *
545 610 * @param int $user_id Acting user.
546 611 * @param int $base_parent_id Folder to resolve under (0 = root).
547 612 * @param string $relative_path `a/b/c.ext` or `a/b/` (trailing
548 613 * slash = pure directory path, e.g.
549 614 * an empty folder from a drag).
615 + * @param array $created Optional, by reference. Receives one
616 + * `{ folder_id, placement_id }` entry per
617 + * folder this call created, outermost
618 + * first, so the caller can hand the new
619 + * tiles to the client in the same
620 + * response. Reused segments are not
621 + * listed.
550 622 * @return int|WP_Error Folder id to place the file in.
551 623 */
552 -function desktop_mode_files_resolve_relative_path( $user_id, $base_parent_id, $relative_path ) {
624 +function openstation_files_resolve_relative_path( $user_id, $base_parent_id, $relative_path, &$created = null ) {
553 625 global $wpdb;
554 626 $user_id = (int) $user_id;
555 627 $parent_id = max( 0, (int) $base_parent_id );
556 628 $path = str_replace( '\\', '/', (string) $relative_path );
@@ -555,9 +627,9 @@
555 627 $parent_id = max( 0, (int) $base_parent_id );
556 628 $path = str_replace( '\\', '/', (string) $relative_path );
557 629
558 630 if ( false !== strpos( $path, "\0" ) ) {
559 - return new WP_Error( 'desktop_mode_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
631 + return new WP_Error( 'openstation_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
560 632 }
561 633
562 634 $is_dir_path = '/' === substr( $path, -1 );
563 635 $segments = array_values( array_filter( explode( '/', $path ), 'strlen' ) );
@@ -567,19 +639,19 @@
567 639 if ( empty( $segments ) ) {
568 640 return $parent_id;
569 641 }
570 642 if ( count( $segments ) > 32 ) {
571 - return new WP_Error( 'desktop_mode_stored_files_path_too_deep', __( 'That folder tree is nested too deeply.', 'desktop-mode' ), array( 'status' => 400 ) );
643 + return new WP_Error( 'openstation_stored_files_path_too_deep', __( 'That folder tree is nested too deeply.', 'desktop-mode' ), array( 'status' => 400 ) );
572 644 }
573 645
574 - $tables = desktop_mode_files_table_names();
646 + $tables = openstation_files_table_names();
575 647 foreach ( $segments as $segment ) {
576 648 if ( '.' === $segment || '..' === $segment ) {
577 - return new WP_Error( 'desktop_mode_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
649 + return new WP_Error( 'openstation_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
578 650 }
579 651 $name = sanitize_file_name( wp_strip_all_tags( $segment ) );
580 652 if ( '' === $name ) {
581 - return new WP_Error( 'desktop_mode_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
653 + return new WP_Error( 'openstation_stored_files_bad_path', __( 'Invalid path.', 'desktop-mode' ), array( 'status' => 400 ) );
582 654 }
583 655
584 656 // Dedupe: a live folder of this name, placed in this parent,
585 657 // owned by the acting user.
@@ -606,16 +678,26 @@
606 678 $parent_id = (int) $existing;
607 679 continue;
608 680 }
609 681
610 - $folder_id = desktop_mode_files_create_folder( $user_id, array( 'name' => $name ) );
682 + $folder_id = openstation_files_create_folder( $user_id, array( 'name' => $name ) );
611 683 if ( is_wp_error( $folder_id ) ) {
612 684 return $folder_id;
613 685 }
614 - $placement = desktop_mode_files_place( $user_id, $parent_id, 'folder', (string) $folder_id );
686 + // Next free slot, same as the file placements below: a bare
687 + // `openstation_files_place()` pins the tile at 0,0, which the
688 + // layer only displaces client-side — the stored coordinates
689 + // stay wrong and the tile jumps on the next repaint.
690 + $placement = openstation_files_place_at_next_free_slot( $user_id, $parent_id, 'folder', (string) $folder_id );
615 691 if ( is_wp_error( $placement ) ) {
616 692 return $placement;
617 693 }
694 + if ( is_array( $created ) ) {
695 + $created[] = array(
696 + 'folder_id' => (int) $folder_id,
697 + 'placement_id' => (int) $placement,
698 + );
699 + }
618 700 $parent_id = (int) $folder_id;
619 701 }
620 702 return $parent_id;
621 703 }
@@ -623,20 +705,18 @@
623 705 /**
624 706 * Shell-config injection: what the client upload/download UX needs
625 707 * to know up front.
626 708 *
627 - * @since 0.9.6
628 - *
629 709 * @param array $config Shell config.
630 710 * @return array
631 711 */
632 -function desktop_mode_stored_files_inject_shell_config( $config ) {
633 - $user_id = get_current_user_id();
712 +function openstation_stored_files_inject_shell_config( $config ) {
713 + $user_id = get_current_user_id();
634 714 $config['desktopStorage'] = array(
635 - 'canUpload' => $user_id > 0 && current_user_can( desktop_mode_stored_files_upload_capability() ),
636 - 'maxBytes' => desktop_mode_stored_files_max_upload_bytes( $user_id ),
637 - 'quotaBytes' => desktop_mode_stored_files_user_quota_bytes( $user_id ),
715 + 'canUpload' => $user_id > 0 && current_user_can( openstation_stored_files_upload_capability() ),
716 + 'maxBytes' => openstation_stored_files_max_upload_bytes( $user_id ),
717 + 'quotaBytes' => openstation_stored_files_user_quota_bytes( $user_id ),
638 718 'zipAvailable' => class_exists( 'ZipArchive' ),
639 719 );
640 720 return $config;
641 721 }
642 -add_filter( 'desktop_mode_shell_config', 'desktop_mode_stored_files_inject_shell_config', 20 );
722 +add_filter( 'openstation_shell_config', 'openstation_stored_files_inject_shell_config', 20 );