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 +52 -68 0.9.71.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_cg3_';
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,14 +94,14 @@
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
101 105 $default_category = max( 1, (int) get_option( 'default_category', 1 ) );
102 106
103 107 $nodes = array();
@@ -105,12 +109,12 @@
105 109 $author_ids = array();
106 110 $cat_ids = array();
107 111 $tag_ids = array();
108 112 foreach ( $rows as $row ) {
109 - $id = (int) $row->ID;
110 - $author_id = (int) $row->post_author;
111 - $year = 0;
112 - $year_month = '';
113 + $id = (int) $row->ID;
114 + $author_id = (int) $row->post_author;
115 + $year = 0;
116 + $year_month = '';
113 117 if ( ! empty( $row->post_date ) ) {
114 118 // Use post_date (site-local) rather than post_date_gmt for the
115 119 // "year published" / "year-month published" buckets — editors
116 120 // think in their own timezone.
@@ -132,9 +136,9 @@
132 136 }
133 137 $post_tags = isset( $terms_by_post[ $id ]['post_tag'] )
134 138 ? $terms_by_post[ $id ]['post_tag']
135 139 : array();
136 - $contribs = isset( $contribs_by_post[ $id ] )
140 + $contribs = isset( $contribs_by_post[ $id ] )
137 141 ? $contribs_by_post[ $id ]
138 142 : array();
139 143 // Strip the primary author from the contributor list — the
140 144 // node carries that separately in `author_id`, and surfacing
@@ -150,9 +154,9 @@
150 154 )
151 155 );
152 156 }
153 157
154 - $node = array(
158 + $node = array(
155 159 'id' => $id,
156 160 'type' => (string) $row->post_type,
157 161 'title' => (string) get_the_title( $row ),
158 162 'status' => (string) $row->post_status,
@@ -183,11 +187,11 @@
183 187 }
184 188 }
185 189
186 190 $groups = array(
187 - 'authors' => desktop_mode_content_graph_format_author_catalog( array_keys( $author_ids ) ),
188 - 'categories' => desktop_mode_content_graph_format_term_catalog( array_keys( $cat_ids ), 'category' ),
189 - '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' ),
190 194 );
191 195
192 196 $edges_seen = array();
193 197 $edges = array();
@@ -192,9 +196,9 @@
192 196 $edges_seen = array();
193 197 $edges = array();
194 198 foreach ( $rows as $row ) {
195 199 $from = (int) $row->ID;
196 - $tos = desktop_mode_content_graph_extract_internal_links( (string) $row->post_content );
200 + $tos = openstation_content_graph_extract_internal_links( (string) $row->post_content );
197 201 foreach ( $tos as $to ) {
198 202 if ( $to === $from ) {
199 203 continue;
200 204 }
@@ -226,9 +230,9 @@
226 230 'generated_at' => time(),
227 231 ),
228 232 );
229 233
230 - set_transient( $cache_key, $payload, DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_TTL );
234 + set_transient( $cache_key, $payload, OPENSTATION_CONTENT_GRAPH_TRANSIENT_TTL );
231 235
232 236 return $payload;
233 237 }
234 238
@@ -235,18 +239,16 @@
235 239 /**
236 240 * Filter, sanitize, and uniquify the requested type slugs against the
237 241 * public post-type registry. Returned slugs are guaranteed to exist
238 242 * AND to be among the slugs declared by
239 - * `desktop_mode_content_graph_post_types()`.
243 + * `openstation_content_graph_post_types()`.
240 244 *
241 - * @since 0.8.2
242 - *
243 245 * @param string[] $types
244 246 * @return string[]
245 247 */
246 -function desktop_mode_content_graph_normalize_types( array $types ) {
248 +function openstation_content_graph_normalize_types( array $types ) {
247 249 $allowed = array();
248 - foreach ( desktop_mode_content_graph_post_types() as $entry ) {
250 + foreach ( openstation_content_graph_post_types() as $entry ) {
249 251 if ( ! empty( $entry['slug'] ) ) {
250 252 $allowed[ (string) $entry['slug'] ] = true;
251 253 }
252 254 }
@@ -274,14 +276,12 @@
274 276 * The `key` element encodes the resulting privilege tier (and, when
275 277 * the own-author clause is active, the user id) so cached payloads
276 278 * are never served across privilege levels.
277 279 *
278 - * @since 0.9.2
279 - *
280 280 * @param string[] $types Already normalized.
281 281 * @return array{ where: string, values: array, key: string }
282 282 */
283 -function desktop_mode_content_graph_visibility_sql( array $types ) {
283 +function openstation_content_graph_visibility_sql( array $types ) {
284 284 $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
285 285 $values = $types;
286 286
287 287 $priv_types = array();
@@ -326,16 +326,14 @@
326 326 * relevant edit busts the cache implicitly, plus the viewer's
327 327 * privilege-tier signature so a payload built for a user who can read
328 328 * private posts is never served to one who can't (and vice versa).
329 329 *
330 - * @since 0.8.2
331 - *
332 330 * @param string[] $types Already normalized.
333 331 * @return string
334 332 */
335 -function desktop_mode_content_graph_cache_key( array $types ) {
333 +function openstation_content_graph_cache_key( array $types ) {
336 334 global $wpdb;
337 - $visibility = desktop_mode_content_graph_visibility_sql( $types );
335 + $visibility = openstation_content_graph_visibility_sql( $types );
338 336 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
339 337 $hash = (string) $wpdb->get_var(
340 338 $wpdb->prepare(
341 339 "SELECT MD5( GROUP_CONCAT( CONCAT( ID, ':', post_modified_gmt ) ORDER BY ID ) )
@@ -347,9 +345,9 @@
347 345 // phpcs:enable
348 346 if ( '' === $hash || null === $hash ) {
349 347 $hash = 'empty';
350 348 }
351 - 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 );
352 350 }
353 351
354 352 /**
355 353 * Fetch the participating posts in a single query. Returns full rows
@@ -358,18 +356,16 @@
358 356 *
359 357 * Rows are scoped to what the current user can read: published posts,
360 358 * plus private posts only where the user holds the type's
361 359 * `read_private_posts` capability (or authored the post). See
362 - * `desktop_mode_content_graph_visibility_sql()`.
360 + * `openstation_content_graph_visibility_sql()`.
363 361 *
364 - * @since 0.8.2
365 - *
366 362 * @param string[] $types Already normalized.
367 363 * @return WP_Post[]
368 364 */
369 -function desktop_mode_content_graph_fetch_rows( array $types ) {
365 +function openstation_content_graph_fetch_rows( array $types ) {
370 366 global $wpdb;
371 - $visibility = desktop_mode_content_graph_visibility_sql( $types );
367 + $visibility = openstation_content_graph_visibility_sql( $types );
372 368 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
373 369 $rows = $wpdb->get_results(
374 370 $wpdb->prepare(
375 371 "SELECT ID, post_type, post_status, post_title, post_name, post_content, post_author, post_date
@@ -403,14 +399,12 @@
403 399 * Pull every internal-target post id out of a chunk of post_content.
404 400 * Uses DOMDocument for robustness against malformed HTML, then
405 401 * `url_to_postid()` to resolve each href.
406 402 *
407 - * @since 0.8.2
408 - *
409 403 * @param string $content
410 404 * @return int[] Unique target post ids (order preserved).
411 405 */
412 -function desktop_mode_content_graph_extract_internal_links( $content ) {
406 +function openstation_content_graph_extract_internal_links( $content ) {
413 407 if ( '' === trim( (string) $content ) ) {
414 408 return array();
415 409 }
416 410
@@ -454,15 +448,13 @@
454 448 }
455 449
456 450 /**
457 451 * Cache invalidation. Any post-type change wipes every transient
458 - * 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
459 453 * index so we wipe globally, the cost is one extra build on next
460 454 * open which dominates the time-savings on subsequent opens.
461 - *
462 - * @since 0.8.2
463 455 */
464 -function desktop_mode_content_graph_flush_cache() {
456 +function openstation_content_graph_flush_cache() {
465 457 global $wpdb;
466 458 // Enumerate matching transient option names first, then route each
467 459 // through `delete_transient()` so WP's transient/options cache
468 460 // invalidation runs alongside the wp_options delete. The earlier
@@ -471,9 +463,9 @@
471 463 // the in-memory options cache and returned the stale payload.
472 464 // This bit the `set_object_terms` invalidation path because that
473 465 // hook doesn't update `post_modified_gmt`, so the cache key stayed
474 466 // identical and `get_transient` (cache hit) returned pre-retag data.
475 - $prefix_like = $wpdb->esc_like( '_transient_' . DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_PREFIX ) . '%';
467 + $prefix_like = $wpdb->esc_like( '_transient_' . OPENSTATION_CONTENT_GRAPH_TRANSIENT_PREFIX ) . '%';
476 468 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
477 469 $option_names = $wpdb->get_col(
478 470 $wpdb->prepare(
479 471 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
@@ -491,14 +483,14 @@
491 483 delete_transient( $transient );
492 484 }
493 485 }
494 486 }
495 -add_action( 'save_post', 'desktop_mode_content_graph_flush_cache' );
496 -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' );
497 489 // Term assignments can change outside the post-edit path (CLI, bulk
498 490 // quick-edit, REST). Without this the per-node `category_ids` / `tag_ids`
499 491 // the group-by UI reads would go stale until the 6h TTL expires.
500 -add_action( 'set_object_terms', 'desktop_mode_content_graph_flush_cache' );
492 +add_action( 'set_object_terms', 'openstation_content_graph_flush_cache' );
501 493
502 494 /**
503 495 * Bulk-fetch every (post_id → taxonomy → term_ids[]) mapping for the
504 496 * `category` and `post_tag` taxonomies in a single query. Used to
@@ -504,15 +496,13 @@
504 496 * `category` and `post_tag` taxonomies in a single query. Used to
505 497 * populate the per-node `category_ids` / `tag_ids` arrays without N+1
506 498 * `wp_get_object_terms` calls.
507 499 *
508 - * @since 0.8.6
509 - *
510 500 * @param int[] $post_ids
511 501 * @return array<int, array<string, int[]>> Outer key = post id; inner
512 502 * key = taxonomy slug; value = term ids the post is in.
513 503 */
514 -function desktop_mode_content_graph_collect_post_terms( array $post_ids ) {
504 +function openstation_content_graph_collect_post_terms( array $post_ids ) {
515 505 $post_ids = array_values( array_filter( array_map( 'intval', $post_ids ) ) );
516 506 if ( empty( $post_ids ) ) {
517 507 return array();
518 508 }
@@ -551,9 +541,9 @@
551 541 }
552 542
553 543 /**
554 544 * Bulk-fetch distinct revision authors per post for the requested
555 - * post ids. Used by `desktop_mode_content_graph_build()` to populate
545 + * post ids. Used by `openstation_content_graph_build()` to populate
556 546 * each node's `contributor_ids` array so the cluster-attractor force
557 547 * can pull collaborator posts toward both the primary author's
558 548 * cluster AND each contributor's cluster (weighted so the primary
559 549 * still wins).
@@ -561,14 +551,12 @@
561 551 * Returns the distinct `post_author` values from each in-scope post's
562 552 * revision children. Includes the primary author if they also
563 553 * authored a revision; the caller is expected to filter that out.
564 554 *
565 - * @since 0.8.6
566 - *
567 555 * @param int[] $post_ids
568 556 * @return array<int, int[]> post_id => list of contributor user ids.
569 557 */
570 -function desktop_mode_content_graph_collect_post_contributors( array $post_ids ) {
558 +function openstation_content_graph_collect_post_contributors( array $post_ids ) {
571 559 $post_ids = array_values( array_filter( array_map( 'intval', $post_ids ) ) );
572 560 if ( empty( $post_ids ) ) {
573 561 return array();
574 562 }
@@ -605,14 +593,12 @@
605 593 /**
606 594 * Build a `{ id => { name } }` catalog for the given author ids.
607 595 * Uses one `WP_User_Query` rather than per-id `get_userdata` calls.
608 596 *
609 - * @since 0.8.6
610 - *
611 597 * @param int[] $author_ids
612 598 * @return array<int, array{ name: string }>
613 599 */
614 -function desktop_mode_content_graph_format_author_catalog( array $author_ids ) {
600 +function openstation_content_graph_format_author_catalog( array $author_ids ) {
615 601 $author_ids = array_values( array_unique( array_filter( array_map( 'intval', $author_ids ) ) ) );
616 602 if ( empty( $author_ids ) ) {
617 603 return array();
618 604 }
@@ -622,9 +608,9 @@
622 608 'fields' => array( 'ID', 'display_name' ),
623 609 'number' => count( $author_ids ),
624 610 )
625 611 );
626 - $out = array();
612 + $out = array();
627 613 foreach ( (array) $query->get_results() as $user ) {
628 614 $out[ (int) $user->ID ] = array(
629 615 'name' => (string) $user->display_name,
630 616 );
@@ -636,15 +622,13 @@
636 622 * Build a `{ id => { name } }` catalog for the given term ids in a
637 623 * single taxonomy. Uses `get_terms` with `include` so the names come
638 624 * back in one query.
639 625 *
640 - * @since 0.8.6
641 - *
642 626 * @param int[] $term_ids
643 627 * @param string $taxonomy
644 628 * @return array<int, array{ name: string }>
645 629 */
646 -function desktop_mode_content_graph_format_term_catalog( array $term_ids, $taxonomy ) {
630 +function openstation_content_graph_format_term_catalog( array $term_ids, $taxonomy ) {
647 631 $term_ids = array_values( array_unique( array_filter( array_map( 'intval', $term_ids ) ) ) );
648 632 if ( empty( $term_ids ) ) {
649 633 return array();
650 634 }
@@ -655,9 +639,9 @@
655 639 'hide_empty' => false,
656 640 'number' => count( $term_ids ),
657 641 )
658 642 );
659 - $out = array();
643 + $out = array();
660 644 if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
661 645 return $out;
662 646 }
663 647 foreach ( $terms as $term ) {