| @@ -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 ); | |