| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — Files placement store. | |
| 3 | + * OpenStation — Files placement store. | |
| 4 | 4 | * |
| 5 | 5 | * CRUD primitives for the `_desktop_mode_file_placements` table. |
| 6 | - * Every read goes through `desktop_mode_files_query_args` so | |
| 6 | + * Every read goes through `openstation_files_query_args` so | |
| 7 | 7 | * plugins can scope what's visible (mirror of the recycle-bin's |
| 8 | 8 | * filter pattern). Every write fires before / after actions so |
| 9 | 9 | * other plugins can react and so Phase 6's Heartbeat sync has a |
| 10 | 10 | * single subscription point. |
| @@ -10,18 +10,17 @@ | ||
| 10 | 10 | * single subscription point. |
| 11 | 11 | * |
| 12 | 12 | * Capability gate is per-call: callers pass the `$user_id` they |
| 13 | 13 | * intend to act for; the function consults |
| 14 | - * `desktop_mode_files_can_place` (filter) and the file's | |
| 15 | - * `Desktop_Mode_File::can_read()` before writing. | |
| 14 | + * `openstation_files_can_place` (filter) and the file's | |
| 15 | + * `OpenStation_File::can_read()` before writing. | |
| 16 | 16 | * |
| 17 | 17 | * Tombstones are written only for permanent removals (hard |
| 18 | 18 | * deletes); soft-trash and moves are surfaced to clients via |
| 19 | 19 | * `updated_at_ms` / `trashed_at_ms` in the Heartbeat delta — see |
| 20 | - * desktop_mode_files_write_tombstone() for the invariant. | |
| 20 | + * openstation_files_write_tombstone() for the invariant. | |
| 21 | 21 | * |
| 22 | - * @package WPDesktopMode | |
| 23 | - * @since 0.9.0 | |
| 22 | + * @package OpenStation | |
| 24 | 23 | */ |
| 25 | 24 | |
| 26 | 25 | defined( 'ABSPATH' ) || exit; |
| 27 | 26 | |
| @@ -27,10 +26,8 @@ | ||
| 27 | 26 | |
| 28 | 27 | /** |
| 29 | 28 | * Insert a placement. |
| 30 | 29 | * |
| 31 | - * @since 0.9.0 | |
| 32 | - * | |
| 33 | 30 | * @param int $user_id Owner of the placement (the user the |
| 34 | 31 | * tile lives on). |
| 35 | 32 | * @param int $parent_id Folder id, or 0 for the desktop root. |
| 36 | 33 | * @param string $type File-type slug. |
| @@ -37,9 +34,9 @@ | ||
| 37 | 34 | * @param string $ref Entity reference. |
| 38 | 35 | * @param array $args Optional. `x`, `y`, `sort_order`, `meta`. |
| 39 | 36 | * @return int|WP_Error Placement id on success, `WP_Error` otherwise. |
| 40 | 37 | */ |
| 41 | -function desktop_mode_files_place( $user_id, $parent_id, $type, $ref, $args = array() ) { | |
| 38 | +function openstation_files_place( $user_id, $parent_id, $type, $ref, $args = array() ) { | |
| 42 | 39 | global $wpdb; |
| 43 | 40 | |
| 44 | 41 | $user_id = (int) $user_id; |
| 45 | 42 | $parent_id = (int) $parent_id; |
| @@ -46,13 +43,13 @@ | ||
| 46 | 43 | $type = (string) $type; |
| 47 | 44 | $ref = (string) $ref; |
| 48 | 45 | |
| 49 | 46 | if ( $user_id <= 0 ) { |
| 50 | - return new WP_Error( 'desktop_mode_files_invalid_user', __( 'A user id is required.', 'desktop-mode' ), array( 'status' => 400 ) ); | |
| 47 | + return new WP_Error( 'openstation_files_invalid_user', __( 'A user id is required.', 'desktop-mode' ), array( 'status' => 400 ) ); | |
| 51 | 48 | } |
| 52 | - $entry = desktop_mode_get_file_type( $type ); | |
| 49 | + $entry = openstation_get_file_type( $type ); | |
| 53 | 50 | if ( ! $entry ) { |
| 54 | - return new WP_Error( 'desktop_mode_files_unknown_type', __( 'Unknown file type.', 'desktop-mode' ), array( 'status' => 400 ) ); | |
| 51 | + return new WP_Error( 'openstation_files_unknown_type', __( 'Unknown file type.', 'desktop-mode' ), array( 'status' => 400 ) ); | |
| 55 | 52 | } |
| 56 | 53 | |
| 57 | 54 | /** |
| 58 | 55 | * Gate placement creation. Defaults to allowing the user to |
| @@ -58,20 +55,18 @@ | ||
| 58 | 55 | * Gate placement creation. Defaults to allowing the user to |
| 59 | 56 | * place any type they can read; plugins use this to enforce |
| 60 | 57 | * stricter rules (e.g. only admins may place users). |
| 61 | 58 | * |
| 62 | - * @since 0.9.0 | |
| 63 | - * | |
| 64 | 59 | * @param bool $can Default: file's `can_read( $user_id )`. |
| 65 | 60 | * @param int $user_id Owner. |
| 66 | 61 | * @param string $type File-type slug. |
| 67 | 62 | * @param string $ref Entity reference. |
| 68 | 63 | */ |
| 69 | - $file = desktop_mode_resolve_file( $type, $ref ); | |
| 64 | + $file = openstation_resolve_file( $type, $ref ); | |
| 70 | 65 | $can = $file ? $file->can_read( $user_id ) : false; |
| 71 | - $can = (bool) apply_filters( 'desktop_mode_files_can_place', $can, $user_id, $type, $ref ); | |
| 66 | + $can = (bool) apply_filters( 'openstation_files_can_place', $can, $user_id, $type, $ref ); | |
| 72 | 67 | if ( ! $can ) { |
| 73 | - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You are not allowed to place this file.', 'desktop-mode' ), array( 'status' => 403 ) ); | |
| 68 | + return new WP_Error( 'openstation_files_forbidden', __( 'You are not allowed to place this file.', 'desktop-mode' ), array( 'status' => 403 ) ); | |
| 74 | 69 | } |
| 75 | 70 | |
| 76 | 71 | // Write-gate: placing INTO a non-owned folder requires the |
| 77 | 72 | // folder's `write` cap. Owner / desktop-root placements are |
| @@ -76,16 +71,16 @@ | ||
| 76 | 71 | // Write-gate: placing INTO a non-owned folder requires the |
| 77 | 72 | // folder's `write` cap. Owner / desktop-root placements are |
| 78 | 73 | // always allowed. |
| 79 | 74 | if ( (int) $parent_id > 0 ) { |
| 80 | - $target_folder = desktop_mode_files_get_folder( (int) $parent_id ); | |
| 75 | + $target_folder = openstation_files_get_folder( (int) $parent_id ); | |
| 81 | 76 | if ( $target_folder && (int) $target_folder['owner_id'] !== $user_id ) { |
| 82 | - $cap = function_exists( 'desktop_mode_folder_share_user_capability' ) | |
| 83 | - ? desktop_mode_folder_share_user_capability( (int) $parent_id, $user_id ) | |
| 77 | + $cap = function_exists( 'openstation_folder_share_user_capability' ) | |
| 78 | + ? openstation_folder_share_user_capability( (int) $parent_id, $user_id ) | |
| 84 | 79 | : 'none'; |
| 85 | 80 | if ( 'write' !== $cap ) { |
| 86 | 81 | return new WP_Error( |
| 87 | - 'desktop_mode_files_no_write_in_shared_folder', | |
| 82 | + 'openstation_files_no_write_in_shared_folder', | |
| 88 | 83 | __( 'You only have read access to that folder.', 'desktop-mode' ), |
| 89 | 84 | array( 'status' => 403 ) |
| 90 | 85 | ); |
| 91 | 86 | } |
| @@ -101,10 +96,10 @@ | ||
| 101 | 96 | 'meta' => null, |
| 102 | 97 | ) |
| 103 | 98 | ); |
| 104 | 99 | |
| 105 | - $tables = desktop_mode_files_table_names(); | |
| 106 | - $now = desktop_mode_files_now_ms(); | |
| 100 | + $tables = openstation_files_table_names(); | |
| 101 | + $now = openstation_files_now_ms(); | |
| 107 | 102 | $row = array( |
| 108 | 103 | 'owner_id' => $user_id, |
| 109 | 104 | 'updated_by' => $user_id, |
| 110 | 105 | 'parent_id' => max( 0, $parent_id ), |
| @@ -123,21 +118,30 @@ | ||
| 123 | 118 | // `<div class="wpdberror">…</div>` to the REST response body and |
| 124 | 119 | // break `await response.json()` on the client. `$wpdb->last_error` |
| 125 | 120 | // still holds the message, so genuine DB failures surface via the |
| 126 | 121 | // `WP_Error` we return when no existing row is found. |
| 127 | - $prev_suppress = $wpdb->suppress_errors( true ); | |
| 128 | - $ok = $wpdb->insert( $tables['placements'], $row, array( '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ) ); | |
| 129 | - $wpdb->suppress_errors( $prev_suppress ); | |
| 122 | + $insert = static function () use ( $tables, $row ) { | |
| 123 | + global $wpdb; | |
| 124 | + $prev_suppress = $wpdb->suppress_errors( true ); | |
| 125 | + $ok = $wpdb->insert( $tables['placements'], $row, array( '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ) ); | |
| 126 | + $wpdb->suppress_errors( $prev_suppress ); | |
| 127 | + return array( $ok, (int) $wpdb->insert_id ); | |
| 128 | + }; | |
| 129 | + $result = 'upload' === $type ? openstation_stored_files_place_insert( $ref, $insert ) : $insert(); | |
| 130 | + if ( is_wp_error( $result ) ) { | |
| 131 | + return $result; | |
| 132 | + } | |
| 133 | + list( $ok, $id ) = $result; | |
| 130 | 134 | if ( false === $ok ) { |
| 131 | 135 | // Disambiguate the two cases hidden behind a generic `false`: |
| 132 | - // (a) The `placement_unique` index (since 0.8.0) collided | |
| 133 | - // with an existing row for this (user, parent, type, | |
| 134 | - // ref). The collider may be active (the orphan placer | |
| 135 | - // won a race against this caller, or a stale duplicate | |
| 136 | - // client request) or soft-trashed (the user removed a | |
| 137 | - // link tile and is now recreating the same URL). | |
| 138 | - // (b) Any other DB failure — connection, deadlock, bad | |
| 139 | - // column. The error must surface to the caller as-is. | |
| 136 | + // (a) The `placement_unique` index collided | |
| 137 | + // with an existing row for this (user, parent, type, | |
| 138 | + // ref). The collider may be active (the orphan placer | |
| 139 | + // won a race against this caller, or a stale duplicate | |
| 140 | + // client request) or soft-trashed (the user removed a | |
| 141 | + // link tile and is now recreating the same URL). | |
| 142 | + // (b) Any other DB failure — connection, deadlock, bad | |
| 143 | + // column. The error must surface to the caller as-is. | |
| 140 | 144 | // We treat (a) idempotently: restore if trashed, then apply |
| 141 | 145 | // the caller's coords / meta so the new placement lands |
| 142 | 146 | // where the user clicked. Reported as #167. |
| 143 | 147 | $existing = $wpdb->get_row( |
| @@ -155,15 +159,15 @@ | ||
| 155 | 159 | ), |
| 156 | 160 | ARRAY_A |
| 157 | 161 | ); |
| 158 | 162 | if ( ! $existing ) { |
| 159 | - return new WP_Error( 'desktop_mode_files_insert_failed', __( 'Failed to write placement.', 'desktop-mode' ), array( 'status' => 500 ) ); | |
| 163 | + return new WP_Error( 'openstation_files_insert_failed', __( 'Failed to write placement.', 'desktop-mode' ), array( 'status' => 500 ) ); | |
| 160 | 164 | } |
| 161 | 165 | |
| 162 | 166 | $existing_id = (int) $existing['id']; |
| 163 | 167 | |
| 164 | 168 | if ( ! empty( $existing['trashed_at_ms'] ) ) { |
| 165 | - $restore = desktop_mode_files_restore_placement( $user_id, $existing_id ); | |
| 169 | + $restore = openstation_files_restore_placement( $user_id, $existing_id ); | |
| 166 | 170 | if ( is_wp_error( $restore ) ) { |
| 167 | 171 | return $restore; |
| 168 | 172 | } |
| 169 | 173 | } |
| @@ -173,11 +177,11 @@ | ||
| 173 | 177 | // stale tombstones for this id should be cleared so a fresh |
| 174 | 178 | // heartbeat tick can't surface "alive + removed" together. |
| 175 | 179 | // `restore_placement` already clears its own tombstones, so |
| 176 | 180 | // this is a no-op in the soft-trashed branch above. |
| 177 | - desktop_mode_files_clear_tombstones_for( 'placement', $existing_id ); | |
| 181 | + openstation_files_clear_tombstones_for( 'placement', $existing_id ); | |
| 178 | 182 | |
| 179 | - $move = desktop_mode_files_move( | |
| 183 | + $move = openstation_files_move( | |
| 180 | 184 | $existing_id, |
| 181 | 185 | $user_id, |
| 182 | 186 | array( |
| 183 | 187 | 'parent_id' => max( 0, $parent_id ), |
| @@ -192,21 +196,17 @@ | ||
| 192 | 196 | } |
| 193 | 197 | |
| 194 | 198 | return $existing_id; |
| 195 | 199 | } |
| 196 | - $id = (int) $wpdb->insert_id; | |
| 197 | - | |
| 198 | 200 | $row['id'] = $id; |
| 199 | 201 | |
| 200 | 202 | /** |
| 201 | 203 | * Fires after a placement is created. |
| 202 | 204 | * |
| 203 | - * @since 0.9.0 | |
| 204 | - * | |
| 205 | 205 | * @param int $id Placement id. |
| 206 | 206 | * @param array $row Inserted row. |
| 207 | 207 | */ |
| 208 | - do_action( 'desktop_mode_file_placed', $id, $row ); | |
| 208 | + do_action( 'openstation_file_placed', $id, $row ); | |
| 209 | 209 | |
| 210 | 210 | return $id; |
| 211 | 211 | } |
| 212 | 212 | |
| @@ -216,29 +216,36 @@ | ||
| 216 | 216 | * the same as omitting the key; for `meta`, an explicit |
| 217 | 217 | * `meta => null` CLEARS the column (keyed on array_key_exists) — |
| 218 | 218 | * omit the key to preserve it. |
| 219 | 219 | * |
| 220 | - * @since 0.9.0 | |
| 221 | - * | |
| 222 | 220 | * @param int $placement_id Placement id. |
| 223 | 221 | * @param int $user_id Acting user (for capability gate). |
| 224 | 222 | * @param array $changes `parent_id`, `x`, `y`, `sort_order`, `meta`. |
| 225 | 223 | * @return true|WP_Error |
| 226 | 224 | */ |
| 227 | -function desktop_mode_files_move( $placement_id, $user_id, $changes = array() ) { | |
| 225 | +function openstation_files_move( $placement_id, $user_id, $changes = array() ) { | |
| 228 | 226 | global $wpdb; |
| 229 | 227 | |
| 230 | 228 | $placement_id = (int) $placement_id; |
| 231 | 229 | $user_id = (int) $user_id; |
| 232 | 230 | if ( $placement_id <= 0 || $user_id <= 0 ) { |
| 233 | - return new WP_Error( 'desktop_mode_files_bad_request', __( 'Invalid arguments.', 'desktop-mode' ), array( 'status' => 400 ) ); | |
| 231 | + return new WP_Error( 'openstation_files_bad_request', __( 'Invalid arguments.', 'desktop-mode' ), array( 'status' => 400 ) ); | |
| 234 | 232 | } |
| 235 | 233 | |
| 236 | - $row = desktop_mode_files_get_placement( $placement_id ); | |
| 234 | + $row = openstation_files_get_placement( $placement_id ); | |
| 237 | 235 | if ( ! $row ) { |
| 238 | - return new WP_Error( 'desktop_mode_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) ); | |
| 236 | + return new WP_Error( 'openstation_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) ); | |
| 239 | 237 | } |
| 240 | 238 | |
| 239 | + // Owner-lock for `upload` placements: only the stored file's | |
| 240 | + // owner may move them — folder-share write capability does NOT | |
| 241 | + // extend to uploaded files (recipients are read + download | |
| 242 | + // only; see stored-files-store.php). | |
| 243 | + $upload_lock = openstation_files_upload_owner_lock( $row, $user_id ); | |
| 244 | + if ( is_wp_error( $upload_lock ) ) { | |
| 245 | + return $upload_lock; | |
| 246 | + } | |
| 247 | + | |
| 241 | 248 | // Permission check. Owner of the row is always allowed. For |
| 242 | 249 | // rows inside a shared folder, the FOLDER's write cap is the |
| 243 | 250 | // gate — anyone with write on the folder can move/rearrange |
| 244 | 251 | // every icon in it, regardless of which user originally placed |
| @@ -244,17 +251,17 @@ | ||
| 244 | 251 | // every icon in it, regardless of which user originally placed |
| 245 | 252 | // the row (shared-namespace semantics). |
| 246 | 253 | $is_row_owner = (int) $row['owner_id'] === $user_id; |
| 247 | 254 | if ( (int) $row['parent_id'] > 0 ) { |
| 248 | - $source_folder = desktop_mode_files_get_folder( (int) $row['parent_id'] ); | |
| 255 | + $source_folder = openstation_files_get_folder( (int) $row['parent_id'] ); | |
| 249 | 256 | if ( $source_folder ) { |
| 250 | 257 | $is_folder_owner = (int) $source_folder['owner_id'] === $user_id; |
| 251 | - $source_cap = function_exists( 'desktop_mode_folder_share_user_capability' ) | |
| 252 | - ? desktop_mode_folder_share_user_capability( (int) $row['parent_id'], $user_id ) | |
| 258 | + $source_cap = function_exists( 'openstation_folder_share_user_capability' ) | |
| 259 | + ? openstation_folder_share_user_capability( (int) $row['parent_id'], $user_id ) | |
| 253 | 260 | : 'none'; |
| 254 | 261 | if ( ! $is_row_owner && ! $is_folder_owner && 'write' !== $source_cap ) { |
| 255 | 262 | return new WP_Error( |
| 256 | - 'desktop_mode_files_no_write_in_shared_folder', | |
| 263 | + 'openstation_files_no_write_in_shared_folder', | |
| 257 | 264 | __( 'You only have read access to this folder.', 'desktop-mode' ), |
| 258 | 265 | array( 'status' => 403 ) |
| 259 | 266 | ); |
| 260 | 267 | } |
| @@ -260,9 +267,9 @@ | ||
| 260 | 267 | } |
| 261 | 268 | // Folder reader on their own row inside the folder — still no. |
| 262 | 269 | if ( $is_row_owner && ! $is_folder_owner && 'write' !== $source_cap ) { |
| 263 | 270 | return new WP_Error( |
| 264 | - 'desktop_mode_files_no_write_in_shared_folder', | |
| 271 | + 'openstation_files_no_write_in_shared_folder', | |
| 265 | 272 | __( 'You only have read access to this folder.', 'desktop-mode' ), |
| 266 | 273 | array( 'status' => 403 ) |
| 267 | 274 | ); |
| 268 | 275 | } |
| @@ -268,21 +275,21 @@ | ||
| 268 | 275 | } |
| 269 | 276 | } |
| 270 | 277 | } elseif ( ! $is_row_owner ) { |
| 271 | 278 | // Row at root, viewer doesn't own it. |
| 272 | - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot edit this placement.', 'desktop-mode' ), array( 'status' => 403 ) ); | |
| 279 | + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot edit this placement.', 'desktop-mode' ), array( 'status' => 403 ) ); | |
| 273 | 280 | } |
| 274 | 281 | if ( isset( $changes['parent_id'] ) ) { |
| 275 | 282 | $target_parent = max( 0, (int) $changes['parent_id'] ); |
| 276 | 283 | if ( $target_parent > 0 ) { |
| 277 | - $target = desktop_mode_files_get_folder( $target_parent ); | |
| 284 | + $target = openstation_files_get_folder( $target_parent ); | |
| 278 | 285 | if ( $target && (int) $target['owner_id'] !== $user_id ) { |
| 279 | - $cap = function_exists( 'desktop_mode_folder_share_user_capability' ) | |
| 280 | - ? desktop_mode_folder_share_user_capability( $target_parent, $user_id ) | |
| 286 | + $cap = function_exists( 'openstation_folder_share_user_capability' ) | |
| 287 | + ? openstation_folder_share_user_capability( $target_parent, $user_id ) | |
| 281 | 288 | : 'none'; |
| 282 | 289 | if ( 'write' !== $cap ) { |
| 283 | 290 | return new WP_Error( |
| 284 | - 'desktop_mode_files_no_write_in_shared_folder', | |
| 291 | + 'openstation_files_no_write_in_shared_folder', | |
| 285 | 292 | __( 'You only have read access to that folder.', 'desktop-mode' ), |
| 286 | 293 | array( 'status' => 403 ) |
| 287 | 294 | ); |
| 288 | 295 | } |
| @@ -299,9 +306,9 @@ | ||
| 299 | 306 | if ( 'folder' === (string) $row['file_type'] && $target_parent > 0 ) { |
| 300 | 307 | $moving_folder_id = (int) $row['file_ref']; |
| 301 | 308 | if ( $moving_folder_id > 0 ) { |
| 302 | 309 | if ( |
| 303 | - desktop_mode_files_would_create_folder_cycle( | |
| 310 | + openstation_files_would_create_folder_cycle( | |
| 304 | 311 | $user_id, |
| 305 | 312 | $moving_folder_id, |
| 306 | 313 | $target_parent |
| 307 | 314 | ) |
| @@ -306,9 +313,9 @@ | ||
| 306 | 313 | $target_parent |
| 307 | 314 | ) |
| 308 | 315 | ) { |
| 309 | 316 | return new WP_Error( |
| 310 | - 'desktop_mode_files_folder_cycle', | |
| 317 | + 'openstation_files_folder_cycle', | |
| 311 | 318 | __( 'A folder cannot be placed inside itself or one of its descendants.', 'desktop-mode' ), |
| 312 | 319 | array( 'status' => 409 ) |
| 313 | 320 | ); |
| 314 | 321 | } |
| @@ -315,9 +322,9 @@ | ||
| 315 | 322 | } |
| 316 | 323 | } |
| 317 | 324 | } |
| 318 | 325 | |
| 319 | - $tables = desktop_mode_files_table_names(); | |
| 326 | + $tables = openstation_files_table_names(); | |
| 320 | 327 | $set = array(); |
| 321 | 328 | $fmt = array(); |
| 322 | 329 | |
| 323 | 330 | if ( isset( $changes['parent_id'] ) ) { |
| @@ -337,33 +344,31 @@ | ||
| 337 | 344 | if ( empty( $set ) ) { |
| 338 | 345 | return true; // No-op. |
| 339 | 346 | } |
| 340 | 347 | |
| 341 | - $set['updated_at_ms'] = desktop_mode_files_now_ms(); | |
| 348 | + $set['updated_at_ms'] = openstation_files_now_ms(); | |
| 342 | 349 | $fmt[] = '%d'; |
| 343 | 350 | // Track who actually fired this mutation so a future |
| 344 | 351 | // `If-Match` 409 can name the session that won the race, |
| 345 | 352 | // not just whoever happens to own the row. |
| 346 | - $set['updated_by'] = $user_id; | |
| 347 | - $fmt[] = '%d'; | |
| 353 | + $set['updated_by'] = $user_id; | |
| 354 | + $fmt[] = '%d'; | |
| 348 | 355 | |
| 349 | 356 | $ok = $wpdb->update( $tables['placements'], $set, array( 'id' => $placement_id ), $fmt, array( '%d' ) ); |
| 350 | 357 | if ( false === $ok ) { |
| 351 | - return new WP_Error( 'desktop_mode_files_update_failed', __( 'Failed to update placement.', 'desktop-mode' ), array( 'status' => 500 ) ); | |
| 358 | + return new WP_Error( 'openstation_files_update_failed', __( 'Failed to update placement.', 'desktop-mode' ), array( 'status' => 500 ) ); | |
| 352 | 359 | } |
| 353 | 360 | |
| 354 | - $next = desktop_mode_files_get_placement( $placement_id ); | |
| 361 | + $next = openstation_files_get_placement( $placement_id ); | |
| 355 | 362 | |
| 356 | 363 | /** |
| 357 | 364 | * Fires after a placement is moved / mutated. |
| 358 | 365 | * |
| 359 | - * @since 0.9.0 | |
| 360 | - * | |
| 361 | 366 | * @param int $id Placement id. |
| 362 | 367 | * @param array $next Row after the change. |
| 363 | 368 | * @param array $prev Row before the change. |
| 364 | 369 | */ |
| 365 | - do_action( 'desktop_mode_file_moved', $placement_id, $next, $row ); | |
| 370 | + do_action( 'openstation_file_moved', $placement_id, $next, $row ); | |
| 366 | 371 | |
| 367 | 372 | return true; |
| 368 | 373 | } |
| 369 | 374 | |
| @@ -369,69 +374,104 @@ | ||
| 369 | 374 | |
| 370 | 375 | /** |
| 371 | 376 | * Remove a placement. Writes a tombstone for Phase-6 sync. |
| 372 | 377 | * |
| 373 | - * @since 0.9.0 | |
| 374 | - * | |
| 375 | 378 | * @param int $placement_id Placement id. |
| 376 | 379 | * @param int $user_id Acting user. |
| 377 | 380 | * @return true|WP_Error |
| 378 | 381 | */ |
| 379 | -function desktop_mode_files_remove( $placement_id, $user_id ) { | |
| 382 | +function openstation_files_remove( $placement_id, $user_id ) { | |
| 380 | 383 | global $wpdb; |
| 381 | 384 | |
| 382 | 385 | $placement_id = (int) $placement_id; |
| 383 | 386 | $user_id = (int) $user_id; |
| 384 | - $row = desktop_mode_files_get_placement( $placement_id ); | |
| 387 | + $row = openstation_files_get_placement( $placement_id ); | |
| 385 | 388 | if ( ! $row ) { |
| 386 | - return new WP_Error( 'desktop_mode_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) ); | |
| 389 | + return new WP_Error( 'openstation_files_not_found', __( 'Placement not found.', 'desktop-mode' ), array( 'status' => 404 ) ); | |
| 387 | 390 | } |
| 391 | + // Owner-lock for `upload` placements — removal is destructive | |
| 392 | + // for real bytes, so only the stored file's owner may do it. | |
| 393 | + $upload_lock = openstation_files_upload_owner_lock( $row, $user_id ); | |
| 394 | + if ( is_wp_error( $upload_lock ) ) { | |
| 395 | + return $upload_lock; | |
| 396 | + } | |
| 388 | 397 | // Same shared-namespace rule as the trash gate: owner of the |
| 389 | 398 | // row OR write cap on the parent folder. |
| 390 | 399 | $is_row_owner = (int) $row['owner_id'] === $user_id; |
| 391 | 400 | $allowed = $is_row_owner; |
| 392 | 401 | if ( ! $allowed && (int) $row['parent_id'] > 0 ) { |
| 393 | - $cap = function_exists( 'desktop_mode_folder_share_user_capability' ) | |
| 394 | - ? desktop_mode_folder_share_user_capability( (int) $row['parent_id'], $user_id ) | |
| 402 | + $cap = function_exists( 'openstation_folder_share_user_capability' ) | |
| 403 | + ? openstation_folder_share_user_capability( (int) $row['parent_id'], $user_id ) | |
| 395 | 404 | : 'none'; |
| 396 | 405 | $allowed = 'write' === $cap; |
| 397 | 406 | } |
| 398 | 407 | if ( ! $allowed ) { |
| 399 | - return new WP_Error( 'desktop_mode_files_forbidden', __( 'You cannot remove this placement.', 'desktop-mode' ), array( 'status' => 403 ) ); | |
| 408 | + return new WP_Error( 'openstation_files_forbidden', __( 'You cannot remove this placement.', 'desktop-mode' ), array( 'status' => 403 ) ); | |
| 400 | 409 | } |
| 401 | 410 | |
| 402 | - $tables = desktop_mode_files_table_names(); | |
| 411 | + $tables = openstation_files_table_names(); | |
| 403 | 412 | $ok = $wpdb->delete( $tables['placements'], array( 'id' => $placement_id ), array( '%d' ) ); |
| 404 | 413 | if ( false === $ok ) { |
| 405 | - return new WP_Error( 'desktop_mode_files_delete_failed', __( 'Failed to remove placement.', 'desktop-mode' ), array( 'status' => 500 ) ); | |
| 414 | + return new WP_Error( 'openstation_files_delete_failed', __( 'Failed to remove placement.', 'desktop-mode' ), array( 'status' => 500 ) ); | |
| 406 | 415 | } |
| 407 | 416 | |
| 408 | - desktop_mode_files_write_tombstone( 'placement', $placement_id ); | |
| 417 | + openstation_files_write_tombstone( 'placement', $placement_id ); | |
| 409 | 418 | |
| 410 | 419 | /** |
| 411 | 420 | * Fires after a placement is removed. |
| 412 | 421 | * |
| 413 | - * @since 0.9.0 | |
| 414 | - * | |
| 415 | 422 | * @param int $id Placement id. |
| 416 | 423 | * @param array $row Removed row. |
| 417 | 424 | */ |
| 418 | - do_action( 'desktop_mode_file_unplaced', $placement_id, $row ); | |
| 425 | + do_action( 'openstation_file_unplaced', $placement_id, $row ); | |
| 419 | 426 | |
| 420 | 427 | return true; |
| 421 | 428 | } |
| 422 | 429 | |
| 423 | 430 | /** |
| 431 | + * Owner-lock gate for `upload` placements. Returns a `WP_Error` | |
| 432 | + * when `$user_id` is NOT the underlying stored file's owner — | |
| 433 | + * uploaded files are immutable to everyone else, including folder | |
| 434 | + * write-collaborators (the deliberate divergence from the shared- | |
| 435 | + * namespace rule; recipients are read + download only). Returns | |
| 436 | + * `true` for every other file type, and falls back to the normal | |
| 437 | + * rules when the stored-file row is gone (dangling tiles must stay | |
| 438 | + * cleanable). | |
| 439 | + * | |
| 440 | + * @param array $row Placement row. | |
| 441 | + * @param int $user_id Acting user. | |
| 442 | + * @return true|WP_Error | |
| 443 | + */ | |
| 444 | +function openstation_files_upload_owner_lock( $row, $user_id ) { | |
| 445 | + if ( ! is_array( $row ) || 'upload' !== (string) ( $row['file_type'] ?? '' ) ) { | |
| 446 | + return true; | |
| 447 | + } | |
| 448 | + if ( ! function_exists( 'openstation_stored_files_get' ) ) { | |
| 449 | + return true; | |
| 450 | + } | |
| 451 | + $stored = openstation_stored_files_get( (int) $row['file_ref'] ); | |
| 452 | + if ( ! $stored ) { | |
| 453 | + return true; | |
| 454 | + } | |
| 455 | + if ( (int) $stored['owner_id'] === (int) $user_id ) { | |
| 456 | + return true; | |
| 457 | + } | |
| 458 | + return new WP_Error( | |
| 459 | + 'openstation_files_upload_owner_locked', | |
| 460 | + __( 'Only the file’s owner can move or delete an uploaded file.', 'desktop-mode' ), | |
| 461 | + array( 'status' => 403 ) | |
| 462 | + ); | |
| 463 | +} | |
| 464 | + | |
| 465 | +/** | |
| 424 | 466 | * Read a single placement row by id. |
| 425 | 467 | * |
| 426 | - * @since 0.9.0 | |
| 427 | - * | |
| 428 | 468 | * @param int $placement_id Placement id. |
| 429 | 469 | * @return array|null |
| 430 | 470 | */ |
| 431 | -function desktop_mode_files_get_placement( $placement_id ) { | |
| 471 | +function openstation_files_get_placement( $placement_id ) { | |
| 432 | 472 | global $wpdb; |
| 433 | - $tables = desktop_mode_files_table_names(); | |
| 473 | + $tables = openstation_files_table_names(); | |
| 434 | 474 | $row = $wpdb->get_row( |
| 435 | 475 | $wpdb->prepare( "SELECT * FROM {$tables['placements']} WHERE id = %d", (int) $placement_id ), |
| 436 | 476 | ARRAY_A |
| 437 | 477 | ); |
| @@ -437,23 +477,21 @@ | ||
| 437 | 477 | ); |
| 438 | 478 | if ( ! $row ) { |
| 439 | 479 | return null; |
| 440 | 480 | } |
| 441 | - return desktop_mode_files_normalize_placement_row( $row ); | |
| 481 | + return openstation_files_normalize_placement_row( $row ); | |
| 442 | 482 | } |
| 443 | 483 | |
| 444 | 484 | /** |
| 445 | 485 | * List placements for a user under a given folder (0 = desktop |
| 446 | - * root). Honors the `desktop_mode_files_query_args` filter and | |
| 486 | + * root). Honors the `openstation_files_query_args` filter and | |
| 447 | 487 | * applies the file-type's `can_read()` per row. |
| 448 | 488 | * |
| 449 | - * @since 0.9.0 | |
| 450 | - * | |
| 451 | 489 | * @param int $user_id Viewer. |
| 452 | 490 | * @param int $parent_id Folder id (0 for desktop root). |
| 453 | 491 | * @return array[] |
| 454 | 492 | */ |
| 455 | -function desktop_mode_files_get_for_user_folder( $user_id, $parent_id = 0 ) { | |
| 493 | +function openstation_files_get_for_user_folder( $user_id, $parent_id = 0 ) { | |
| 456 | 494 | global $wpdb; |
| 457 | 495 | $user_id = (int) $user_id; |
| 458 | 496 | $parent_id = max( 0, (int) $parent_id ); |
| 459 | 497 | if ( $user_id <= 0 ) { |
| @@ -459,9 +497,9 @@ | ||
| 459 | 497 | if ( $user_id <= 0 ) { |
| 460 | 498 | return array(); |
| 461 | 499 | } |
| 462 | 500 | |
| 463 | - $tables = desktop_mode_files_table_names(); | |
| 501 | + $tables = openstation_files_table_names(); | |
| 464 | 502 | |
| 465 | 503 | // Access gate + shared-namespace decision for non-root folders. |
| 466 | 504 | // Desktop root (parent_id = 0) is always per-user. For sub- |
| 467 | 505 | // folders, the contents of a SHARED folder are visible to every |
| @@ -468,15 +506,15 @@ | ||
| 468 | 506 | // user who has at least 'read' on it — the icons inside belong |
| 469 | 507 | // to the folder, not to the user who originally placed them. |
| 470 | 508 | $share_view = false; |
| 471 | 509 | if ( $parent_id > 0 ) { |
| 472 | - $folder = desktop_mode_files_get_folder( $parent_id ); | |
| 510 | + $folder = openstation_files_get_folder( $parent_id ); | |
| 473 | 511 | if ( ! $folder ) { |
| 474 | 512 | return array(); |
| 475 | 513 | } |
| 476 | 514 | if ( (int) $folder['owner_id'] !== $user_id ) { |
| 477 | - $cap = function_exists( 'desktop_mode_folder_share_user_capability' ) | |
| 478 | - ? desktop_mode_folder_share_user_capability( $parent_id, $user_id ) | |
| 515 | + $cap = function_exists( 'openstation_folder_share_user_capability' ) | |
| 516 | + ? openstation_folder_share_user_capability( $parent_id, $user_id ) | |
| 479 | 517 | : 'none'; |
| 480 | 518 | if ( 'none' === $cap ) { |
| 481 | 519 | return array(); |
| 482 | 520 | } |
| @@ -491,15 +529,13 @@ | ||
| 491 | 529 | ); |
| 492 | 530 | /** |
| 493 | 531 | * Filter the args used to read placements. |
| 494 | 532 | * |
| 495 | - * @since 0.9.0 | |
| 496 | - * | |
| 497 | 533 | * @param array $args Defaults: `{ user_id, parent_id, share_view }`. |
| 498 | 534 | * @param int $user_id Viewer. |
| 499 | 535 | * @param int $parent_id Folder id. |
| 500 | 536 | */ |
| 501 | - $args = (array) apply_filters( 'desktop_mode_files_query_args', $args, $user_id, $parent_id ); | |
| 537 | + $args = (array) apply_filters( 'openstation_files_query_args', $args, $user_id, $parent_id ); | |
| 502 | 538 | |
| 503 | 539 | // Active queries always exclude trashed rows. Recycle-bin |
| 504 | 540 | // callers reach for the dedicated trash store. |
| 505 | 541 | if ( ! empty( $args['share_view'] ) ) { |
| @@ -535,10 +571,10 @@ | ||
| 535 | 571 | return array(); |
| 536 | 572 | } |
| 537 | 573 | $out = array(); |
| 538 | 574 | foreach ( $rows as $row ) { |
| 539 | - $normalized = desktop_mode_files_normalize_placement_row( $row ); | |
| 540 | - $file = desktop_mode_resolve_file( $normalized['file_type'], $normalized['file_ref'] ); | |
| 575 | + $normalized = openstation_files_normalize_placement_row( $row ); | |
| 576 | + $file = openstation_resolve_file( $normalized['file_type'], $normalized['file_ref'] ); | |
| 541 | 577 | if ( empty( $args['share_view'] ) ) { |
| 542 | 578 | // Private folder / desktop root — keep the existing |
| 543 | 579 | // per-row read filter so stale/inaccessible entities |
| 544 | 580 | // don't clutter the user's own view. |
| @@ -544,9 +580,9 @@ | ||
| 544 | 580 | // don't clutter the user's own view. |
| 545 | 581 | if ( $file && ! $file->can_read( $user_id ) ) { |
| 546 | 582 | continue; |
| 547 | 583 | } |
| 548 | - } else { | |
| 584 | + } elseif ( $file && ! $file->can_read( $user_id ) ) { | |
| 549 | 585 | // Shared folder view — every placement the OWNER chose |
| 550 | 586 | // to include is surfaced to the recipient. When the |
| 551 | 587 | // recipient lacks read on the underlying entity, we |
| 552 | 588 | // mark the row as `access_gated` so the tile renderer |
| @@ -553,11 +589,9 @@ | ||
| 553 | 589 | // can paint a lock overlay + tooltip + intercept the |
| 554 | 590 | // open. Entity-level access enforcement still happens |
| 555 | 591 | // at open time in each opener — this flag is just the |
| 556 | 592 | // pre-emptive visual cue. |
| 557 | - if ( $file && ! $file->can_read( $user_id ) ) { | |
| 558 | - $normalized['access_gated'] = true; | |
| 559 | - } | |
| 593 | + $normalized['access_gated'] = true; | |
| 560 | 594 | } |
| 561 | 595 | $out[] = $normalized; |
| 562 | 596 | } |
| 563 | 597 | return $out; |
| @@ -570,9 +604,9 @@ | ||
| 570 | 604 | * 1. Folders the viewer owns that have no placement anywhere. |
| 571 | 605 | * (Pre-fix folder-create flow could leak these; new flow |
| 572 | 606 | * writes the placement atomically.) |
| 573 | 607 | * |
| 574 | - * 2. Plugin shortcuts (`desktop_mode_register_icon()`) the | |
| 608 | + * 2. Plugin shortcuts (`openstation_register_icon()`) the | |
| 575 | 609 | * viewer hasn't placed yet. The unified-rail merge means |
| 576 | 610 | * every registered icon shows up as a `shortcut` placement |
| 577 | 611 | * on first hydrate so plugin shortcuts behave like any |
| 578 | 612 | * other tile (drag, sort, right-click, clean up). |
| @@ -577,20 +611,20 @@ | ||
| 577 | 611 | * on first hydrate so plugin shortcuts behave like any |
| 578 | 612 | * other tile (drag, sort, right-click, clean up). |
| 579 | 613 | * |
| 580 | 614 | * Idempotent on both axes: a folder/shortcut that already has |
| 581 | - * any placement is left alone. Coordinates use the column-major | |
| 582 | - * grid that `src/desktop-files/grid.ts` mirrors on the JS side. | |
| 615 | + * any placement is left alone. Coordinates come from | |
| 616 | + * `includes/desktop-files/grid.php`, which mirrors | |
| 617 | + * `src/desktop-files/grid.ts` — pitch, reading order, and the | |
| 618 | + * assumed canvas the scan wraps at. | |
| 583 | 619 | * |
| 584 | 620 | * Called by the placements list endpoint when the requested |
| 585 | 621 | * folder is the root (`parent_id=0`). |
| 586 | 622 | * |
| 587 | - * @since 0.9.0 | |
| 588 | - * | |
| 589 | 623 | * @param int $user_id Viewer. |
| 590 | 624 | * @return int Total number of orphans that were auto-placed. |
| 591 | 625 | */ |
| 592 | -function desktop_mode_files_auto_place_orphans( $user_id ) { | |
| 626 | +function openstation_files_auto_place_orphans( $user_id ) { | |
| 593 | 627 | global $wpdb; |
| 594 | 628 | $user_id = (int) $user_id; |
| 595 | 629 | if ( $user_id <= 0 ) { |
| 596 | 630 | return 0; |
| @@ -595,9 +629,9 @@ | ||
| 595 | 629 | if ( $user_id <= 0 ) { |
| 596 | 630 | return 0; |
| 597 | 631 | } |
| 598 | 632 | |
| 599 | - $tables = desktop_mode_files_table_names(); | |
| 633 | + $tables = openstation_files_table_names(); | |
| 600 | 634 | |
| 601 | 635 | // 1) Owned folders without any placement. Skip trashed folders |
| 602 | 636 | // and trashed placement rows so a recycled folder doesn't get |
| 603 | 637 | // auto-placed back on the desktop on next hydrate. |
| @@ -616,20 +650,20 @@ | ||
| 616 | 650 | ARRAY_A |
| 617 | 651 | ); |
| 618 | 652 | |
| 619 | 653 | // 2) Registered plugin shortcuts the viewer hasn't placed yet. |
| 620 | - // Pull the registered ids first, then ask the placements | |
| 621 | - // table which the viewer already has — set difference | |
| 622 | - // yields the orphans without a heavy join. | |
| 623 | - $shortcut_ids = array(); | |
| 624 | - $registry = function_exists( 'desktop_mode_desktop_icon_registry' ) | |
| 625 | - ? desktop_mode_desktop_icon_registry() | |
| 654 | + // Pull the registered ids first, then ask the placements | |
| 655 | + // table which the viewer already has — set difference | |
| 656 | + // yields the orphans without a heavy join. | |
| 657 | + $shortcut_ids = array(); | |
| 658 | + $registry = function_exists( 'openstation_desktop_icon_registry' ) | |
| 659 | + ? openstation_desktop_icon_registry() | |
| 626 | 660 | : array(); |
| 627 | 661 | if ( is_array( $registry ) ) { |
| 628 | - // Run through the same `desktop_mode_icons` filter the | |
| 662 | + // Run through the same `openstation_icons` filter the | |
| 629 | 663 | // build-payload path uses so plugins (and tests) can inject |
| 630 | 664 | // virtual entries. |
| 631 | - $registry = (array) apply_filters( 'desktop_mode_icons', $registry ); | |
| 665 | + $registry = (array) apply_filters( 'openstation_icons', $registry ); | |
| 632 | 666 | } |
| 633 | 667 | if ( is_array( $registry ) && ! empty( $registry ) ) { |
| 634 | 668 | $registered_ids = array_map( 'strval', array_keys( $registry ) ); |
| 635 | 669 | $placeholders = implode( ',', array_fill( 0, count( $registered_ids ), '%s' ) ); |
| @@ -643,9 +677,9 @@ | ||
| 643 | 677 | AND file_ref IN ($placeholders)", |
| 644 | 678 | $args |
| 645 | 679 | ) |
| 646 | 680 | ); |
| 647 | - $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) ); | |
| 681 | + $placed_set = array_flip( array_map( 'strval', (array) $placed_ids ) ); | |
| 648 | 682 | foreach ( $registered_ids as $id ) { |
| 649 | 683 | if ( ! isset( $placed_set[ $id ] ) ) { |
| 650 | 684 | $shortcut_ids[] = $id; |
| 651 | 685 | } |
| @@ -657,10 +691,11 @@ | ||
| 657 | 691 | } |
| 658 | 692 | |
| 659 | 693 | // Build an occupied set from EXISTING root placements so |
| 660 | 694 | // we never drop an orphan on top of a tile the user |
| 661 | - // already has. Cell math mirrors `src/desktop-files/grid.ts` | |
| 662 | - // (padding 16 + col 96 + row 110). | |
| 695 | + // already has. Cell math lives in | |
| 696 | + // `includes/desktop-files/grid.php`, the mirror of | |
| 697 | + // `src/desktop-files/grid.ts`. | |
| 663 | 698 | $existing = $wpdb->get_results( |
| 664 | 699 | $wpdb->prepare( |
| 665 | 700 | "SELECT x, y FROM {$tables['placements']} |
| 666 | 701 | WHERE owner_id = %d |
| @@ -669,47 +704,33 @@ | ||
| 669 | 704 | $user_id |
| 670 | 705 | ), |
| 671 | 706 | ARRAY_A |
| 672 | 707 | ); |
| 673 | - $occupied = array(); | |
| 674 | - foreach ( (array) $existing as $row ) { | |
| 675 | - $col = max( 0, (int) round( ( (int) $row['x'] - 16 ) / 96 ) ); | |
| 676 | - $row_idx = max( 0, (int) round( ( (int) $row['y'] - 16 ) / 110 ) ); | |
| 677 | - $occupied[ "$col,$row_idx" ] = true; | |
| 678 | - } | |
| 708 | + $occupied = openstation_files_grid_occupied( $existing ); | |
| 679 | 709 | |
| 680 | - $find_next = function () use ( &$occupied ) { | |
| 681 | - for ( $col = 0; $col < 999; $col++ ) { | |
| 682 | - for ( $row = 0; $row < 999; $row++ ) { | |
| 683 | - $key = "$col,$row"; | |
| 684 | - if ( ! isset( $occupied[ $key ] ) ) { | |
| 685 | - $occupied[ $key ] = true; | |
| 686 | - return array( $col, $row ); | |
| 687 | - } | |
| 688 | - } | |
| 689 | - } | |
| 690 | - return array( 0, 0 ); | |
| 691 | - }; | |
| 710 | + // The desktop reads in columns, and the scan wraps to the next one | |
| 711 | + // at the assumed canvas height rather than running a column 999 | |
| 712 | + // cells deep. The server has no viewport; a slot it invents below | |
| 713 | + // the fold is a tile the user cannot reach, because the layer that | |
| 714 | + // renders it does not scroll. | |
| 715 | + $order = openstation_files_grid_order( 0 ); | |
| 692 | 716 | |
| 693 | - $placed = 0; | |
| 717 | + $placed = 0; | |
| 694 | 718 | $emit_at = function ( $type, $ref, $col, $row ) use ( $user_id, &$occupied, &$placed ) { |
| 695 | 719 | $occupied[ "$col,$row" ] = true; |
| 696 | - $result = desktop_mode_files_place( | |
| 720 | + $result = openstation_files_place( | |
| 697 | 721 | $user_id, |
| 698 | 722 | 0, |
| 699 | 723 | $type, |
| 700 | 724 | (string) $ref, |
| 701 | - array( | |
| 702 | - 'x' => 16 + $col * 96, | |
| 703 | - 'y' => 16 + $row * 110, | |
| 704 | - ) | |
| 725 | + openstation_files_grid_cell_to_point( $col, $row ) | |
| 705 | 726 | ); |
| 706 | 727 | if ( ! is_wp_error( $result ) ) { |
| 707 | - $placed++; | |
| 728 | + ++$placed; | |
| 708 | 729 | } |
| 709 | 730 | }; |
| 710 | - $emit_next = function ( $type, $ref ) use ( $find_next, $emit_at ) { | |
| 711 | - list( $col, $row ) = $find_next(); | |
| 731 | + $emit_next = function ( $type, $ref ) use ( &$occupied, $order, $emit_at ) { | |
| 732 | + list( $col, $row ) = openstation_files_grid_next_free( $occupied, $order ); | |
| 712 | 733 | $emit_at( $type, $ref, $col, $row ); |
| 713 | 734 | }; |
| 714 | 735 | |
| 715 | 736 | // Pinned shortcuts get reserved top-left slots. Anchored to |
| @@ -734,9 +755,9 @@ | ||
| 734 | 755 | // client-side override anyway, but a future cleanup pass |
| 735 | 756 | // can compact the column. |
| 736 | 757 | $occupied[ "0,$pinned_idx" ] = true; |
| 737 | 758 | $emit_at( 'shortcut', $id, 0, $pinned_idx ); |
| 738 | - $pinned_idx++; | |
| 759 | + ++$pinned_idx; | |
| 739 | 760 | } |
| 740 | 761 | |
| 741 | 762 | foreach ( $folder_rows as $row ) { |
| 742 | 763 | $emit_next( 'folder', $row['id'] ); |
| @@ -752,15 +773,15 @@ | ||
| 752 | 773 | |
| 753 | 774 | /** |
| 754 | 775 | * Backwards-compat alias for the older folder-only name. |
| 755 | 776 | * |
| 756 | - * @deprecated 0.9.0 Use {@see desktop_mode_files_auto_place_orphans}. | |
| 777 | + * @deprecated Use {@see openstation_files_auto_place_orphans}. | |
| 757 | 778 | * |
| 758 | 779 | * @param int $user_id Viewer. |
| 759 | 780 | * @return int |
| 760 | 781 | */ |
| 761 | -function desktop_mode_files_auto_place_orphan_folders( $user_id ) { | |
| 762 | - return desktop_mode_files_auto_place_orphans( $user_id ); | |
| 782 | +function openstation_files_auto_place_orphan_folders( $user_id ) { | |
| 783 | + return openstation_files_auto_place_orphans( $user_id ); | |
| 763 | 784 | } |
| 764 | 785 | |
| 765 | 786 | /** |
| 766 | 787 | * Coerce wpdb's stringly-typed row into typed values + decoded |
| @@ -765,15 +786,14 @@ | ||
| 765 | 786 | /** |
| 766 | 787 | * Coerce wpdb's stringly-typed row into typed values + decoded |
| 767 | 788 | * meta. Internal helper. |
| 768 | 789 | * |
| 769 | - * @since 0.9.0 | |
| 770 | 790 | * @internal |
| 771 | 791 | * |
| 772 | 792 | * @param array $row Raw wpdb row. |
| 773 | 793 | * @return array |
| 774 | 794 | */ |
| 775 | -function desktop_mode_files_normalize_placement_row( $row ) { | |
| 795 | +function openstation_files_normalize_placement_row( $row ) { | |
| 776 | 796 | $meta_raw = isset( $row['meta'] ) ? (string) $row['meta'] : ''; |
| 777 | 797 | $meta = '' !== $meta_raw ? json_decode( $meta_raw, true ) : null; |
| 778 | 798 | return array( |
| 779 | 799 | 'id' => (int) $row['id'], |
| @@ -778,9 +798,9 @@ | ||
| 778 | 798 | return array( |
| 779 | 799 | 'id' => (int) $row['id'], |
| 780 | 800 | 'owner_id' => (int) $row['owner_id'], |
| 781 | 801 | // `updated_by` is v10. Null on legacy rows — callers that |
| 782 | - // need the actor (e.g. `desktop_mode_files_check_if_match`) | |
| 802 | + // need the actor (e.g. `openstation_files_check_if_match`) | |
| 783 | 803 | // fall back to `owner_id` when this is null/missing. |
| 784 | 804 | 'updated_by' => isset( $row['updated_by'] ) ? (int) $row['updated_by'] : null, |
| 785 | 805 | 'parent_id' => (int) $row['parent_id'], |
| 786 | 806 | 'file_type' => (string) $row['file_type'], |
| @@ -799,34 +819,32 @@ | ||
| 799 | 819 | * Invariant (enforced by callers): tombstones may exist only for |
| 800 | 820 | * ids of PERMANENTLY-DELETED rows. Never write one for a soft- |
| 801 | 821 | * trashed row — soft-trash is reversible and the heartbeat already |
| 802 | 822 | * surfaces it via the `trashed_at_ms IS NOT NULL` query in |
| 803 | - * `desktop_mode_files_compute_heartbeat_delta`. A tombstone on a | |
| 823 | + * `openstation_files_compute_heartbeat_delta`. A tombstone on a | |
| 804 | 824 | * soft-trashed row lingers past restore and tells clients the row |
| 805 | 825 | * is gone while it is in fact alive — see the "shared folder |
| 806 | - * disappears on refresh" bug fixed in 0.8.5. | |
| 826 | + * disappears on refresh" bug. | |
| 807 | 827 | * |
| 808 | - * Pair every revival path (`desktop_mode_files_restore_placement`, | |
| 809 | - * `desktop_mode_files_restore_folder`, and the duplicate-key | |
| 810 | - * revival branch in `desktop_mode_files_place`) with | |
| 811 | - * {@see desktop_mode_files_clear_tombstones_for} so a row coming | |
| 828 | + * Pair every revival path (`openstation_files_restore_placement`, | |
| 829 | + * `openstation_files_restore_folder`, and the duplicate-key | |
| 830 | + * revival branch in `openstation_files_place`) with | |
| 831 | + * {@see openstation_files_clear_tombstones_for} so a row coming | |
| 812 | 832 | * back to life never carries lingering tombstones from a previous |
| 813 | 833 | * removal that turned out to be reversible. |
| 814 | 834 | * |
| 815 | - * @since 0.9.0 | |
| 816 | - * | |
| 817 | 835 | * @param string $kind 'placement' | 'folder'. |
| 818 | 836 | * @param int $ref Removed id. |
| 819 | 837 | */ |
| 820 | -function desktop_mode_files_write_tombstone( $kind, $ref ) { | |
| 838 | +function openstation_files_write_tombstone( $kind, $ref ) { | |
| 821 | 839 | global $wpdb; |
| 822 | - $tables = desktop_mode_files_table_names(); | |
| 840 | + $tables = openstation_files_table_names(); | |
| 823 | 841 | $wpdb->insert( |
| 824 | 842 | $tables['tombstones'], |
| 825 | 843 | array( |
| 826 | 844 | 'kind' => (string) $kind, |
| 827 | 845 | 'ref_id' => (int) $ref, |
| 828 | - 'removed_at_ms' => desktop_mode_files_now_ms(), | |
| 846 | + 'removed_at_ms' => openstation_files_now_ms(), | |
| 829 | 847 | ), |
| 830 | 848 | array( '%s', '%d', '%d' ) |
| 831 | 849 | ); |
| 832 | 850 | } |
| @@ -838,20 +856,18 @@ | ||
| 838 | 856 | * previous removal that turned out to be reversible. |
| 839 | 857 | * |
| 840 | 858 | * Idempotent — running it on a ref with no tombstones is a no-op. |
| 841 | 859 | * |
| 842 | - * @since 0.8.5 | |
| 843 | - * | |
| 844 | 860 | * @param string $kind 'placement' | 'folder'. |
| 845 | 861 | * @param int $ref_id Row id whose tombstones should be dropped. |
| 846 | 862 | */ |
| 847 | -function desktop_mode_files_clear_tombstones_for( $kind, $ref_id ) { | |
| 863 | +function openstation_files_clear_tombstones_for( $kind, $ref_id ) { | |
| 848 | 864 | global $wpdb; |
| 849 | 865 | $ref_id = (int) $ref_id; |
| 850 | 866 | if ( $ref_id <= 0 ) { |
| 851 | 867 | return; |
| 852 | 868 | } |
| 853 | - $tables = desktop_mode_files_table_names(); | |
| 869 | + $tables = openstation_files_table_names(); | |
| 854 | 870 | $wpdb->delete( |
| 855 | 871 | $tables['tombstones'], |
| 856 | 872 | array( |
| 857 | 873 | 'kind' => (string) $kind, |
| @@ -865,32 +881,28 @@ | ||
| 865 | 881 | * Daily prune of tombstones older than 7 days. Phase 6 may tune |
| 866 | 882 | * the retention window when the Heartbeat sync lands; for now 7d |
| 867 | 883 | * is plenty since a client that's been offline that long will |
| 868 | 884 | * always need a full REST resync anyway. |
| 869 | - * | |
| 870 | - * @since 0.9.0 | |
| 871 | 885 | */ |
| 872 | -function desktop_mode_files_prune_tombstones() { | |
| 886 | +function openstation_files_prune_tombstones() { | |
| 873 | 887 | global $wpdb; |
| 874 | - $tables = desktop_mode_files_table_names(); | |
| 875 | - $cutoff = desktop_mode_files_now_ms() - ( 7 * DAY_IN_SECONDS * 1000 ); | |
| 888 | + $tables = openstation_files_table_names(); | |
| 889 | + $cutoff = openstation_files_now_ms() - ( 7 * DAY_IN_SECONDS * 1000 ); | |
| 876 | 890 | $wpdb->query( $wpdb->prepare( "DELETE FROM {$tables['tombstones']} WHERE removed_at_ms < %d", $cutoff ) ); |
| 877 | 891 | } |
| 878 | -add_action( 'desktop_mode_files_daily_prune', 'desktop_mode_files_prune_tombstones' ); | |
| 892 | +add_action( 'desktop_mode_files_daily_prune', 'openstation_files_prune_tombstones' ); | |
| 879 | 893 | |
| 880 | 894 | /** |
| 881 | 895 | * Schedule the daily prune. Hooked on `init` and idempotent via |
| 882 | 896 | * wp_next_scheduled(), so a manual file-copy install (no activation |
| 883 | 897 | * hook) still gets the cron event. |
| 884 | - * | |
| 885 | - * @since 0.9.0 | |
| 886 | 898 | */ |
| 887 | -function desktop_mode_files_schedule_prune() { | |
| 899 | +function openstation_files_schedule_prune() { | |
| 888 | 900 | if ( ! wp_next_scheduled( 'desktop_mode_files_daily_prune' ) ) { |
| 889 | 901 | wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'desktop_mode_files_daily_prune' ); |
| 890 | 902 | } |
| 891 | 903 | } |
| 892 | -add_action( 'init', 'desktop_mode_files_schedule_prune' ); | |
| 904 | +add_action( 'init', 'openstation_files_schedule_prune' ); | |
| 893 | 905 | |
| 894 | 906 | /** |
| 895 | 907 | * Walk the folder-parentage chain upward from `$target_parent_id` and |
| 896 | 908 | * return `true` when `$moving_folder_id` appears anywhere in it — |
| @@ -908,16 +920,14 @@ | ||
| 908 | 920 | * Defends against pre-existing cycles in the data: if we re-visit a |
| 909 | 921 | * cursor we've already seen, we treat it as a cycle and reject, so a |
| 910 | 922 | * corrupted history can't drive this function into an infinite loop. |
| 911 | 923 | * |
| 912 | - * @since 0.8.6 | |
| 913 | - * | |
| 914 | 924 | * @param int $user_id Acting user. |
| 915 | 925 | * @param int $moving_folder_id Folder being moved (its `folders.id`). |
| 916 | 926 | * @param int $target_parent_id New container folder id (0 = desktop root). |
| 917 | 927 | * @return bool True when the move would create a cycle. |
| 918 | 928 | */ |
| 919 | -function desktop_mode_files_would_create_folder_cycle( $user_id, $moving_folder_id, $target_parent_id ) { | |
| 929 | +function openstation_files_would_create_folder_cycle( $user_id, $moving_folder_id, $target_parent_id ) { | |
| 920 | 930 | $moving_folder_id = (int) $moving_folder_id; |
| 921 | 931 | $target_parent_id = (int) $target_parent_id; |
| 922 | 932 | $user_id = (int) $user_id; |
| 923 | 933 | if ( $moving_folder_id <= 0 || $target_parent_id <= 0 || $user_id <= 0 ) { |
| @@ -926,9 +936,9 @@ | ||
| 926 | 936 | if ( $moving_folder_id === $target_parent_id ) { |
| 927 | 937 | return true; |
| 928 | 938 | } |
| 929 | 939 | global $wpdb; |
| 930 | - $tables = desktop_mode_files_table_names(); | |
| 940 | + $tables = openstation_files_table_names(); | |
| 931 | 941 | $visited = array(); |
| 932 | 942 | $cursor = $target_parent_id; |
| 933 | 943 | // Hard cap to defend against catastrophically deep trees too — |
| 934 | 944 | // real installs won't approach 256. |