| @@ -12,12 +12,11 @@ | ||
| 12 | 12 | * It runs inside the chromeless iframe request — real admin context, |
| 13 | 13 | * where `get_current_screen()` and the content globals are live — so |
| 14 | 14 | * relations the URL alone can't answer (which post a comment belongs |
| 15 | 15 | * to) resolve server-side and reach the shell via the chromeless |
| 16 | - * bridge's `desktop-mode-content-identity` postMessage. | |
| 16 | + * bridge's `os-content-identity` postMessage. | |
| 17 | 17 | * |
| 18 | - * @since 0.9.4 | |
| 19 | - * @package WPDesktopMode | |
| 18 | + * @package OpenStation | |
| 20 | 19 | */ |
| 21 | 20 | |
| 22 | 21 | defined( 'ABSPATH' ) || exit; |
| 23 | 22 | |
| @@ -41,14 +40,21 @@ | ||
| 41 | 40 | * `post_parent` when attached. |
| 42 | 41 | * - `comment.php` (comment edit / moderation) — `comment`, rooted at |
| 43 | 42 | * the parent post. The URL alone can't answer this one; only real |
| 44 | 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. | |
| 50 | + * - `user-edit.php` / `profile.php` — `user`, a root identity. What | |
| 51 | + * points at a person (a post's author, an order's customer) does so | |
| 52 | + * from its own `links`. | |
| 45 | 53 | * |
| 46 | - * @since 0.9.4 | |
| 47 | - * | |
| 48 | 54 | * @return array|null Identity array, or `null` when none applies. |
| 49 | 55 | */ |
| 50 | -function desktop_mode_build_content_identity() { | |
| 56 | +function openstation_build_content_identity() { | |
| 51 | 57 | $identity = null; |
| 52 | 58 | $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; |
| 53 | 59 | $pagenow = isset( $GLOBALS['pagenow'] ) ? (string) $GLOBALS['pagenow'] : ''; |
| 54 | 60 | |
| @@ -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 ) { |
| @@ -101,13 +138,24 @@ | ||
| 101 | 138 | // media, and assigned terms. When a window showing a |
| 102 | 139 | // referenced object is open, the shell draws a directed |
| 103 | 140 | // tie toward it (mutual links collapse into one |
| 104 | 141 | // bidirectional arrow). |
| 105 | - $links = desktop_mode_window_links_extract_references( $post ); | |
| 142 | + $links = openstation_window_links_extract_references( $post ); | |
| 106 | 143 | if ( ! empty( $links ) ) { |
| 107 | 144 | $identity['links'] = $links; |
| 108 | 145 | } |
| 109 | 146 | |
| 147 | + $preview_url = openstation_window_preview_url( $post ); | |
| 148 | + if ( '' !== $preview_url ) { | |
| 149 | + $identity['previewUrl'] = $preview_url; | |
| 150 | + } | |
| 151 | + | |
| 152 | + $revisions = openstation_window_revisions( $post ); | |
| 153 | + if ( '' !== $revisions['url'] ) { | |
| 154 | + $identity['revisionsUrl'] = $revisions['url']; | |
| 155 | + $identity['revisionCount'] = $revisions['count']; | |
| 156 | + } | |
| 157 | + | |
| 110 | 158 | // Source for the built-in related-entity items attached |
| 111 | 159 | // after the identity filter below. |
| 112 | 160 | $related_source_post = $post; |
| 113 | 161 | } |
| @@ -175,8 +223,24 @@ | ||
| 175 | 223 | 'id' => (int) $term->term_id, |
| 176 | 224 | 'label' => $term->name, |
| 177 | 225 | ); |
| 178 | 226 | } |
| 227 | + } elseif ( 'user-edit.php' === $pagenow || 'profile.php' === $pagenow ) { | |
| 228 | + // Profile editor — `user-edit.php?user_id=N`, or `profile.php` | |
| 229 | + // for your own. A person is its own root: everything that | |
| 230 | + // points AT them (an order's customer, a post's author) does | |
| 231 | + // so through its identity's `links`, so an open profile window | |
| 232 | + // ties to whatever else on the desktop is about them. | |
| 233 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only identity harvest; the host admin page enforces capability + nonce. | |
| 234 | + $user_id = isset( $_GET['user_id'] ) ? absint( $_GET['user_id'] ) : get_current_user_id(); | |
| 235 | + $user = $user_id ? get_userdata( $user_id ) : null; | |
| 236 | + if ( $user instanceof WP_User && current_user_can( 'edit_user', $user->ID ) ) { | |
| 237 | + $identity = array( | |
| 238 | + 'type' => 'user', | |
| 239 | + 'id' => (int) $user->ID, | |
| 240 | + 'label' => $user->display_name ? $user->display_name : $user->user_login, | |
| 241 | + ); | |
| 242 | + } | |
| 179 | 243 | } |
| 180 | 244 | |
| 181 | 245 | /** |
| 182 | 246 | * Filters the content identity announced for the current admin screen. |
| @@ -186,14 +250,12 @@ | ||
| 186 | 250 | * built-in detection. The shape must match the JS |
| 187 | 251 | * `WindowContentRef`: `type` (lowercase slug), `id` (int|string), |
| 188 | 252 | * optional `label`, optional `root => array( 'type', 'id' )`. |
| 189 | 253 | * |
| 190 | - * @since 0.9.4 | |
| 191 | - * | |
| 192 | 254 | * @param array|null $identity Identity array, or `null` for none. |
| 193 | 255 | * @param WP_Screen|null $screen The current screen, when available. |
| 194 | 256 | */ |
| 195 | - $identity = apply_filters( 'desktop_mode_window_content_identity', $identity, $screen ); | |
| 257 | + $identity = apply_filters( 'openstation_window_content_identity', $identity, $screen ); | |
| 196 | 258 | |
| 197 | 259 | // Related-entity navigation targets — what the title bar's |
| 198 | 260 | // "Related" button lists. Runs AFTER the identity filter so |
| 199 | 261 | // plugin-injected identities for custom screens get the related |
| @@ -198,9 +260,9 @@ | ||
| 198 | 260 | // "Related" button lists. Runs AFTER the identity filter so |
| 199 | 261 | // plugin-injected identities for custom screens get the related |
| 200 | 262 | // filter too, and only for a resolved identity: no identity, no |
| 201 | 263 | // related menu. |
| 202 | - return desktop_mode_window_related_attach( | |
| 264 | + return openstation_window_related_attach( | |
| 203 | 265 | $identity, |
| 204 | 266 | isset( $related_source_post ) && $related_source_post instanceof WP_Post ? $related_source_post : null, |
| 205 | 267 | $screen |
| 206 | 268 | ); |
| @@ -206,14 +268,175 @@ | ||
| 206 | 268 | ); |
| 207 | 269 | } |
| 208 | 270 | |
| 209 | 271 | /** |
| 272 | + * Build the front-end preview URL for a post — the target of the | |
| 273 | + * shell's "Preview" (eye) title-bar button. | |
| 274 | + * | |
| 275 | + * Wraps `get_preview_post_link()` with the same query args core's own | |
| 276 | + * `post_preview()` passes (`preview_id` + a `post_preview_{ID}` nonce), | |
| 277 | + * so `_set_preview()` swaps in the newest autosave revision on the | |
| 278 | + * front end. Required for published/scheduled/private posts, whose | |
| 279 | + * autosaves land in a revision; harmless for drafts, where autosave | |
| 280 | + * writes the post itself and `_set_preview()` no-ops. | |
| 281 | + * | |
| 282 | + * Nonce freshness is bounded by save cadence: the content identity is | |
| 283 | + * rebuilt on every chromeless page render AND on the editor | |
| 284 | + * save-watcher's REST recompute, so a long-lived editor window always | |
| 285 | + * holds a recent nonce. | |
| 286 | + * | |
| 287 | + * @param WP_Post $post The post being edited. | |
| 288 | + * @return string Preview URL, or `''` when the post has no front-end | |
| 289 | + * preview (non-viewable type, attachment, insufficient | |
| 290 | + * capability) or a filter suppressed it. | |
| 291 | + */ | |
| 292 | +function openstation_window_preview_url( $post ) { | |
| 293 | + $preview_url = ''; | |
| 294 | + | |
| 295 | + if ( | |
| 296 | + $post instanceof WP_Post && | |
| 297 | + $post->ID > 0 && | |
| 298 | + 'attachment' !== $post->post_type && | |
| 299 | + is_post_type_viewable( get_post_type_object( $post->post_type ) ) && | |
| 300 | + current_user_can( 'edit_post', $post->ID ) | |
| 301 | + ) { | |
| 302 | + $link = get_preview_post_link( | |
| 303 | + $post, | |
| 304 | + array( | |
| 305 | + 'preview_id' => $post->ID, | |
| 306 | + 'preview_nonce' => wp_create_nonce( 'post_preview_' . $post->ID ), | |
| 307 | + ) | |
| 308 | + ); | |
| 309 | + if ( is_string( $link ) ) { | |
| 310 | + $preview_url = $link; | |
| 311 | + } | |
| 312 | + } | |
| 313 | + | |
| 314 | + /** | |
| 315 | + * Filters the front-end preview URL attached to a post-editor | |
| 316 | + * content identity (the target of the shell's "Preview" eye | |
| 317 | + * title-bar button). | |
| 318 | + * | |
| 319 | + * Return `''` to suppress the preview button for this post, or | |
| 320 | + * rewrite the URL to point somewhere else (a headless front end, | |
| 321 | + * a staging domain). Note the shell only accepts same-origin URLs; | |
| 322 | + * a cross-origin rewrite hides the button. | |
| 323 | + * | |
| 324 | + * @param string $preview_url Preview URL, `''` when none applies. | |
| 325 | + * @param WP_Post $post The post being edited. | |
| 326 | + */ | |
| 327 | + return (string) apply_filters( 'openstation_window_preview_url', $preview_url, $post ); | |
| 328 | +} | |
| 329 | + | |
| 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 | +/** | |
| 210 | 434 | * The related-entity pass: attach the `related` navigation items to a |
| 211 | 435 | * (post-identity-filter) content identity. Shared by the page-render |
| 212 | 436 | * builder above and the REST recompute endpoint the editor |
| 213 | 437 | * save-watcher hits (where `$screen` is `null`). |
| 214 | 438 | * |
| 215 | - * @since 0.9.6 | |
| 216 | 439 | * @internal |
| 217 | 440 | * |
| 218 | 441 | * @param array|null $identity Filtered identity, or `null`. |
| 219 | 442 | * @param WP_Post|null $post The detected source post, when the |
| @@ -221,9 +444,9 @@ | ||
| 221 | 444 | * @param WP_Screen|null $screen The current screen, when available. |
| 222 | 445 | * @return array|null The identity with `related` attached (or the |
| 223 | 446 | * input untouched when it was `null`). |
| 224 | 447 | */ |
| 225 | -function desktop_mode_window_related_attach( $identity, $post, $screen ) { | |
| 448 | +function openstation_window_related_attach( $identity, $post, $screen ) { | |
| 226 | 449 | if ( ! is_array( $identity ) ) { |
| 227 | 450 | return $identity; |
| 228 | 451 | } |
| 229 | 452 | |
| @@ -239,9 +462,9 @@ | ||
| 239 | 462 | isset( $identity['type'], $identity['id'] ) && |
| 240 | 463 | sanitize_key( $post->post_type ) === $identity['type'] && |
| 241 | 464 | (int) $post->ID === (int) $identity['id'] |
| 242 | 465 | ) { |
| 243 | - $related = desktop_mode_window_related_entities_for_post( $post ); | |
| 466 | + $related = openstation_window_related_entities_for_post( $post ); | |
| 244 | 467 | } |
| 245 | 468 | if ( isset( $identity['related'] ) && is_array( $identity['related'] ) ) { |
| 246 | 469 | // An identity filter may ship related items with its own |
| 247 | 470 | // identity — fold them in so they reach the related filter |
| @@ -278,16 +501,14 @@ | ||
| 278 | 501 | * Runs during the chromeless page render AND on the |
| 279 | 502 | * `desktop-mode/v1/content-identity` REST recompute the editor |
| 280 | 503 | * save-watcher triggers — in the REST context `$screen` is `null`. |
| 281 | 504 | * |
| 282 | - * @since 0.9.6 | |
| 283 | - * | |
| 284 | 505 | * @param array[] $related Related-entity items. |
| 285 | 506 | * @param array $identity The resolved content identity. |
| 286 | 507 | * @param WP_Screen|null $screen The current screen, when available. |
| 287 | 508 | */ |
| 288 | - $related = apply_filters( 'desktop_mode_window_related_entities', $related, $identity, $screen ); | |
| 289 | - $related = desktop_mode_window_related_entities_sanitize( $related ); | |
| 509 | + $related = apply_filters( 'openstation_window_related_entities', $related, $identity, $screen ); | |
| 510 | + $related = openstation_window_related_entities_sanitize( $related ); | |
| 290 | 511 | // The related pass is the single authority over the key — an |
| 291 | 512 | // identity filter smuggling its own `related` would bypass the |
| 292 | 513 | // sanitizer above. |
| 293 | 514 | unset( $identity['related'] ); |
| @@ -317,14 +538,12 @@ | ||
| 317 | 538 | * |
| 318 | 539 | * Deduped by type:id and capped so a link-farm post can't flood the |
| 319 | 540 | * shell. |
| 320 | 541 | * |
| 321 | - * @since 0.9.4 | |
| 322 | - * | |
| 323 | 542 | * @param WP_Post $post Source post. |
| 324 | 543 | * @return array[] Reference entries, possibly empty. |
| 325 | 544 | */ |
| 326 | -function desktop_mode_window_links_extract_references( $post ) { | |
| 545 | +function openstation_window_links_extract_references( $post ) { | |
| 327 | 546 | $links = array(); |
| 328 | 547 | $seen = array(); |
| 329 | 548 | $push = static function ( $type, $id, $rel = '' ) use ( &$links, &$seen ) { |
| 330 | 549 | $key = $type . ':' . $id; |
| @@ -346,10 +565,10 @@ | ||
| 346 | 565 | }; |
| 347 | 566 | |
| 348 | 567 | // 1. Internal hyperlinks → posts. Guarded: the content-graph |
| 349 | 568 | // extractor lives in a separate include. |
| 350 | - if ( function_exists( 'desktop_mode_content_graph_extract_internal_links' ) ) { | |
| 351 | - $ids = desktop_mode_content_graph_extract_internal_links( (string) $post->post_content ); | |
| 569 | + if ( function_exists( 'openstation_content_graph_extract_internal_links' ) ) { | |
| 570 | + $ids = openstation_content_graph_extract_internal_links( (string) $post->post_content ); | |
| 352 | 571 | foreach ( array_slice( $ids, 0, 32 ) as $target_id ) { |
| 353 | 572 | $target_id = (int) $target_id; |
| 354 | 573 | if ( $target_id === (int) $post->ID ) { |
| 355 | 574 | continue; |
| @@ -422,16 +641,14 @@ | ||
| 422 | 641 | * are excluded. |
| 423 | 642 | * |
| 424 | 643 | * Built-ins deliberately cover `post` and `page` only; other post |
| 425 | 644 | * types (and non-post screens) join via the |
| 426 | - * `desktop_mode_window_related_entities` filter. | |
| 645 | + * `openstation_window_related_entities` filter. | |
| 427 | 646 | * |
| 428 | - * @since 0.9.6 | |
| 429 | - * | |
| 430 | 647 | * @param WP_Post $post Source post. |
| 431 | 648 | * @return array[] Related-entity items, possibly empty. |
| 432 | 649 | */ |
| 433 | -function desktop_mode_window_related_entities_for_post( $post ) { | |
| 650 | +function openstation_window_related_entities_for_post( $post ) { | |
| 434 | 651 | if ( ! $post instanceof WP_Post || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) { |
| 435 | 652 | return array(); |
| 436 | 653 | } |
| 437 | 654 | |
| @@ -539,10 +756,10 @@ | ||
| 539 | 756 | // 4. Linked posts — internal hyperlinks resolving to another post |
| 540 | 757 | // on this site. Guarded: the extractor lives in the content-graph |
| 541 | 758 | // include. Capped tighter than the reference extractor (10) to |
| 542 | 759 | // stay inside the overall 64-item engine budget. |
| 543 | - if ( function_exists( 'desktop_mode_content_graph_extract_internal_links' ) ) { | |
| 544 | - $link_ids = desktop_mode_content_graph_extract_internal_links( (string) $post->post_content ); | |
| 760 | + if ( function_exists( 'openstation_content_graph_extract_internal_links' ) ) { | |
| 761 | + $link_ids = openstation_content_graph_extract_internal_links( (string) $post->post_content ); | |
| 545 | 762 | $count = 0; |
| 546 | 763 | foreach ( $link_ids as $target_id ) { |
| 547 | 764 | if ( $count >= 10 ) { |
| 548 | 765 | break; |
| @@ -577,20 +794,19 @@ | ||
| 577 | 794 | |
| 578 | 795 | /** |
| 579 | 796 | * Drop malformed related-entity items and whitelist their fields. |
| 580 | 797 | * |
| 581 | - * Runs on the `desktop_mode_window_related_entities` filter output | |
| 798 | + * Runs on the `openstation_window_related_entities` filter output | |
| 582 | 799 | * before the payload is announced: a plugin returning one bad entry |
| 583 | 800 | * must not invalidate the whole identity client-side (the JS engine |
| 584 | 801 | * validates the ref as a unit and would discard everything). |
| 585 | 802 | * |
| 586 | - * @since 0.9.6 | |
| 587 | 803 | * @internal |
| 588 | 804 | * |
| 589 | 805 | * @param mixed $related Filter output. |
| 590 | 806 | * @return array[] Well-formed items, reindexed. |
| 591 | 807 | */ |
| 592 | -function desktop_mode_window_related_entities_sanitize( $related ) { | |
| 808 | +function openstation_window_related_entities_sanitize( $related ) { | |
| 593 | 809 | if ( ! is_array( $related ) ) { |
| 594 | 810 | return array(); |
| 595 | 811 | } |
| 596 | 812 | |
| @@ -640,23 +856,21 @@ | ||
| 640 | 856 | * without reloading, so the page-render announcement alone would go |
| 641 | 857 | * stale the moment the user adds a category or an image) and |
| 642 | 858 | * re-announces the fresh identity to the parent shell. |
| 643 | 859 | * |
| 644 | - * Both public filters (`desktop_mode_window_content_identity`, | |
| 645 | - * `desktop_mode_window_related_entities`) run here exactly as they | |
| 860 | + * Both public filters (`openstation_window_content_identity`, | |
| 861 | + * `openstation_window_related_entities`) run here exactly as they | |
| 646 | 862 | * do at page render, with `$screen = null` — there is no WP_Screen |
| 647 | 863 | * in REST context. |
| 648 | - * | |
| 649 | - * @since 0.9.6 | |
| 650 | 864 | */ |
| 651 | -function desktop_mode_register_content_identity_route() { | |
| 865 | +function openstation_register_content_identity_route() { | |
| 652 | 866 | register_rest_route( |
| 653 | 867 | 'desktop-mode/v1', |
| 654 | 868 | '/content-identity', |
| 655 | 869 | array( |
| 656 | 870 | 'methods' => 'GET', |
| 657 | - 'callback' => 'desktop_mode_rest_content_identity', | |
| 658 | - 'permission_callback' => 'desktop_mode_rest_content_identity_permission', | |
| 871 | + 'callback' => 'openstation_rest_content_identity', | |
| 872 | + 'permission_callback' => 'openstation_rest_content_identity_permission', | |
| 659 | 873 | 'args' => array( |
| 660 | 874 | 'post' => array( |
| 661 | 875 | 'description' => __( 'Post ID to recompute the content identity for.', 'desktop-mode' ), |
| 662 | 876 | 'type' => 'integer', |
| @@ -666,22 +880,20 @@ | ||
| 666 | 880 | ), |
| 667 | 881 | ) |
| 668 | 882 | ); |
| 669 | 883 | } |
| 670 | -add_action( 'rest_api_init', 'desktop_mode_register_content_identity_route' ); | |
| 884 | +add_action( 'rest_api_init', 'openstation_register_content_identity_route' ); | |
| 671 | 885 | |
| 672 | 886 | /** |
| 673 | - * Permission: desktop mode enabled AND the caller can edit the post — | |
| 887 | + * Permission: OpenStation enabled AND the caller can edit the post — | |
| 674 | 888 | * the identity carries the post title, term names, and media labels, |
| 675 | 889 | * which is exactly what the edit screen itself exposes. |
| 676 | 890 | * |
| 677 | - * @since 0.9.6 | |
| 678 | - * | |
| 679 | 891 | * @param WP_REST_Request $request REST request. |
| 680 | 892 | * @return true|WP_Error |
| 681 | 893 | */ |
| 682 | -function desktop_mode_rest_content_identity_permission( $request ) { | |
| 683 | - $enabled = desktop_mode_rest_require_enabled(); | |
| 894 | +function openstation_rest_content_identity_permission( $request ) { | |
| 895 | + $enabled = openstation_rest_require_enabled(); | |
| 684 | 896 | if ( true !== $enabled ) { |
| 685 | 897 | return $enabled; |
| 686 | 898 | } |
| 687 | 899 | if ( ! current_user_can( 'edit_post', (int) $request['post'] ) ) { |
| @@ -697,18 +909,16 @@ | ||
| 697 | 909 | /** |
| 698 | 910 | * REST handler — rebuild the post-editor identity the same way the |
| 699 | 911 | * page-render builder's `post.php` branch does, filters included. |
| 700 | 912 | * |
| 701 | - * @since 0.9.6 | |
| 702 | - * | |
| 703 | 913 | * @param WP_REST_Request $request REST request. |
| 704 | 914 | * @return WP_REST_Response|WP_Error |
| 705 | 915 | */ |
| 706 | -function desktop_mode_rest_content_identity( $request ) { | |
| 916 | +function openstation_rest_content_identity( $request ) { | |
| 707 | 917 | $post = get_post( (int) $request['post'] ); |
| 708 | 918 | if ( ! $post instanceof WP_Post || 'attachment' === $post->post_type ) { |
| 709 | 919 | return new WP_Error( |
| 710 | - 'desktop_mode_no_identity', | |
| 920 | + 'openstation_no_identity', | |
| 711 | 921 | __( 'No content identity for this object.', 'desktop-mode' ), |
| 712 | 922 | array( 'status' => 404 ) |
| 713 | 923 | ); |
| 714 | 924 | } |
| @@ -717,16 +927,31 @@ | ||
| 717 | 927 | 'type' => sanitize_key( $post->post_type ), |
| 718 | 928 | 'id' => (int) $post->ID, |
| 719 | 929 | 'label' => get_the_title( $post ), |
| 720 | 930 | ); |
| 721 | - $links = desktop_mode_window_links_extract_references( $post ); | |
| 931 | + $links = openstation_window_links_extract_references( $post ); | |
| 722 | 932 | if ( ! empty( $links ) ) { |
| 723 | 933 | $identity['links'] = $links; |
| 724 | 934 | } |
| 725 | 935 | |
| 936 | + $preview_url = openstation_window_preview_url( $post ); | |
| 937 | + if ( '' !== $preview_url ) { | |
| 938 | + $identity['previewUrl'] = $preview_url; | |
| 939 | + } | |
| 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 | + | |
| 726 | 951 | /** This filter is documented in includes/window-links.php */ |
| 727 | - $identity = apply_filters( 'desktop_mode_window_content_identity', $identity, null ); | |
| 728 | - $identity = desktop_mode_window_related_attach( $identity, $post, null ); | |
| 952 | + $identity = apply_filters( 'openstation_window_content_identity', $identity, null ); | |
| 953 | + $identity = openstation_window_related_attach( $identity, $post, null ); | |
| 729 | 954 | |
| 730 | 955 | return rest_ensure_response( array( 'identity' => $identity ) ); |
| 731 | 956 | } |
| 732 | 957 | |
| @@ -741,9 +966,9 @@ | ||
| 741 | 966 | * surfaces its renderer in OS Settings → Effects → Window links |
| 742 | 967 | * immediately, no F5 needed. |
| 743 | 968 | * |
| 744 | 969 | * Renderers themselves are declared JS-side via |
| 745 | - * `wp.desktop.registerWindowLinkRenderer( … )` — the mount callback | |
| 970 | + * `wp.os.registerWindowLinkRenderer( … )` — the mount callback | |
| 746 | 971 | * and label live in the plugin's JavaScript. The built-in |
| 747 | 972 | * `svg-splines` is registered through the very same JS hook (see |
| 748 | 973 | * `src/window-links/renderers/svg-splines.ts`). |
| 749 | 974 | * |
| @@ -753,15 +978,15 @@ | ||
| 753 | 978 | * add_action( 'admin_enqueue_scripts', function () { |
| 754 | 979 | * wp_register_script( |
| 755 | 980 | * 'my-plugin-link-renderer', |
| 756 | 981 | * plugins_url( 'js/link-renderer.js', __FILE__ ), |
| 757 | - * array( 'desktop-mode' ), | |
| 982 | + * array( 'openstation' ), | |
| 758 | 983 | * '1.0.0', |
| 759 | 984 | * true |
| 760 | 985 | * ); |
| 761 | 986 | * wp_enqueue_script( 'my-plugin-link-renderer' ); |
| 762 | 987 | * } ); |
| 763 | - * desktop_mode_register_window_link_renderer_script( 'my-plugin-link-renderer' ); | |
| 988 | + * openstation_register_window_link_renderer_script( 'my-plugin-link-renderer' ); | |
| 764 | 989 | * ``` |
| 765 | 990 | * |
| 766 | 991 | * For live unregistration on deactivation, the plugin's JS should set |
| 767 | 992 | * `owner: 'my-plugin-link-renderer'` on each |
| @@ -767,32 +992,28 @@ | ||
| 767 | 992 | * `owner: 'my-plugin-link-renderer'` on each |
| 768 | 993 | * `registerWindowLinkRenderer` call. Otherwise the renderer stays |
| 769 | 994 | * until the next page reload — graceful backwards-compat. |
| 770 | 995 | * |
| 771 | - * @since 0.9.4 | |
| 772 | - * | |
| 773 | 996 | * @param string $handle WP-registered script handle. |
| 774 | 997 | * @return true|WP_Error `true` on success; `WP_Error` on validation failure. |
| 775 | 998 | */ |
| 776 | -function desktop_mode_register_window_link_renderer_script( $handle ) { | |
| 999 | +function openstation_register_window_link_renderer_script( $handle ) { | |
| 777 | 1000 | $handle = (string) $handle; |
| 778 | 1001 | if ( '' === $handle ) { |
| 779 | - return desktop_mode_registration_error( | |
| 780 | - 'desktop_mode_missing_handle', | |
| 1002 | + return openstation_registration_error( | |
| 1003 | + 'openstation_missing_handle', | |
| 781 | 1004 | __( 'Window-link renderer script registration requires a non-empty script handle.', 'desktop-mode' ) |
| 782 | 1005 | ); |
| 783 | 1006 | } |
| 784 | 1007 | |
| 785 | - desktop_mode_window_link_renderer_script_registry( $handle, true ); | |
| 1008 | + openstation_window_link_renderer_script_registry( $handle, true ); | |
| 786 | 1009 | |
| 787 | 1010 | /** |
| 788 | 1011 | * Fires after a window-link renderer script handle is registered. |
| 789 | 1012 | * |
| 790 | - * @since 0.9.4 | |
| 791 | - * | |
| 792 | 1013 | * @param string $handle The registered script handle. |
| 793 | 1014 | */ |
| 794 | - do_action( 'desktop_mode_window_link_renderer_script_registered', $handle ); | |
| 1015 | + do_action( 'openstation_window_link_renderer_script_registered', $handle ); | |
| 795 | 1016 | |
| 796 | 1017 | return true; |
| 797 | 1018 | } |
| 798 | 1019 | |
| @@ -798,9 +1019,8 @@ | ||
| 798 | 1019 | |
| 799 | 1020 | /** |
| 800 | 1021 | * Internal module-level registry for window-link renderer script handles. |
| 801 | 1022 | * |
| 802 | - * @since 0.9.4 | |
| 803 | 1023 | * @internal |
| 804 | 1024 | * |
| 805 | 1025 | * @param string $handle Script handle to read or write. |
| 806 | 1026 | * @param bool|null $value Pass `true` to register; `null` to read only. |
| @@ -805,9 +1025,9 @@ | ||
| 805 | 1025 | * @param string $handle Script handle to read or write. |
| 806 | 1026 | * @param bool|null $value Pass `true` to register; `null` to read only. |
| 807 | 1027 | * @return array|bool When called with no args returns the full store. |
| 808 | 1028 | */ |
| 809 | -function desktop_mode_window_link_renderer_script_registry( $handle = '', $value = null ) { | |
| 1029 | +function openstation_window_link_renderer_script_registry( $handle = '', $value = null ) { | |
| 810 | 1030 | static $store = array(); |
| 811 | 1031 | |
| 812 | 1032 | if ( '__flush__' === (string) $handle ) { |
| 813 | 1033 | $store = array(); |
| @@ -823,14 +1043,12 @@ | ||
| 823 | 1043 | } |
| 824 | 1044 | |
| 825 | 1045 | /** |
| 826 | 1046 | * Test-only: clear the registry between PHPUnit cases. See |
| 827 | - * {@see desktop_mode_flush_script_handle_registries()}. | |
| 828 | - * | |
| 829 | - * @since 0.9.4 | |
| 1047 | + * {@see openstation_flush_script_handle_registries()}. | |
| 830 | 1048 | */ |
| 831 | -function desktop_mode_flush_window_link_renderer_script_registry() { | |
| 832 | - desktop_mode_window_link_renderer_script_registry( '__flush__' ); | |
| 1049 | +function openstation_flush_window_link_renderer_script_registry() { | |
| 1050 | + openstation_window_link_renderer_script_registry( '__flush__' ); | |
| 833 | 1051 | } |
| 834 | 1052 | |
| 835 | 1053 | /** |
| 836 | 1054 | * Build the script-handle payload fed to the shell. Handles that |
| @@ -835,14 +1053,12 @@ | ||
| 835 | 1053 | /** |
| 836 | 1054 | * Build the script-handle payload fed to the shell. Handles that |
| 837 | 1055 | * aren't currently enqueued resolve to an empty URL and are dropped. |
| 838 | 1056 | * |
| 839 | - * @since 0.9.4 | |
| 840 | - * | |
| 841 | 1057 | * @return array[] List of `{ handle, scriptUrl, … }` entries. |
| 842 | 1058 | */ |
| 843 | -function desktop_mode_build_window_link_renderer_scripts_payload() { | |
| 844 | - $registry = desktop_mode_window_link_renderer_script_registry(); | |
| 1059 | +function openstation_build_window_link_renderer_scripts_payload() { | |
| 1060 | + $registry = openstation_window_link_renderer_script_registry(); | |
| 845 | 1061 | if ( ! is_array( $registry ) || empty( $registry ) ) { |
| 846 | 1062 | return array(); |
| 847 | 1063 | } |
| 848 | 1064 | |
| @@ -851,15 +1067,15 @@ | ||
| 851 | 1067 | foreach ( $registry as $handle => $active ) { |
| 852 | 1068 | if ( ! $active || isset( $seen[ $handle ] ) ) { |
| 853 | 1069 | continue; |
| 854 | 1070 | } |
| 855 | - $payload = desktop_mode_resolve_script_payload( $handle ); | |
| 1071 | + $payload = openstation_resolve_script_payload( $handle ); | |
| 856 | 1072 | if ( '' === $payload['url'] ) { |
| 857 | 1073 | // Loud diagnostic — visible under WP_DEBUG. Deduped by |
| 858 | - // `desktop_mode_warn_unresolvable_script_handle` so the | |
| 1074 | + // `openstation_warn_unresolvable_script_handle` so the | |
| 859 | 1075 | // notice fires once per handle per request. |
| 860 | - desktop_mode_warn_unresolvable_script_handle( | |
| 861 | - 'desktop_mode_register_window_link_renderer_script', | |
| 1076 | + openstation_warn_unresolvable_script_handle( | |
| 1077 | + 'openstation_register_window_link_renderer_script', | |
| 862 | 1078 | 'Window-link renderer', |
| 863 | 1079 | (string) $handle |
| 864 | 1080 | ); |
| 865 | 1081 | continue; |
| @@ -870,8 +1086,11 @@ | ||
| 870 | 1086 | 'scriptBefore' => $payload['before'], |
| 871 | 1087 | 'scriptAfter' => $payload['after'], |
| 872 | 1088 | 'scriptL10n' => $payload['l10n'], |
| 873 | 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 ), | |
| 874 | 1093 | ); |
| 875 | 1094 | $seen[ $handle ] = true; |
| 876 | 1095 | } |
| 877 | 1096 | return $out; |