| @@ -40,8 +40,14 @@ | ||
| 40 | 40 | * `post_parent` when attached. |
| 41 | 41 | * - `comment.php` (comment edit / moderation) — `comment`, rooted at |
| 42 | 42 | * the parent post. The URL alone can't answer this one; only real |
| 43 | 43 | * admin context can. |
| 44 | + * - `revision.php` (the revision browser) — `revisions`, rooted at | |
| 45 | + * the post whose history it shows. Keyed by the PARENT post rather | |
| 46 | + * than the revision on screen, because the browser's slider walks | |
| 47 | + * revisions client-side (`history.replaceState`) without a reload: | |
| 48 | + * a revision-keyed identity would go stale on the first drag, and | |
| 49 | + * the window is "the history of post 123" throughout anyway. | |
| 44 | 50 | * - `user-edit.php` / `profile.php` — `user`, a root identity. What |
| 45 | 51 | * points at a person (a post's author, an order's customer) does so |
| 46 | 52 | * from its own `links`. |
| 47 | 53 | * |
| @@ -71,8 +77,39 @@ | ||
| 71 | 77 | 'id' => $post_id, |
| 72 | 78 | ); |
| 73 | 79 | } |
| 74 | 80 | } |
| 81 | + } elseif ( 'revision.php' === $pagenow ) { | |
| 82 | + // Revision browser — `revision.php?revision=N`. Core falls back | |
| 83 | + // to `?to=N` when `revision` is absent (the compare-two-revisions | |
| 84 | + // form of the URL), so mirror that: both forms show the same | |
| 85 | + // post's history and must announce the same identity. | |
| 86 | + // | |
| 87 | + // The identity is keyed by the PARENT post, not by the revision | |
| 88 | + // on screen — see the "Detected screens" note above — which also | |
| 89 | + // means the shell can seed it at open time knowing only the post | |
| 90 | + // it opened the browser for, so the tie to the editor window | |
| 91 | + // draws before the iframe has finished loading. | |
| 92 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only identity harvest; the host admin page enforces capability + nonce. | |
| 93 | + $revision_id = isset( $_GET['revision'] ) ? absint( $_GET['revision'] ) : 0; | |
| 94 | + if ( ! $revision_id ) { | |
| 95 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only identity harvest; the host admin page enforces capability + nonce. | |
| 96 | + $revision_id = isset( $_GET['to'] ) ? absint( $_GET['to'] ) : 0; | |
| 97 | + } | |
| 98 | + $revision = $revision_id ? wp_get_post_revision( $revision_id ) : null; | |
| 99 | + $parent = $revision ? get_post( (int) $revision->post_parent ) : null; | |
| 100 | + if ( $parent instanceof WP_Post && current_user_can( 'edit_post', $parent->ID ) ) { | |
| 101 | + $identity = array( | |
| 102 | + 'type' => 'revisions', | |
| 103 | + 'id' => (int) $parent->ID, | |
| 104 | + /* translators: %s: post title. */ | |
| 105 | + 'label' => sprintf( __( 'Revisions of %s', 'desktop-mode' ), get_the_title( $parent ) ), | |
| 106 | + 'root' => array( | |
| 107 | + 'type' => sanitize_key( $parent->post_type ), | |
| 108 | + 'id' => (int) $parent->ID, | |
| 109 | + ), | |
| 110 | + ); | |
| 111 | + } | |
| 75 | 112 | } elseif ( $screen && 'post' === $screen->base && 'add' !== $screen->action ) { |
| 76 | 113 | $post = get_post(); |
| 77 | 114 | if ( $post instanceof WP_Post && $post->ID > 0 ) { |
| 78 | 115 | if ( 'attachment' === $post->post_type ) { |
| @@ -111,8 +148,14 @@ | ||
| 111 | 148 | if ( '' !== $preview_url ) { |
| 112 | 149 | $identity['previewUrl'] = $preview_url; |
| 113 | 150 | } |
| 114 | 151 | |
| 152 | + $revisions = openstation_window_revisions( $post ); | |
| 153 | + if ( '' !== $revisions['url'] ) { | |
| 154 | + $identity['revisionsUrl'] = $revisions['url']; | |
| 155 | + $identity['revisionCount'] = $revisions['count']; | |
| 156 | + } | |
| 157 | + | |
| 115 | 158 | // Source for the built-in related-entity items attached |
| 116 | 159 | // after the identity filter below. |
| 117 | 160 | $related_source_post = $post; |
| 118 | 161 | } |
| @@ -284,8 +327,111 @@ | ||
| 284 | 327 | return (string) apply_filters( 'openstation_window_preview_url', $preview_url, $post ); |
| 285 | 328 | } |
| 286 | 329 | |
| 287 | 330 | /** |
| 331 | + * Build the revision-browser link and revision total for a post — the | |
| 332 | + * target of the window ⋯ menu's "View revisions" row, and the count it | |
| 333 | + * shows beside the label. | |
| 334 | + * | |
| 335 | + * Core's revision browser is a whole admin screen that the block editor | |
| 336 | + * can only reach by navigating the editor away from itself; in a | |
| 337 | + * desktop shell it is simply another window, opened beside the editor | |
| 338 | + * and tied to it by a window link. That only needs two facts, and both | |
| 339 | + * are cheap enough to compute on every identity build. | |
| 340 | + * | |
| 341 | + * `wp_get_post_revisions()` with `fields => ids` returns the flat, | |
| 342 | + * newest-first id list straight out of `get_children()` — one query, no | |
| 343 | + * post hydration — and already answers `wp_revisions_enabled()` for the | |
| 344 | + * post type and the `WP_POST_REVISIONS` constant. Autosaves are | |
| 345 | + * included in the total, exactly as they are in Core's own revisions | |
| 346 | + * meta box and in the block editor's revisions panel, so the number | |
| 347 | + * beside the menu row matches the number the browser will list. | |
| 348 | + * | |
| 349 | + * @param WP_Post $post The post being edited. | |
| 350 | + * @return array { | |
| 351 | + * Revision browser descriptor. `url` is `''` when the post has no | |
| 352 | + * revisions to browse (revisions disabled for the type, none | |
| 353 | + * written yet, insufficient capability) or a filter suppressed it. | |
| 354 | + * | |
| 355 | + * @type string $url Admin URL of the revision browser. | |
| 356 | + * @type int $count Total revisions the browser will list. | |
| 357 | + * } | |
| 358 | + */ | |
| 359 | +function openstation_window_revisions( $post ) { | |
| 360 | + $revisions = array( | |
| 361 | + 'url' => '', | |
| 362 | + 'count' => 0, | |
| 363 | + ); | |
| 364 | + | |
| 365 | + if ( | |
| 366 | + $post instanceof WP_Post && | |
| 367 | + $post->ID > 0 && | |
| 368 | + 'attachment' !== $post->post_type && | |
| 369 | + // An auto-draft has never been saved, so it cannot have a | |
| 370 | + // revision — worth its own check because the REST recompute | |
| 371 | + // takes any post id and would otherwise spend a guaranteed- | |
| 372 | + // empty query on one. | |
| 373 | + 'auto-draft' !== $post->post_status && | |
| 374 | + post_type_supports( $post->post_type, 'revisions' ) && | |
| 375 | + current_user_can( 'edit_post', $post->ID ) | |
| 376 | + ) { | |
| 377 | + // One query, IDs only — `wp_get_post_revisions()` checks | |
| 378 | + // `wp_revisions_enabled()` BEFORE querying, so a post type with | |
| 379 | + // revisions turned off costs nothing here either. | |
| 380 | + $ids = wp_get_post_revisions( $post->ID, array( 'fields' => 'ids' ) ); | |
| 381 | + if ( ! empty( $ids ) ) { | |
| 382 | + /* | |
| 383 | + * `get_edit_post_link()` maps the `revision` post type onto | |
| 384 | + * `revision.php?revision=%d` and runs the `edit_post` meta | |
| 385 | + * cap — which for a revision maps to its parent — so it is | |
| 386 | + * the capability gate as much as the URL builder, and it | |
| 387 | + * stays correct if a plugin re-points the revision screen | |
| 388 | + * through the `get_edit_post_link` filter. | |
| 389 | + * | |
| 390 | + * Raw context deliberately: this URL is JSON-encoded into | |
| 391 | + * the bridge payload and ends up as an iframe `src`, where | |
| 392 | + * a display-escaped `&` would arrive as a literal. | |
| 393 | + */ | |
| 394 | + $link = get_edit_post_link( (int) reset( $ids ), 'raw' ); | |
| 395 | + if ( is_string( $link ) && '' !== $link ) { | |
| 396 | + $revisions['url'] = $link; | |
| 397 | + $revisions['count'] = count( $ids ); | |
| 398 | + } | |
| 399 | + } | |
| 400 | + } | |
| 401 | + | |
| 402 | + /** | |
| 403 | + * Filters the revision-browser descriptor attached to a post-editor | |
| 404 | + * content identity (`revisionsUrl` / `revisionCount` — the target | |
| 405 | + * of the window ⋯ menu's "View revisions" row). | |
| 406 | + * | |
| 407 | + * Return `array( 'url' => '', 'count' => 0 )` to hide the row for | |
| 408 | + * this post, or rewrite `url` to point somewhere else (a custom | |
| 409 | + * diff screen, a plugin's own history UI). Note the shell only | |
| 410 | + * accepts same-origin URLs; a cross-origin rewrite hides the row. | |
| 411 | + * | |
| 412 | + * @param array $revisions { | |
| 413 | + * @type string $url Admin URL of the revision browser, `''` when none applies. | |
| 414 | + * @type int $count Total revisions the browser will list. | |
| 415 | + * } | |
| 416 | + * @param WP_Post $post The post being edited. | |
| 417 | + */ | |
| 418 | + $revisions = apply_filters( 'openstation_window_revisions', $revisions, $post ); | |
| 419 | + | |
| 420 | + if ( ! is_array( $revisions ) ) { | |
| 421 | + return array( | |
| 422 | + 'url' => '', | |
| 423 | + 'count' => 0, | |
| 424 | + ); | |
| 425 | + } | |
| 426 | + | |
| 427 | + return array( | |
| 428 | + 'url' => isset( $revisions['url'] ) && is_string( $revisions['url'] ) ? $revisions['url'] : '', | |
| 429 | + 'count' => isset( $revisions['count'] ) ? max( 0, (int) $revisions['count'] ) : 0, | |
| 430 | + ); | |
| 431 | +} | |
| 432 | + | |
| 433 | +/** | |
| 288 | 434 | * The related-entity pass: attach the `related` navigation items to a |
| 289 | 435 | * (post-identity-filter) content identity. Shared by the page-render |
| 290 | 436 | * builder above and the REST recompute endpoint the editor |
| 291 | 437 | * save-watcher hits (where `$screen` is `null`). |
| @@ -791,8 +937,18 @@ | ||
| 791 | 937 | if ( '' !== $preview_url ) { |
| 792 | 938 | $identity['previewUrl'] = $preview_url; |
| 793 | 939 | } |
| 794 | 940 | |
| 941 | + // The reason this recompute exists at all, for revisions: the FIRST | |
| 942 | + // save of a draft is what creates its first revision, so the "View | |
| 943 | + // revisions" row can only appear after a save — and a block-editor | |
| 944 | + // save never reloads the page. | |
| 945 | + $revisions = openstation_window_revisions( $post ); | |
| 946 | + if ( '' !== $revisions['url'] ) { | |
| 947 | + $identity['revisionsUrl'] = $revisions['url']; | |
| 948 | + $identity['revisionCount'] = $revisions['count']; | |
| 949 | + } | |
| 950 | + | |
| 795 | 951 | /** This filter is documented in includes/window-links.php */ |
| 796 | 952 | $identity = apply_filters( 'openstation_window_content_identity', $identity, null ); |
| 797 | 953 | $identity = openstation_window_related_attach( $identity, $post, null ); |
| 798 | 954 | |
| @@ -930,8 +1086,11 @@ | ||
| 930 | 1086 | 'scriptBefore' => $payload['before'], |
| 931 | 1087 | 'scriptAfter' => $payload['after'], |
| 932 | 1088 | 'scriptL10n' => $payload['l10n'], |
| 933 | 1089 | 'scriptTranslations' => $payload['translations'], |
| 1090 | + // The handle's dependency closure, replayed before the bundle | |
| 1091 | + // on its lazy load — see `openstation_resolve_script_dependencies()`. | |
| 1092 | + 'scriptDeps' => openstation_resolve_script_dependencies( $handle ), | |
| 934 | 1093 | ); |
| 935 | 1094 | $seen[ $handle ] = true; |
| 936 | 1095 | } |
| 937 | 1096 | return $out; |