PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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 0.8.8 0.8.7 All 34 releases
← All changes | includes/content-graph/graph-builder.php +64 -68 0.9.31.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Content Graph: graph builder.
3 + * OpenStation — Content Graph: graph builder.
4 4 *
5 5 * Walks the WordPress post store to produce two arrays for the bundle:
6 6 *
7 7 * - `nodes` — one entry per post matching the requested types.
@@ -19,10 +19,9 @@
19 19 * also explicitly bust the cache on `save_post` and `deleted_post` so
20 20 * editors don't see stale data even if some other process pre-warms
21 21 * the transient.
22 22 *
23 - * @package WPDesktopMode
24 - * @since 0.8.2
23 + * @package OpenStation
25 24 */
26 25
27 26 defined( 'ABSPATH' ) || exit;
28 27
@@ -30,17 +29,22 @@
30 29 // transients still alive at upgrade time would otherwise return the
31 30 // previous schema (e.g. missing per-node `contributor_ids`) and
32 31 // surface as runtime errors on the client. Each bump is a one-time
33 32 // cache miss for every site that updates the plugin.
34 -const DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_PREFIX = 'desktop_mode_cg2_';
35 -const DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_TTL = 6 * HOUR_IN_SECONDS;
33 +/**
34 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
35 + * persisted or externally-visible identifier, so renaming it would
36 + * orphan data already written by live installs (or break a live
37 + * URL). The mismatch between this constant's name and its value is
38 + * deliberate — it is NOT a half-finished rename.
39 + */
40 +const OPENSTATION_CONTENT_GRAPH_TRANSIENT_PREFIX = 'desktop_mode_cg3_';
41 +const OPENSTATION_CONTENT_GRAPH_TRANSIENT_TTL = 6 * HOUR_IN_SECONDS;
36 42
37 43 /**
38 44 * Build (or reuse the cached version of) the graph payload for the
39 45 * requested post types.
40 46 *
41 - * @since 0.8.2
42 - *
43 47 * @param string[] $types Post type slugs. Filtered against the public
44 48 * post-type registry, attachments excluded.
45 49 * @return array{
46 50 * nodes: array<int, array{
@@ -58,10 +62,10 @@
58 62 * },
59 63 * stats: array{ nodes: int, edges: int, generated_at: int }
60 64 * }
61 65 */
62 -function desktop_mode_content_graph_build( array $types ) {
63 - $types = desktop_mode_content_graph_normalize_types( $types );
66 +function openstation_content_graph_build( array $types ) {
67 + $types = openstation_content_graph_normalize_types( $types );
64 68 if ( empty( $types ) ) {
65 69 return array(
66 70 'nodes' => array(),
67 71 'edges' => array(),
@@ -77,15 +81,15 @@
77 81 ),
78 82 );
79 83 }
80 84
81 - $cache_key = desktop_mode_content_graph_cache_key( $types );
85 + $cache_key = openstation_content_graph_cache_key( $types );
82 86 $cached = get_transient( $cache_key );
83 87 if ( is_array( $cached ) && isset( $cached['nodes'], $cached['edges'] ) ) {
84 88 return $cached;
85 89 }
86 90
87 - $rows = desktop_mode_content_graph_fetch_rows( $types );
91 + $rows = openstation_content_graph_fetch_rows( $types );
88 92
89 93 $post_ids = array();
90 94 foreach ( $rows as $row ) {
91 95 $post_ids[] = (int) $row->ID;
@@ -90,15 +94,17 @@
90 94 foreach ( $rows as $row ) {
91 95 $post_ids[] = (int) $row->ID;
92 96 }
93 97
94 - $terms_by_post = desktop_mode_content_graph_collect_post_terms( $post_ids );
98 + $terms_by_post = openstation_content_graph_collect_post_terms( $post_ids );
95 99 // Distinct revision authors per post — used to pull collaborator
96 100 // posts toward both the primary author's cluster AND the
97 101 // contributor's clusters when grouping by author. Bulk-queried so
98 102 // we don't N+1 `wp_get_post_revisions` per node.
99 - $contribs_by_post = desktop_mode_content_graph_collect_post_contributors( $post_ids );
103 + $contribs_by_post = openstation_content_graph_collect_post_contributors( $post_ids );
100 104
105 + $default_category = max( 1, (int) get_option( 'default_category', 1 ) );
106 +
101 107 $nodes = array();
102 108 $nodes_by_id = array();
103 109 $author_ids = array();
104 110 $cat_ids = array();
@@ -103,12 +109,12 @@
103 109 $author_ids = array();
104 110 $cat_ids = array();
105 111 $tag_ids = array();
106 112 foreach ( $rows as $row ) {
107 - $id = (int) $row->ID;
108 - $author_id = (int) $row->post_author;
109 - $year = 0;
110 - $year_month = '';
113 + $id = (int) $row->ID;
114 + $author_id = (int) $row->post_author;
115 + $year = 0;
116 + $year_month = '';
111 117 if ( ! empty( $row->post_date ) ) {
112 118 // Use post_date (site-local) rather than post_date_gmt for the
113 119 // "year published" / "year-month published" buckets — editors
114 120 // think in their own timezone.
@@ -117,12 +123,22 @@
117 123 }
118 124 $post_cats = isset( $terms_by_post[ $id ]['category'] )
119 125 ? $terms_by_post[ $id ]['category']
120 126 : array();
127 + // A category-supporting post with zero terms is what WP treats
128 + // as "in the default category" at authoring time (core auto-
129 + // assigns it on save for `post`). Mirror that here so such
130 + // posts group under the real default-category cluster instead
131 + // of the client's synthetic "Uncategorized" pseudo-cluster
132 + // (`cat:uncat`, kept client-side only as a stale-payload
133 + // fallback).
134 + if ( empty( $post_cats ) && is_object_in_taxonomy( $row->post_type, 'category' ) ) {
135 + $post_cats = array( $default_category );
136 + }
121 137 $post_tags = isset( $terms_by_post[ $id ]['post_tag'] )
122 138 ? $terms_by_post[ $id ]['post_tag']
123 139 : array();
124 - $contribs = isset( $contribs_by_post[ $id ] )
140 + $contribs = isset( $contribs_by_post[ $id ] )
125 141 ? $contribs_by_post[ $id ]
126 142 : array();
127 143 // Strip the primary author from the contributor list — the
128 144 // node carries that separately in `author_id`, and surfacing
@@ -138,9 +154,9 @@
138 154 )
139 155 );
140 156 }
141 157
142 - $node = array(
158 + $node = array(
143 159 'id' => $id,
144 160 'type' => (string) $row->post_type,
145 161 'title' => (string) get_the_title( $row ),
146 162 'status' => (string) $row->post_status,
@@ -171,11 +187,11 @@
171 187 }
172 188 }
173 189
174 190 $groups = array(
175 - 'authors' => desktop_mode_content_graph_format_author_catalog( array_keys( $author_ids ) ),
176 - 'categories' => desktop_mode_content_graph_format_term_catalog( array_keys( $cat_ids ), 'category' ),
177 - 'tags' => desktop_mode_content_graph_format_term_catalog( array_keys( $tag_ids ), 'post_tag' ),
191 + 'authors' => openstation_content_graph_format_author_catalog( array_keys( $author_ids ) ),
192 + 'categories' => openstation_content_graph_format_term_catalog( array_keys( $cat_ids ), 'category' ),
193 + 'tags' => openstation_content_graph_format_term_catalog( array_keys( $tag_ids ), 'post_tag' ),
178 194 );
179 195
180 196 $edges_seen = array();
181 197 $edges = array();
@@ -180,9 +196,9 @@
180 196 $edges_seen = array();
181 197 $edges = array();
182 198 foreach ( $rows as $row ) {
183 199 $from = (int) $row->ID;
184 - $tos = desktop_mode_content_graph_extract_internal_links( (string) $row->post_content );
200 + $tos = openstation_content_graph_extract_internal_links( (string) $row->post_content );
185 201 foreach ( $tos as $to ) {
186 202 if ( $to === $from ) {
187 203 continue;
188 204 }
@@ -214,9 +230,9 @@
214 230 'generated_at' => time(),
215 231 ),
216 232 );
217 233
218 - set_transient( $cache_key, $payload, DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_TTL );
234 + set_transient( $cache_key, $payload, OPENSTATION_CONTENT_GRAPH_TRANSIENT_TTL );
219 235
220 236 return $payload;
221 237 }
222 238
@@ -223,18 +239,16 @@
223 239 /**
224 240 * Filter, sanitize, and uniquify the requested type slugs against the
225 241 * public post-type registry. Returned slugs are guaranteed to exist
226 242 * AND to be among the slugs declared by
227 - * `desktop_mode_content_graph_post_types()`.
243 + * `openstation_content_graph_post_types()`.
228 244 *
229 - * @since 0.8.2
230 - *
231 245 * @param string[] $types
232 246 * @return string[]
233 247 */
234 -function desktop_mode_content_graph_normalize_types( array $types ) {
248 +function openstation_content_graph_normalize_types( array $types ) {
235 249 $allowed = array();
236 - foreach ( desktop_mode_content_graph_post_types() as $entry ) {
250 + foreach ( openstation_content_graph_post_types() as $entry ) {
237 251 if ( ! empty( $entry['slug'] ) ) {
238 252 $allowed[ (string) $entry['slug'] ] = true;
239 253 }
240 254 }
@@ -262,14 +276,12 @@
262 276 * The `key` element encodes the resulting privilege tier (and, when
263 277 * the own-author clause is active, the user id) so cached payloads
264 278 * are never served across privilege levels.
265 279 *
266 - * @since 0.9.2
267 - *
268 280 * @param string[] $types Already normalized.
269 281 * @return array{ where: string, values: array, key: string }
270 282 */
271 -function desktop_mode_content_graph_visibility_sql( array $types ) {
283 +function openstation_content_graph_visibility_sql( array $types ) {
272 284 $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
273 285 $values = $types;
274 286
275 287 $priv_types = array();
@@ -314,16 +326,14 @@
314 326 * relevant edit busts the cache implicitly, plus the viewer's
315 327 * privilege-tier signature so a payload built for a user who can read
316 328 * private posts is never served to one who can't (and vice versa).
317 329 *
318 - * @since 0.8.2
319 - *
320 330 * @param string[] $types Already normalized.
321 331 * @return string
322 332 */
323 -function desktop_mode_content_graph_cache_key( array $types ) {
333 +function openstation_content_graph_cache_key( array $types ) {
324 334 global $wpdb;
325 - $visibility = desktop_mode_content_graph_visibility_sql( $types );
335 + $visibility = openstation_content_graph_visibility_sql( $types );
326 336 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
327 337 $hash = (string) $wpdb->get_var(
328 338 $wpdb->prepare(
329 339 "SELECT MD5( GROUP_CONCAT( CONCAT( ID, ':', post_modified_gmt ) ORDER BY ID ) )
@@ -335,9 +345,9 @@
335 345 // phpcs:enable
336 346 if ( '' === $hash || null === $hash ) {
337 347 $hash = 'empty';
338 348 }
339 - return DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_PREFIX . substr( md5( implode( ',', $types ) . '|' . $visibility['key'] . '|' . $hash ), 0, 24 );
349 + return OPENSTATION_CONTENT_GRAPH_TRANSIENT_PREFIX . substr( md5( implode( ',', $types ) . '|' . $visibility['key'] . '|' . $hash ), 0, 24 );
340 350 }
341 351
342 352 /**
343 353 * Fetch the participating posts in a single query. Returns full rows
@@ -346,18 +356,16 @@
346 356 *
347 357 * Rows are scoped to what the current user can read: published posts,
348 358 * plus private posts only where the user holds the type's
349 359 * `read_private_posts` capability (or authored the post). See
350 - * `desktop_mode_content_graph_visibility_sql()`.
360 + * `openstation_content_graph_visibility_sql()`.
351 361 *
352 - * @since 0.8.2
353 - *
354 362 * @param string[] $types Already normalized.
355 363 * @return WP_Post[]
356 364 */
357 -function desktop_mode_content_graph_fetch_rows( array $types ) {
365 +function openstation_content_graph_fetch_rows( array $types ) {
358 366 global $wpdb;
359 - $visibility = desktop_mode_content_graph_visibility_sql( $types );
367 + $visibility = openstation_content_graph_visibility_sql( $types );
360 368 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
361 369 $rows = $wpdb->get_results(
362 370 $wpdb->prepare(
363 371 "SELECT ID, post_type, post_status, post_title, post_name, post_content, post_author, post_date
@@ -391,14 +399,12 @@
391 399 * Pull every internal-target post id out of a chunk of post_content.
392 400 * Uses DOMDocument for robustness against malformed HTML, then
393 401 * `url_to_postid()` to resolve each href.
394 402 *
395 - * @since 0.8.2
396 - *
397 403 * @param string $content
398 404 * @return int[] Unique target post ids (order preserved).
399 405 */
400 -function desktop_mode_content_graph_extract_internal_links( $content ) {
406 +function openstation_content_graph_extract_internal_links( $content ) {
401 407 if ( '' === trim( (string) $content ) ) {
402 408 return array();
403 409 }
404 410
@@ -442,15 +448,13 @@
442 448 }
443 449
444 450 /**
445 451 * Cache invalidation. Any post-type change wipes every transient
446 - * carrying the `desktop_mode_cg_` prefix. We don't have a per-type
452 + * carrying the `openstation_cg_` prefix. We don't have a per-type
447 453 * index so we wipe globally, the cost is one extra build on next
448 454 * open which dominates the time-savings on subsequent opens.
449 - *
450 - * @since 0.8.2
451 455 */
452 -function desktop_mode_content_graph_flush_cache() {
456 +function openstation_content_graph_flush_cache() {
453 457 global $wpdb;
454 458 // Enumerate matching transient option names first, then route each
455 459 // through `delete_transient()` so WP's transient/options cache
456 460 // invalidation runs alongside the wp_options delete. The earlier
@@ -459,9 +463,9 @@
459 463 // the in-memory options cache and returned the stale payload.
460 464 // This bit the `set_object_terms` invalidation path because that
461 465 // hook doesn't update `post_modified_gmt`, so the cache key stayed
462 466 // identical and `get_transient` (cache hit) returned pre-retag data.
463 - $prefix_like = $wpdb->esc_like( '_transient_' . DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_PREFIX ) . '%';
467 + $prefix_like = $wpdb->esc_like( '_transient_' . OPENSTATION_CONTENT_GRAPH_TRANSIENT_PREFIX ) . '%';
464 468 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
465 469 $option_names = $wpdb->get_col(
466 470 $wpdb->prepare(
467 471 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
@@ -479,14 +483,14 @@
479 483 delete_transient( $transient );
480 484 }
481 485 }
482 486 }
483 -add_action( 'save_post', 'desktop_mode_content_graph_flush_cache' );
484 -add_action( 'deleted_post', 'desktop_mode_content_graph_flush_cache' );
487 +add_action( 'save_post', 'openstation_content_graph_flush_cache' );
488 +add_action( 'deleted_post', 'openstation_content_graph_flush_cache' );
485 489 // Term assignments can change outside the post-edit path (CLI, bulk
486 490 // quick-edit, REST). Without this the per-node `category_ids` / `tag_ids`
487 491 // the group-by UI reads would go stale until the 6h TTL expires.
488 -add_action( 'set_object_terms', 'desktop_mode_content_graph_flush_cache' );
492 +add_action( 'set_object_terms', 'openstation_content_graph_flush_cache' );
489 493
490 494 /**
491 495 * Bulk-fetch every (post_id → taxonomy → term_ids[]) mapping for the
492 496 * `category` and `post_tag` taxonomies in a single query. Used to
@@ -492,15 +496,13 @@
492 496 * `category` and `post_tag` taxonomies in a single query. Used to
493 497 * populate the per-node `category_ids` / `tag_ids` arrays without N+1
494 498 * `wp_get_object_terms` calls.
495 499 *
496 - * @since 0.8.6
497 - *
498 500 * @param int[] $post_ids
499 501 * @return array<int, array<string, int[]>> Outer key = post id; inner
500 502 * key = taxonomy slug; value = term ids the post is in.
501 503 */
502 -function desktop_mode_content_graph_collect_post_terms( array $post_ids ) {
504 +function openstation_content_graph_collect_post_terms( array $post_ids ) {
503 505 $post_ids = array_values( array_filter( array_map( 'intval', $post_ids ) ) );
504 506 if ( empty( $post_ids ) ) {
505 507 return array();
506 508 }
@@ -539,9 +541,9 @@
539 541 }
540 542
541 543 /**
542 544 * Bulk-fetch distinct revision authors per post for the requested
543 - * post ids. Used by `desktop_mode_content_graph_build()` to populate
545 + * post ids. Used by `openstation_content_graph_build()` to populate
544 546 * each node's `contributor_ids` array so the cluster-attractor force
545 547 * can pull collaborator posts toward both the primary author's
546 548 * cluster AND each contributor's cluster (weighted so the primary
547 549 * still wins).
@@ -549,14 +551,12 @@
549 551 * Returns the distinct `post_author` values from each in-scope post's
550 552 * revision children. Includes the primary author if they also
551 553 * authored a revision; the caller is expected to filter that out.
552 554 *
553 - * @since 0.8.6
554 - *
555 555 * @param int[] $post_ids
556 556 * @return array<int, int[]> post_id => list of contributor user ids.
557 557 */
558 -function desktop_mode_content_graph_collect_post_contributors( array $post_ids ) {
558 +function openstation_content_graph_collect_post_contributors( array $post_ids ) {
559 559 $post_ids = array_values( array_filter( array_map( 'intval', $post_ids ) ) );
560 560 if ( empty( $post_ids ) ) {
561 561 return array();
562 562 }
@@ -593,14 +593,12 @@
593 593 /**
594 594 * Build a `{ id => { name } }` catalog for the given author ids.
595 595 * Uses one `WP_User_Query` rather than per-id `get_userdata` calls.
596 596 *
597 - * @since 0.8.6
598 - *
599 597 * @param int[] $author_ids
600 598 * @return array<int, array{ name: string }>
601 599 */
602 -function desktop_mode_content_graph_format_author_catalog( array $author_ids ) {
600 +function openstation_content_graph_format_author_catalog( array $author_ids ) {
603 601 $author_ids = array_values( array_unique( array_filter( array_map( 'intval', $author_ids ) ) ) );
604 602 if ( empty( $author_ids ) ) {
605 603 return array();
606 604 }
@@ -610,9 +608,9 @@
610 608 'fields' => array( 'ID', 'display_name' ),
611 609 'number' => count( $author_ids ),
612 610 )
613 611 );
614 - $out = array();
612 + $out = array();
615 613 foreach ( (array) $query->get_results() as $user ) {
616 614 $out[ (int) $user->ID ] = array(
617 615 'name' => (string) $user->display_name,
618 616 );
@@ -624,15 +622,13 @@
624 622 * Build a `{ id => { name } }` catalog for the given term ids in a
625 623 * single taxonomy. Uses `get_terms` with `include` so the names come
626 624 * back in one query.
627 625 *
628 - * @since 0.8.6
629 - *
630 626 * @param int[] $term_ids
631 627 * @param string $taxonomy
632 628 * @return array<int, array{ name: string }>
633 629 */
634 -function desktop_mode_content_graph_format_term_catalog( array $term_ids, $taxonomy ) {
630 +function openstation_content_graph_format_term_catalog( array $term_ids, $taxonomy ) {
635 631 $term_ids = array_values( array_unique( array_filter( array_map( 'intval', $term_ids ) ) ) );
636 632 if ( empty( $term_ids ) ) {
637 633 return array();
638 634 }
@@ -643,9 +639,9 @@
643 639 'hide_empty' => false,
644 640 'number' => count( $term_ids ),
645 641 )
646 642 );
647 - $out = array();
643 + $out = array();
648 644 if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
649 645 return $out;
650 646 }
651 647 foreach ( $terms as $term ) {