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 +133 -71 0.9.11.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.
@@ -11,9 +11,10 @@
11 11 * The expensive piece is the link extraction: every published post in
12 12 * scope is parsed with `DOMDocument`, every `<a href>` is scanned, and
13 13 * each href is resolved with `url_to_postid()`. We cache the full
14 14 * `{ nodes, edges }` tuple in a transient keyed on the requested
15 - * `types` plus a hash of the relevant rows' `post_modified_gmt`. Any
15 + * `types`, the viewer's private-post privilege tier, plus a hash of
16 + * the relevant rows' `post_modified_gmt`. Any
16 17 * change to a participating post invalidates the hash, so save_post
17 18 * implicitly refreshes the graph the next time it's requested. We
18 19 * also explicitly bust the cache on `save_post` and `deleted_post` so
19 20 * editors don't see stale data even if some other process pre-warms
@@ -18,10 +19,9 @@
18 19 * also explicitly bust the cache on `save_post` and `deleted_post` so
19 20 * editors don't see stale data even if some other process pre-warms
20 21 * the transient.
21 22 *
22 - * @package WPDesktopMode
23 - * @since 0.8.2
23 + * @package OpenStation
24 24 */
25 25
26 26 defined( 'ABSPATH' ) || exit;
27 27
@@ -29,17 +29,22 @@
29 29 // transients still alive at upgrade time would otherwise return the
30 30 // previous schema (e.g. missing per-node `contributor_ids`) and
31 31 // surface as runtime errors on the client. Each bump is a one-time
32 32 // cache miss for every site that updates the plugin.
33 -const DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_PREFIX = 'desktop_mode_cg2_';
34 -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;
35 42
36 43 /**
37 44 * Build (or reuse the cached version of) the graph payload for the
38 45 * requested post types.
39 46 *
40 - * @since 0.8.2
41 - *
42 47 * @param string[] $types Post type slugs. Filtered against the public
43 48 * post-type registry, attachments excluded.
44 49 * @return array{
45 50 * nodes: array<int, array{
@@ -57,10 +62,10 @@
57 62 * },
58 63 * stats: array{ nodes: int, edges: int, generated_at: int }
59 64 * }
60 65 */
61 -function desktop_mode_content_graph_build( array $types ) {
62 - $types = desktop_mode_content_graph_normalize_types( $types );
66 +function openstation_content_graph_build( array $types ) {
67 + $types = openstation_content_graph_normalize_types( $types );
63 68 if ( empty( $types ) ) {
64 69 return array(
65 70 'nodes' => array(),
66 71 'edges' => array(),
@@ -76,15 +81,15 @@
76 81 ),
77 82 );
78 83 }
79 84
80 - $cache_key = desktop_mode_content_graph_cache_key( $types );
85 + $cache_key = openstation_content_graph_cache_key( $types );
81 86 $cached = get_transient( $cache_key );
82 87 if ( is_array( $cached ) && isset( $cached['nodes'], $cached['edges'] ) ) {
83 88 return $cached;
84 89 }
85 90
86 - $rows = desktop_mode_content_graph_fetch_rows( $types );
91 + $rows = openstation_content_graph_fetch_rows( $types );
87 92
88 93 $post_ids = array();
89 94 foreach ( $rows as $row ) {
90 95 $post_ids[] = (int) $row->ID;
@@ -89,15 +94,17 @@
89 94 foreach ( $rows as $row ) {
90 95 $post_ids[] = (int) $row->ID;
91 96 }
92 97
93 - $terms_by_post = desktop_mode_content_graph_collect_post_terms( $post_ids );
98 + $terms_by_post = openstation_content_graph_collect_post_terms( $post_ids );
94 99 // Distinct revision authors per post — used to pull collaborator
95 100 // posts toward both the primary author's cluster AND the
96 101 // contributor's clusters when grouping by author. Bulk-queried so
97 102 // we don't N+1 `wp_get_post_revisions` per node.
98 - $contribs_by_post = desktop_mode_content_graph_collect_post_contributors( $post_ids );
103 + $contribs_by_post = openstation_content_graph_collect_post_contributors( $post_ids );
99 104
105 + $default_category = max( 1, (int) get_option( 'default_category', 1 ) );
106 +
100 107 $nodes = array();
101 108 $nodes_by_id = array();
102 109 $author_ids = array();
103 110 $cat_ids = array();
@@ -102,12 +109,12 @@
102 109 $author_ids = array();
103 110 $cat_ids = array();
104 111 $tag_ids = array();
105 112 foreach ( $rows as $row ) {
106 - $id = (int) $row->ID;
107 - $author_id = (int) $row->post_author;
108 - $year = 0;
109 - $year_month = '';
113 + $id = (int) $row->ID;
114 + $author_id = (int) $row->post_author;
115 + $year = 0;
116 + $year_month = '';
110 117 if ( ! empty( $row->post_date ) ) {
111 118 // Use post_date (site-local) rather than post_date_gmt for the
112 119 // "year published" / "year-month published" buckets — editors
113 120 // think in their own timezone.
@@ -116,12 +123,22 @@
116 123 }
117 124 $post_cats = isset( $terms_by_post[ $id ]['category'] )
118 125 ? $terms_by_post[ $id ]['category']
119 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 + }
120 137 $post_tags = isset( $terms_by_post[ $id ]['post_tag'] )
121 138 ? $terms_by_post[ $id ]['post_tag']
122 139 : array();
123 - $contribs = isset( $contribs_by_post[ $id ] )
140 + $contribs = isset( $contribs_by_post[ $id ] )
124 141 ? $contribs_by_post[ $id ]
125 142 : array();
126 143 // Strip the primary author from the contributor list — the
127 144 // node carries that separately in `author_id`, and surfacing
@@ -137,9 +154,9 @@
137 154 )
138 155 );
139 156 }
140 157
141 - $node = array(
158 + $node = array(
142 159 'id' => $id,
143 160 'type' => (string) $row->post_type,
144 161 'title' => (string) get_the_title( $row ),
145 162 'status' => (string) $row->post_status,
@@ -170,11 +187,11 @@
170 187 }
171 188 }
172 189
173 190 $groups = array(
174 - 'authors' => desktop_mode_content_graph_format_author_catalog( array_keys( $author_ids ) ),
175 - 'categories' => desktop_mode_content_graph_format_term_catalog( array_keys( $cat_ids ), 'category' ),
176 - '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' ),
177 194 );
178 195
179 196 $edges_seen = array();
180 197 $edges = array();
@@ -179,9 +196,9 @@
179 196 $edges_seen = array();
180 197 $edges = array();
181 198 foreach ( $rows as $row ) {
182 199 $from = (int) $row->ID;
183 - $tos = desktop_mode_content_graph_extract_internal_links( (string) $row->post_content );
200 + $tos = openstation_content_graph_extract_internal_links( (string) $row->post_content );
184 201 foreach ( $tos as $to ) {
185 202 if ( $to === $from ) {
186 203 continue;
187 204 }
@@ -213,9 +230,9 @@
213 230 'generated_at' => time(),
214 231 ),
215 232 );
216 233
217 - set_transient( $cache_key, $payload, DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_TTL );
234 + set_transient( $cache_key, $payload, OPENSTATION_CONTENT_GRAPH_TRANSIENT_TTL );
218 235
219 236 return $payload;
220 237 }
221 238
@@ -222,18 +239,16 @@
222 239 /**
223 240 * Filter, sanitize, and uniquify the requested type slugs against the
224 241 * public post-type registry. Returned slugs are guaranteed to exist
225 242 * AND to be among the slugs declared by
226 - * `desktop_mode_content_graph_post_types()`.
243 + * `openstation_content_graph_post_types()`.
227 244 *
228 - * @since 0.8.2
229 - *
230 245 * @param string[] $types
231 246 * @return string[]
232 247 */
233 -function desktop_mode_content_graph_normalize_types( array $types ) {
248 +function openstation_content_graph_normalize_types( array $types ) {
234 249 $allowed = array();
235 - foreach ( desktop_mode_content_graph_post_types() as $entry ) {
250 + foreach ( openstation_content_graph_post_types() as $entry ) {
236 251 if ( ! empty( $entry['slug'] ) ) {
237 252 $allowed[ (string) $entry['slug'] ] = true;
238 253 }
239 254 }
@@ -247,28 +262,85 @@
247 262 return array_keys( $out );
248 263 }
249 264
250 265 /**
266 + * Build the SQL WHERE fragment (plus its `prepare()` values and a
267 + * cache-key signature) scoping graph rows to posts the current user
268 + * is allowed to read.
269 + *
270 + * Published posts are always in scope. Private posts of a type are
271 + * only included when the user holds that type's `read_private_posts`
272 + * capability; for the remaining types the user still sees their OWN
273 + * private posts (mirroring core's `WP_Query` status semantics for
274 + * logged-in users).
275 + *
276 + * The `key` element encodes the resulting privilege tier (and, when
277 + * the own-author clause is active, the user id) so cached payloads
278 + * are never served across privilege levels.
279 + *
280 + * @param string[] $types Already normalized.
281 + * @return array{ where: string, values: array, key: string }
282 + */
283 +function openstation_content_graph_visibility_sql( array $types ) {
284 + $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
285 + $values = $types;
286 +
287 + $priv_types = array();
288 + foreach ( $types as $type ) {
289 + $type_obj = get_post_type_object( $type );
290 + $cap = ( $type_obj && ! empty( $type_obj->cap->read_private_posts ) )
291 + ? $type_obj->cap->read_private_posts
292 + : 'read_private_posts';
293 + if ( current_user_can( $cap ) ) {
294 + $priv_types[] = $type;
295 + }
296 + }
297 +
298 + $status_clauses = array( "post_status = 'publish'" );
299 + $key_parts = array( 'priv=' . implode( ',', $priv_types ) );
300 +
301 + if ( ! empty( $priv_types ) ) {
302 + $priv_placeholders = implode( ',', array_fill( 0, count( $priv_types ), '%s' ) );
303 + $status_clauses[] = "( post_status = 'private' AND post_type IN ( {$priv_placeholders} ) )";
304 + $values = array_merge( $values, $priv_types );
305 + }
306 +
307 + $user_id = get_current_user_id();
308 + if ( $user_id > 0 && count( $priv_types ) < count( $types ) ) {
309 + $status_clauses[] = "( post_status = 'private' AND post_author = %d )";
310 + $values[] = $user_id;
311 + $key_parts[] = 'own=' . $user_id;
312 + }
313 +
314 + $where = "post_type IN ( {$placeholders} ) AND ( " . implode( ' OR ', $status_clauses ) . ' )';
315 +
316 + return array(
317 + 'where' => $where,
318 + 'values' => $values,
319 + 'key' => implode( '|', $key_parts ),
320 + );
321 +}
322 +
323 +/**
251 324 * Cache key for `{ nodes, edges }` for a given type set. Includes a
252 325 * short hash of the participating rows' post_modified_gmt so any
253 - * relevant edit busts the cache implicitly.
326 + * relevant edit busts the cache implicitly, plus the viewer's
327 + * privilege-tier signature so a payload built for a user who can read
328 + * private posts is never served to one who can't (and vice versa).
254 329 *
255 - * @since 0.8.2
256 - *
257 330 * @param string[] $types Already normalized.
258 331 * @return string
259 332 */
260 -function desktop_mode_content_graph_cache_key( array $types ) {
333 +function openstation_content_graph_cache_key( array $types ) {
261 334 global $wpdb;
262 - $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
335 + $visibility = openstation_content_graph_visibility_sql( $types );
263 336 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
264 337 $hash = (string) $wpdb->get_var(
265 338 $wpdb->prepare(
266 339 "SELECT MD5( GROUP_CONCAT( CONCAT( ID, ':', post_modified_gmt ) ORDER BY ID ) )
267 340 FROM {$wpdb->posts}
268 - WHERE post_status IN ( 'publish', 'private' )
269 - AND post_type IN ( {$placeholders} )",
270 - $types
341 + WHERE {$visibility['where']}",
342 + $visibility['values']
271 343 )
272 344 );
273 345 // phpcs:enable
274 346 if ( '' === $hash || null === $hash ) {
@@ -273,9 +345,9 @@
273 345 // phpcs:enable
274 346 if ( '' === $hash || null === $hash ) {
275 347 $hash = 'empty';
276 348 }
277 - return DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_PREFIX . substr( md5( implode( ',', $types ) . '|' . $hash ), 0, 24 );
349 + return OPENSTATION_CONTENT_GRAPH_TRANSIENT_PREFIX . substr( md5( implode( ',', $types ) . '|' . $visibility['key'] . '|' . $hash ), 0, 24 );
278 350 }
279 351
280 352 /**
281 353 * Fetch the participating posts in a single query. Returns full rows
@@ -281,25 +353,27 @@
281 353 * Fetch the participating posts in a single query. Returns full rows
282 354 * (including `post_content`) so the link extractor can run without N+1
283 355 * `get_post()` calls.
284 356 *
285 - * @since 0.8.2
357 + * Rows are scoped to what the current user can read: published posts,
358 + * plus private posts only where the user holds the type's
359 + * `read_private_posts` capability (or authored the post). See
360 + * `openstation_content_graph_visibility_sql()`.
286 361 *
287 362 * @param string[] $types Already normalized.
288 363 * @return WP_Post[]
289 364 */
290 -function desktop_mode_content_graph_fetch_rows( array $types ) {
365 +function openstation_content_graph_fetch_rows( array $types ) {
291 366 global $wpdb;
292 - $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
367 + $visibility = openstation_content_graph_visibility_sql( $types );
293 368 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
294 369 $rows = $wpdb->get_results(
295 370 $wpdb->prepare(
296 371 "SELECT ID, post_type, post_status, post_title, post_name, post_content, post_author, post_date
297 372 FROM {$wpdb->posts}
298 - WHERE post_status IN ( 'publish', 'private' )
299 - AND post_type IN ( {$placeholders} )
373 + WHERE {$visibility['where']}
300 374 ORDER BY post_date DESC",
301 - $types
375 + $visibility['values']
302 376 )
303 377 );
304 378 // phpcs:enable
305 379
@@ -325,14 +399,12 @@
325 399 * Pull every internal-target post id out of a chunk of post_content.
326 400 * Uses DOMDocument for robustness against malformed HTML, then
327 401 * `url_to_postid()` to resolve each href.
328 402 *
329 - * @since 0.8.2
330 - *
331 403 * @param string $content
332 404 * @return int[] Unique target post ids (order preserved).
333 405 */
334 -function desktop_mode_content_graph_extract_internal_links( $content ) {
406 +function openstation_content_graph_extract_internal_links( $content ) {
335 407 if ( '' === trim( (string) $content ) ) {
336 408 return array();
337 409 }
338 410
@@ -376,15 +448,13 @@
376 448 }
377 449
378 450 /**
379 451 * Cache invalidation. Any post-type change wipes every transient
380 - * 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
381 453 * index so we wipe globally, the cost is one extra build on next
382 454 * open which dominates the time-savings on subsequent opens.
383 - *
384 - * @since 0.8.2
385 455 */
386 -function desktop_mode_content_graph_flush_cache() {
456 +function openstation_content_graph_flush_cache() {
387 457 global $wpdb;
388 458 // Enumerate matching transient option names first, then route each
389 459 // through `delete_transient()` so WP's transient/options cache
390 460 // invalidation runs alongside the wp_options delete. The earlier
@@ -393,9 +463,9 @@
393 463 // the in-memory options cache and returned the stale payload.
394 464 // This bit the `set_object_terms` invalidation path because that
395 465 // hook doesn't update `post_modified_gmt`, so the cache key stayed
396 466 // identical and `get_transient` (cache hit) returned pre-retag data.
397 - $prefix_like = $wpdb->esc_like( '_transient_' . DESKTOP_MODE_CONTENT_GRAPH_TRANSIENT_PREFIX ) . '%';
467 + $prefix_like = $wpdb->esc_like( '_transient_' . OPENSTATION_CONTENT_GRAPH_TRANSIENT_PREFIX ) . '%';
398 468 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
399 469 $option_names = $wpdb->get_col(
400 470 $wpdb->prepare(
401 471 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
@@ -413,14 +483,14 @@
413 483 delete_transient( $transient );
414 484 }
415 485 }
416 486 }
417 -add_action( 'save_post', 'desktop_mode_content_graph_flush_cache' );
418 -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' );
419 489 // Term assignments can change outside the post-edit path (CLI, bulk
420 490 // quick-edit, REST). Without this the per-node `category_ids` / `tag_ids`
421 491 // the group-by UI reads would go stale until the 6h TTL expires.
422 -add_action( 'set_object_terms', 'desktop_mode_content_graph_flush_cache' );
492 +add_action( 'set_object_terms', 'openstation_content_graph_flush_cache' );
423 493
424 494 /**
425 495 * Bulk-fetch every (post_id → taxonomy → term_ids[]) mapping for the
426 496 * `category` and `post_tag` taxonomies in a single query. Used to
@@ -426,15 +496,13 @@
426 496 * `category` and `post_tag` taxonomies in a single query. Used to
427 497 * populate the per-node `category_ids` / `tag_ids` arrays without N+1
428 498 * `wp_get_object_terms` calls.
429 499 *
430 - * @since 0.8.6
431 - *
432 500 * @param int[] $post_ids
433 501 * @return array<int, array<string, int[]>> Outer key = post id; inner
434 502 * key = taxonomy slug; value = term ids the post is in.
435 503 */
436 -function desktop_mode_content_graph_collect_post_terms( array $post_ids ) {
504 +function openstation_content_graph_collect_post_terms( array $post_ids ) {
437 505 $post_ids = array_values( array_filter( array_map( 'intval', $post_ids ) ) );
438 506 if ( empty( $post_ids ) ) {
439 507 return array();
440 508 }
@@ -473,9 +541,9 @@
473 541 }
474 542
475 543 /**
476 544 * Bulk-fetch distinct revision authors per post for the requested
477 - * post ids. Used by `desktop_mode_content_graph_build()` to populate
545 + * post ids. Used by `openstation_content_graph_build()` to populate
478 546 * each node's `contributor_ids` array so the cluster-attractor force
479 547 * can pull collaborator posts toward both the primary author's
480 548 * cluster AND each contributor's cluster (weighted so the primary
481 549 * still wins).
@@ -483,14 +551,12 @@
483 551 * Returns the distinct `post_author` values from each in-scope post's
484 552 * revision children. Includes the primary author if they also
485 553 * authored a revision; the caller is expected to filter that out.
486 554 *
487 - * @since 0.8.6
488 - *
489 555 * @param int[] $post_ids
490 556 * @return array<int, int[]> post_id => list of contributor user ids.
491 557 */
492 -function desktop_mode_content_graph_collect_post_contributors( array $post_ids ) {
558 +function openstation_content_graph_collect_post_contributors( array $post_ids ) {
493 559 $post_ids = array_values( array_filter( array_map( 'intval', $post_ids ) ) );
494 560 if ( empty( $post_ids ) ) {
495 561 return array();
496 562 }
@@ -527,14 +593,12 @@
527 593 /**
528 594 * Build a `{ id => { name } }` catalog for the given author ids.
529 595 * Uses one `WP_User_Query` rather than per-id `get_userdata` calls.
530 596 *
531 - * @since 0.8.6
532 - *
533 597 * @param int[] $author_ids
534 598 * @return array<int, array{ name: string }>
535 599 */
536 -function desktop_mode_content_graph_format_author_catalog( array $author_ids ) {
600 +function openstation_content_graph_format_author_catalog( array $author_ids ) {
537 601 $author_ids = array_values( array_unique( array_filter( array_map( 'intval', $author_ids ) ) ) );
538 602 if ( empty( $author_ids ) ) {
539 603 return array();
540 604 }
@@ -544,9 +608,9 @@
544 608 'fields' => array( 'ID', 'display_name' ),
545 609 'number' => count( $author_ids ),
546 610 )
547 611 );
548 - $out = array();
612 + $out = array();
549 613 foreach ( (array) $query->get_results() as $user ) {
550 614 $out[ (int) $user->ID ] = array(
551 615 'name' => (string) $user->display_name,
552 616 );
@@ -558,15 +622,13 @@
558 622 * Build a `{ id => { name } }` catalog for the given term ids in a
559 623 * single taxonomy. Uses `get_terms` with `include` so the names come
560 624 * back in one query.
561 625 *
562 - * @since 0.8.6
563 - *
564 626 * @param int[] $term_ids
565 627 * @param string $taxonomy
566 628 * @return array<int, array{ name: string }>
567 629 */
568 -function desktop_mode_content_graph_format_term_catalog( array $term_ids, $taxonomy ) {
630 +function openstation_content_graph_format_term_catalog( array $term_ids, $taxonomy ) {
569 631 $term_ids = array_values( array_unique( array_filter( array_map( 'intval', $term_ids ) ) ) );
570 632 if ( empty( $term_ids ) ) {
571 633 return array();
572 634 }
@@ -577,9 +639,9 @@
577 639 'hide_empty' => false,
578 640 'number' => count( $term_ids ),
579 641 )
580 642 );
581 - $out = array();
643 + $out = array();
582 644 if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
583 645 return $out;
584 646 }
585 647 foreach ( $terms as $term ) {