| @@ -290,8 +290,35 @@ | ||
| 290 | 290 | |
| 291 | 291 | /** |
| 292 | 292 | * `desktop-mode/get-post` permission callback. |
| 293 | 293 | * |
| 294 | + * Asks every gate a single read has, in the order Core's REST | |
| 295 | + * controllers ask them: | |
| 296 | + * | |
| 297 | + * - A zero id is refused before any fetch: `get_post( 0 )` returns | |
| 298 | + * the global post, which would judge the request against whatever | |
| 299 | + * another plugin left there. | |
| 300 | + * - `read_post` decides visibility (published / private / draft) and | |
| 301 | + * stays the floor for every row. | |
| 302 | + * - The post password is a separate question — WordPress splits the | |
| 303 | + * two deliberately. A sealed post stays sealed unless the caller can | |
| 304 | + * edit it (the same escape hatch | |
| 305 | + * `WP_REST_Posts_Controller::check_password_required()` grants), and | |
| 306 | + * because this ability returns RAW `post_content` there is no empty | |
| 307 | + * rendered field to fall back to, so the answer is to refuse. | |
| 308 | + * - A post type with no readable front end (`is_post_type_viewable()` | |
| 309 | + * false: an order, a submission log, a queue entry) is read only by | |
| 310 | + * a caller who can edit the row. `map_meta_cap()` resolves | |
| 311 | + * `read_post` on a published row of such a type to plain `read`, | |
| 312 | + * which every logged-in user holds, so `read_post` alone does not | |
| 313 | + * answer the question for it. | |
| 314 | + * | |
| 315 | + * `openstation_ai_can_read_post()` (loaded unconditionally from the AI | |
| 316 | + * Copilot bootstrap, ahead of this module) implements the password and | |
| 317 | + * post-type gates; this callback keeps `read_post` in front of it so a | |
| 318 | + * plugin that narrows `read_post` on a public post still narrows this | |
| 319 | + * ability. | |
| 320 | + * | |
| 294 | 321 | * @param array $args Input args. |
| 295 | 322 | * @return bool |
| 296 | 323 | */ |
| 297 | 324 | function openstation_agents_ability_get_post_can( $args ) { |
| @@ -299,9 +326,12 @@ | ||
| 299 | 326 | $post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : 0; |
| 300 | 327 | if ( $post_id <= 0 ) { |
| 301 | 328 | return false; |
| 302 | 329 | } |
| 303 | - return current_user_can( 'read_post', $post_id ); | |
| 330 | + if ( ! current_user_can( 'read_post', $post_id ) ) { | |
| 331 | + return false; | |
| 332 | + } | |
| 333 | + return openstation_ai_can_read_post( $post_id ); | |
| 304 | 334 | } |
| 305 | 335 | |
| 306 | 336 | /** |
| 307 | 337 | * `desktop-mode/get-media` execute callback. |
| @@ -339,14 +369,33 @@ | ||
| 339 | 369 | |
| 340 | 370 | /** |
| 341 | 371 | * `desktop-mode/get-media` permission callback. |
| 342 | 372 | * |
| 343 | - * Gates on `upload_files` (author+), deliberately NOT on `read_post`: | |
| 344 | - * for `inherit`-status attachments that check defers to the parent | |
| 345 | - * post (and effectively requires edit rights when unattached), which | |
| 346 | - * wrongly blocks read-only access to media whose file URL is public | |
| 347 | - * on a standard site anyway. | |
| 373 | + * Gates on `upload_files` (author+) — the capability the Media | |
| 374 | + * Library itself requires — rather than on `read_post` of the | |
| 375 | + * attachment. | |
| 348 | 376 | * |
| 377 | + * An attached file is a child of its parent post, and the result | |
| 378 | + * carries the attachment's title, caption and `attachedTo` (the parent | |
| 379 | + * id), so an attached file also requires that the caller can read the | |
| 380 | + * parent. That follows the shape of Core's rule for `inherit`-status | |
| 381 | + * attachments, `WP_REST_Posts_Controller::check_read_permission()` | |
| 382 | + * (the attachments controller inherits it): an attachment defers to | |
| 383 | + * its parent whenever one exists. The parent is judged by `read_post`, | |
| 384 | + * plus the post-type rule `desktop-mode/get-post` applies (a type with | |
| 385 | + * no readable front end needs `edit_post`), which is stricter than Core | |
| 386 | + * on a non-viewable parent: Core admits any `publish` parent of a | |
| 387 | + * REST-enabled type. Core's other requirement, that the parent's type | |
| 388 | + * be `show_in_rest`, is not copied: it would refuse media attached to a | |
| 389 | + * non-REST type for every caller, administrators included. The | |
| 390 | + * parent's password is not asked: the attachment's own fields are not | |
| 391 | + * the parent's body, and Core's attachment read does not ask it either. | |
| 392 | + * | |
| 393 | + * An unattached file, or one whose parent row no longer exists, is | |
| 394 | + * judged on `upload_files` alone, as Core treats a parentless | |
| 395 | + * `inherit` attachment as published. A zero id is refused before any | |
| 396 | + * fetch, because `get_post( 0 )` returns the global post. | |
| 397 | + * | |
| 349 | 398 | * @param array $args Input args. |
| 350 | 399 | * @return bool |
| 351 | 400 | */ |
| 352 | 401 | function openstation_agents_ability_get_media_can( $args ) { |
| @@ -354,9 +403,35 @@ | ||
| 354 | 403 | $attachment_id = isset( $args['attachment_id'] ) ? (int) $args['attachment_id'] : 0; |
| 355 | 404 | if ( $attachment_id <= 0 ) { |
| 356 | 405 | return false; |
| 357 | 406 | } |
| 358 | - return current_user_can( 'upload_files' ); | |
| 407 | + if ( ! current_user_can( 'upload_files' ) ) { | |
| 408 | + return false; | |
| 409 | + } | |
| 410 | + | |
| 411 | + $attachment = get_post( $attachment_id ); | |
| 412 | + if ( ! ( $attachment instanceof WP_Post ) || 'attachment' !== $attachment->post_type ) { | |
| 413 | + // The execute callback answers "not found" for these. | |
| 414 | + return true; | |
| 415 | + } | |
| 416 | + | |
| 417 | + $parent_id = (int) $attachment->post_parent; | |
| 418 | + if ( $parent_id <= 0 || $parent_id === $attachment_id ) { | |
| 419 | + return true; | |
| 420 | + } | |
| 421 | + $parent = get_post( $parent_id ); | |
| 422 | + if ( ! ( $parent instanceof WP_Post ) ) { | |
| 423 | + return true; | |
| 424 | + } | |
| 425 | + | |
| 426 | + if ( ! current_user_can( 'read_post', $parent->ID ) ) { | |
| 427 | + return false; | |
| 428 | + } | |
| 429 | + $parent_type = get_post_type_object( $parent->post_type ); | |
| 430 | + if ( ! $parent_type || ! is_post_type_viewable( $parent_type ) ) { | |
| 431 | + return current_user_can( 'edit_post', $parent->ID ); | |
| 432 | + } | |
| 433 | + return true; | |
| 359 | 434 | } |
| 360 | 435 | |
| 361 | 436 | /** |
| 362 | 437 | * `desktop-mode/update-media` execute callback. |