PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 All 36 releases
← All changes | includes/agents/abilities.php +124 -49 0.9.8 → 1.1.12 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Agents: abilities bridge.
3 + * OpenStation — Agents: abilities bridge.
4 4 *
5 5 * Two halves:
6 6 *
7 7 * 1. Registers the agent-oriented abilities against Core's Abilities
@@ -8,9 +8,9 @@
8 8 * API: `desktop-mode/get-post` and `desktop-mode/get-media`
9 9 * (read-only) plus the mutating trio `desktop-mode/update-post`,
10 10 * `desktop-mode/update-media` (alt text / title / caption /
11 11 * description), and `desktop-mode/create-post` (draft-only). The
12 - * `desktop-mode` category ships from the AI Copilot module
12 + * `openstation` category ships from the AI Copilot module
13 13 * (always loaded), so this file only adds abilities to it. The
14 14 * read abilities carry the `readonly` annotation and therefore
15 15 * also become available to the AI Copilot assistant; the mutating
16 16 * ones do not — they are reachable only through an agent whose
@@ -24,9 +24,9 @@
24 24 * compensating controls are the explicit per-agent allowlist set by
25 25 * an `edit_users` human, the agent's role, and each ability's own
26 26 * `permission_callback` evaluated against the agent user.
27 27 *
28 - * @package WPDesktopMode
28 + * @package OpenStation
29 29 */
30 30
31 31 defined( 'ABSPATH' ) || exit;
32 32
@@ -34,9 +34,9 @@
34 34 * Registers the agent-oriented abilities.
35 35 *
36 36 * @return void
37 37 */
38 -function desktop_mode_agents_register_abilities() {
38 +function openstation_agents_register_abilities() {
39 39 if ( ! function_exists( 'wp_register_ability' ) ) {
40 40 return;
41 41 }
42 42
@@ -51,9 +51,9 @@
51 51 // instructions leaves every other agent guessing, and a
52 52 // cautious one will refuse to write rather than risk
53 53 // flattening blocks.
54 54 'description' => 'Return a post — title, content, excerpt, status, author, dates — by its numeric id. `content` is the RAW stored content exactly as saved, with block delimiter comments (`<!-- wp:… -->`) intact; it is never rendered output, so it is safe to edit and write back. Honours the caller\'s read capability.',
55 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
55 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
56 56 'input_schema' => array(
57 57 'type' => 'object',
58 58 'additionalProperties' => false,
59 59 'required' => array( 'post_id' ),
@@ -63,9 +63,9 @@
63 63 'description' => 'The post id to fetch.',
64 64 ),
65 65 ),
66 66 ),
67 - 'output_schema' => desktop_mode_ai_ability_output_schema(
67 + 'output_schema' => openstation_ai_ability_output_schema(
68 68 array(
69 69 'id' => array( 'type' => 'integer' ),
70 70 'title' => array( 'type' => 'string' ),
71 71 'content' => array( 'type' => 'string' ),
@@ -71,10 +71,10 @@
71 71 'content' => array( 'type' => 'string' ),
72 72 'status' => array( 'type' => 'string' ),
73 73 )
74 74 ),
75 - 'execute_callback' => 'desktop_mode_agents_ability_get_post',
76 - 'permission_callback' => 'desktop_mode_agents_ability_get_post_can',
75 + 'execute_callback' => 'openstation_agents_ability_get_post',
76 + 'permission_callback' => 'openstation_agents_ability_get_post_can',
77 77 'meta' => array(
78 78 'annotations' => array(
79 79 'readonly' => true,
80 80 'idempotent' => true,
@@ -88,9 +88,9 @@
88 88 'desktop-mode/get-media',
89 89 array(
90 90 'label' => __( 'Get media details', 'desktop-mode' ),
91 91 'description' => 'Return details for a media library item (attachment) by numeric id: file URL, mime type, dimensions, alt text, caption, and the post it is attached to. Use this to read images or other media referenced by posts.',
92 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
92 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
93 93 'input_schema' => array(
94 94 'type' => 'object',
95 95 'additionalProperties' => false,
96 96 'required' => array( 'attachment_id' ),
@@ -100,9 +100,9 @@
100 100 'description' => 'The attachment (media library) id.',
101 101 ),
102 102 ),
103 103 ),
104 - 'output_schema' => desktop_mode_ai_ability_output_schema(
104 + 'output_schema' => openstation_ai_ability_output_schema(
105 105 array(
106 106 'id' => array( 'type' => 'integer' ),
107 107 'url' => array( 'type' => 'string' ),
108 108 'mime' => array( 'type' => 'string' ),
@@ -107,10 +107,10 @@
107 107 'url' => array( 'type' => 'string' ),
108 108 'mime' => array( 'type' => 'string' ),
109 109 )
110 110 ),
111 - 'execute_callback' => 'desktop_mode_agents_ability_get_media',
112 - 'permission_callback' => 'desktop_mode_agents_ability_get_media_can',
111 + 'execute_callback' => 'openstation_agents_ability_get_media',
112 + 'permission_callback' => 'openstation_agents_ability_get_media_can',
113 113 'meta' => array(
114 114 'annotations' => array(
115 115 'readonly' => true,
116 116 'idempotent' => true,
@@ -124,9 +124,9 @@
124 124 'desktop-mode/update-media',
125 125 array(
126 126 'label' => __( 'Update media details', 'desktop-mode' ),
127 127 'description' => 'Update metadata on a media library item (attachment): alt text, title, caption, and/or description. The file itself is never touched. Honours the edit capability on the attachment.',
128 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
128 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
129 129 'input_schema' => array(
130 130 'type' => 'object',
131 131 'additionalProperties' => false,
132 132 'required' => array( 'attachment_id' ),
@@ -152,16 +152,16 @@
152 152 'description' => 'New description.',
153 153 ),
154 154 ),
155 155 ),
156 - 'output_schema' => desktop_mode_ai_ability_output_schema(
156 + 'output_schema' => openstation_ai_ability_output_schema(
157 157 array(
158 158 'id' => array( 'type' => 'integer' ),
159 159 'updated' => array( 'type' => 'boolean' ),
160 160 )
161 161 ),
162 - 'execute_callback' => 'desktop_mode_agents_ability_update_media',
163 - 'permission_callback' => 'desktop_mode_agents_ability_update_media_can',
162 + 'execute_callback' => 'openstation_agents_ability_update_media',
163 + 'permission_callback' => 'openstation_agents_ability_update_media_can',
164 164 'meta' => array(
165 165 'show_in_rest' => true,
166 166 ),
167 167 )
@@ -171,9 +171,9 @@
171 171 'desktop-mode/create-post',
172 172 array(
173 173 'label' => __( 'Create draft post', 'desktop-mode' ),
174 174 'description' => 'Create a NEW post or page as a DRAFT, authored by the calling user. The status is always draft: this ability can never publish. Use it to produce reviewable content (translations, variants, generated drafts) without touching any existing post. `content` is stored RAW, exactly as passed, so send block markup with its delimiter comments (`<!-- wp:… -->`) intact.',
175 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
175 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
176 176 'input_schema' => array(
177 177 'type' => 'object',
178 178 'additionalProperties' => false,
179 179 'required' => array( 'title', 'content' ),
@@ -196,16 +196,16 @@
196 196 'description' => 'Post type. Defaults to post.',
197 197 ),
198 198 ),
199 199 ),
200 - 'output_schema' => desktop_mode_ai_ability_output_schema(
200 + 'output_schema' => openstation_ai_ability_output_schema(
201 201 array(
202 202 'id' => array( 'type' => 'integer' ),
203 203 'status' => array( 'type' => 'string' ),
204 204 )
205 205 ),
206 - 'execute_callback' => 'desktop_mode_agents_ability_create_post',
207 - 'permission_callback' => 'desktop_mode_agents_ability_create_post_can',
206 + 'execute_callback' => 'openstation_agents_ability_create_post',
207 + 'permission_callback' => 'openstation_agents_ability_create_post_can',
208 208 'meta' => array(
209 209 'show_in_rest' => true,
210 210 ),
211 211 )
@@ -215,9 +215,9 @@
215 215 'desktop-mode/update-post',
216 216 array(
217 217 'label' => __( 'Update post', 'desktop-mode' ),
218 218 'description' => 'Update fields on an existing post. Accepts any subset of title / content / excerpt / status. `content` is stored RAW, exactly as passed, so send block markup with its delimiter comments (`<!-- wp:… -->`) intact — passing rendered HTML would flatten the post\'s blocks. Honours the edit_post capability of the calling user.',
219 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
219 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
220 220 'input_schema' => array(
221 221 'type' => 'object',
222 222 'additionalProperties' => false,
223 223 'required' => array( 'post_id' ),
@@ -244,16 +244,16 @@
244 244 'description' => 'New post status.',
245 245 ),
246 246 ),
247 247 ),
248 - 'output_schema' => desktop_mode_ai_ability_output_schema(
248 + 'output_schema' => openstation_ai_ability_output_schema(
249 249 array(
250 250 'id' => array( 'type' => 'integer' ),
251 251 'updated' => array( 'type' => 'boolean' ),
252 252 )
253 253 ),
254 - 'execute_callback' => 'desktop_mode_agents_ability_update_post',
255 - 'permission_callback' => 'desktop_mode_agents_ability_update_post_can',
254 + 'execute_callback' => 'openstation_agents_ability_update_post',
255 + 'permission_callback' => 'openstation_agents_ability_update_post_can',
256 256 'meta' => array(
257 257 'show_in_rest' => true,
258 258 ),
259 259 )
@@ -258,9 +258,9 @@
258 258 ),
259 259 )
260 260 );
261 261 }
262 -add_action( 'wp_abilities_api_init', 'desktop_mode_agents_register_abilities' );
262 +add_action( 'wp_abilities_api_init', 'openstation_agents_register_abilities' );
263 263
264 264 /**
265 265 * `desktop-mode/get-post` execute callback.
266 266 *
@@ -266,14 +266,14 @@
266 266 *
267 267 * @param array $args Validated input.
268 268 * @return array|WP_Error
269 269 */
270 -function desktop_mode_agents_ability_get_post( $args ) {
270 +function openstation_agents_ability_get_post( $args ) {
271 271 $args = (array) $args;
272 272 $post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : 0;
273 273 $post = $post_id > 0 ? get_post( $post_id ) : null;
274 274 if ( ! ( $post instanceof WP_Post ) ) {
275 - return new WP_Error( 'desktop_mode_agent_post_not_found', __( 'Post not found.', 'desktop-mode' ) );
275 + return new WP_Error( 'openstation_agent_post_not_found', __( 'Post not found.', 'desktop-mode' ) );
276 276 }
277 277 return array(
278 278 'id' => (int) $post->ID,
279 279 'title' => (string) $post->post_title,
@@ -290,18 +290,48 @@
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 -function desktop_mode_agents_ability_get_post_can( $args ) {
324 +function openstation_agents_ability_get_post_can( $args ) {
298 325 $args = (array) $args;
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.
@@ -308,14 +338,14 @@
308 338 *
309 339 * @param array $args Validated input.
310 340 * @return array|WP_Error
311 341 */
312 -function desktop_mode_agents_ability_get_media( $args ) {
342 +function openstation_agents_ability_get_media( $args ) {
313 343 $args = (array) $args;
314 344 $attachment_id = isset( $args['attachment_id'] ) ? (int) $args['attachment_id'] : 0;
315 345 $post = $attachment_id > 0 ? get_post( $attachment_id ) : null;
316 346 if ( ! ( $post instanceof WP_Post ) || 'attachment' !== $post->post_type ) {
317 - return new WP_Error( 'desktop_mode_agent_media_not_found', __( 'Attachment not found.', 'desktop-mode' ) );
347 + return new WP_Error( 'openstation_agent_media_not_found', __( 'Attachment not found.', 'desktop-mode' ) );
318 348 }
319 349
320 350 $meta = wp_get_attachment_metadata( $attachment_id );
321 351 if ( ! is_array( $meta ) ) {
@@ -339,24 +369,69 @@
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 -function desktop_mode_agents_ability_get_media_can( $args ) {
401 +function openstation_agents_ability_get_media_can( $args ) {
353 402 $args = (array) $args;
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.
@@ -363,14 +438,14 @@
363 438 *
364 439 * @param array $args Validated input.
365 440 * @return array|WP_Error
366 441 */
367 -function desktop_mode_agents_ability_update_media( $args ) {
442 +function openstation_agents_ability_update_media( $args ) {
368 443 $args = (array) $args;
369 444 $attachment_id = isset( $args['attachment_id'] ) ? (int) $args['attachment_id'] : 0;
370 445 $post = $attachment_id > 0 ? get_post( $attachment_id ) : null;
371 446 if ( ! ( $post instanceof WP_Post ) || 'attachment' !== $post->post_type ) {
372 - return new WP_Error( 'desktop_mode_agent_media_not_found', __( 'Attachment not found.', 'desktop-mode' ) );
447 + return new WP_Error( 'openstation_agent_media_not_found', __( 'Attachment not found.', 'desktop-mode' ) );
373 448 }
374 449
375 450 if ( isset( $args['alt_text'] ) ) {
376 451 update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( (string) $args['alt_text'] ) );
@@ -405,9 +480,9 @@
405 480 *
406 481 * @param array $args Input args.
407 482 * @return bool
408 483 */
409 -function desktop_mode_agents_ability_update_media_can( $args ) {
484 +function openstation_agents_ability_update_media_can( $args ) {
410 485 $args = (array) $args;
411 486 $attachment_id = isset( $args['attachment_id'] ) ? (int) $args['attachment_id'] : 0;
412 487 if ( $attachment_id <= 0 ) {
413 488 return false;
@@ -422,9 +497,9 @@
422 497 *
423 498 * @param array $args Validated input.
424 499 * @return array|WP_Error
425 500 */
426 -function desktop_mode_agents_ability_create_post( $args ) {
501 +function openstation_agents_ability_create_post( $args ) {
427 502 $args = (array) $args;
428 503 $type = isset( $args['type'] ) && 'page' === $args['type'] ? 'page' : 'post';
429 504
430 505 $post_id = wp_insert_post(
@@ -456,9 +531,9 @@
456 531 *
457 532 * @param array $args Input args.
458 533 * @return bool
459 534 */
460 -function desktop_mode_agents_ability_create_post_can( $args ) {
535 +function openstation_agents_ability_create_post_can( $args ) {
461 536 $args = (array) $args;
462 537 if ( isset( $args['type'] ) && 'page' === $args['type'] ) {
463 538 return current_user_can( 'edit_pages' );
464 539 }
@@ -470,13 +545,13 @@
470 545 *
471 546 * @param array $args Validated input.
472 547 * @return array|WP_Error
473 548 */
474 -function desktop_mode_agents_ability_update_post( $args ) {
549 +function openstation_agents_ability_update_post( $args ) {
475 550 $args = (array) $args;
476 551 $post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : 0;
477 552 if ( $post_id <= 0 || ! get_post( $post_id ) ) {
478 - return new WP_Error( 'desktop_mode_agent_post_not_found', __( 'Post not found.', 'desktop-mode' ) );
553 + return new WP_Error( 'openstation_agent_post_not_found', __( 'Post not found.', 'desktop-mode' ) );
479 554 }
480 555
481 556 $update = array( 'ID' => $post_id );
482 557 if ( isset( $args['title'] ) ) {
@@ -490,9 +565,9 @@
490 565 }
491 566 if ( isset( $args['status'] ) ) {
492 567 $status = sanitize_key( (string) $args['status'] );
493 568 if ( ! in_array( $status, array( 'publish', 'draft', 'pending', 'private' ), true ) ) {
494 - return new WP_Error( 'desktop_mode_agent_invalid_status', __( 'Invalid post status.', 'desktop-mode' ) );
569 + return new WP_Error( 'openstation_agent_invalid_status', __( 'Invalid post status.', 'desktop-mode' ) );
495 570 }
496 571 $update['post_status'] = $status;
497 572 }
498 573
@@ -514,9 +589,9 @@
514 589 *
515 590 * @param array $args Input args.
516 591 * @return bool
517 592 */
518 -function desktop_mode_agents_ability_update_post_can( $args ) {
593 +function openstation_agents_ability_update_post_can( $args ) {
519 594 $args = (array) $args;
520 595 $post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : 0;
521 596 if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
522 597 return false;
@@ -536,9 +611,9 @@
536 611 * readonly/mutating badge derived from `meta.annotations.readonly`.
537 612 *
538 613 * @return array<int, array{slug:string, label:string, description:string, category:string, readonly:bool}>
539 614 */
540 -function desktop_mode_agents_abilities_catalogue() {
615 +function openstation_agents_abilities_catalogue() {
541 616 $catalogue = array();
542 617
543 618 if ( function_exists( 'wp_get_abilities' ) ) {
544 619 foreach ( wp_get_abilities() as $ability ) {
@@ -567,9 +642,9 @@
567 642 * registry.
568 643 *
569 644 * @param array $catalogue Abilities projected from `wp_get_abilities()`.
570 645 */
571 - $catalogue = apply_filters( 'desktop_mode_agent_abilities_catalogue', $catalogue );
646 + $catalogue = apply_filters( 'openstation_agent_abilities_catalogue', $catalogue );
572 647 if ( ! is_array( $catalogue ) ) {
573 648 return array();
574 649 }
575 650