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/window-links.php +175 -2 1.1.0 → 1.1.12 View file →
@@ -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 *
@@ -59,9 +65,9 @@
59 65 if ( $comment ) {
60 66 $identity = array(
61 67 'type' => 'comment',
62 68 'id' => (int) $comment->comment_ID,
63 - 'label' => wp_trim_words( $comment->comment_content, 10 ),
69 + 'label' => wp_trim_words( openstation_strip_all_tags( $comment->comment_content ), 10 ),
64 70 );
65 71
66 72 $post_id = (int) $comment->comment_post_ID;
67 73 $post_type = $post_id ? get_post_type( $post_id ) : false;
@@ -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,12 +327,118 @@
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 - * save-watcher hits (where `$screen` is `null`).
437 + * save-watcher hits (where `$screen` is `null`). Also where the labels
438 + * become plain text: they name windows (Preview, Revisions, Related)
439 + * and are painted as text, where `get_the_title()`'s entities
440 + * (`’`) read literally.
292 441 *
293 442 * @internal
294 443 *
295 444 * @param array|null $identity Filtered identity, or `null`.
@@ -302,8 +451,11 @@
302 451 function openstation_window_related_attach( $identity, $post, $screen ) {
303 452 if ( ! is_array( $identity ) ) {
304 453 return $identity;
305 454 }
455 + if ( isset( $identity['label'] ) && is_string( $identity['label'] ) ) {
456 + $identity['label'] = openstation_plain_text_title( $identity['label'] );
457 + }
306 458
307 459 $related = array();
308 460 if (
309 461 $post instanceof WP_Post &&
@@ -668,8 +820,16 @@
668 820 foreach ( $related as $item ) {
669 821 if ( ! is_array( $item ) ) {
670 822 continue;
671 823 }
824 + // Plain text (see `openstation_window_related_attach()`),
825 + // decoded before the checks below so a label that was only
826 + // markup is dropped here rather than failing the whole ref.
827 + foreach ( array( 'label', 'groupLabel' ) as $text ) {
828 + if ( isset( $item[ $text ] ) && is_string( $item[ $text ] ) ) {
829 + $item[ $text ] = openstation_plain_text_title( $item[ $text ] );
830 + }
831 + }
672 832 foreach ( array( 'id', 'group', 'label', 'url' ) as $required ) {
673 833 // Mirror the JS engine's validation exactly (`.trim() !== ''`):
674 834 // a whitespace-only value passing here would fail validateRef
675 835 // client-side, which rejects the ref AS A UNIT — one bad item
@@ -791,8 +951,18 @@
791 951 if ( '' !== $preview_url ) {
792 952 $identity['previewUrl'] = $preview_url;
793 953 }
794 954
955 + // The reason this recompute exists at all, for revisions: the FIRST
956 + // save of a draft is what creates its first revision, so the "View
957 + // revisions" row can only appear after a save — and a block-editor
958 + // save never reloads the page.
959 + $revisions = openstation_window_revisions( $post );
960 + if ( '' !== $revisions['url'] ) {
961 + $identity['revisionsUrl'] = $revisions['url'];
962 + $identity['revisionCount'] = $revisions['count'];
963 + }
964 +
795 965 /** This filter is documented in includes/window-links.php */
796 966 $identity = apply_filters( 'openstation_window_content_identity', $identity, null );
797 967 $identity = openstation_window_related_attach( $identity, $post, null );
798 968
@@ -930,8 +1100,11 @@
930 1100 'scriptBefore' => $payload['before'],
931 1101 'scriptAfter' => $payload['after'],
932 1102 'scriptL10n' => $payload['l10n'],
933 1103 'scriptTranslations' => $payload['translations'],
1104 + // The handle's dependency closure, replayed before the bundle
1105 + // on its lazy load — see `openstation_resolve_script_dependencies()`.
1106 + 'scriptDeps' => openstation_resolve_script_dependencies( $handle ),
934 1107 );
935 1108 $seen[ $handle ] = true;
936 1109 }
937 1110 return $out;