PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 All 37 releases
desktop-mode / includes / living-tree / snapshot.php

snapshot.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.13, at includes/living-tree/snapshot.php

235 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Living Tree: REST snapshot endpoint.
4 *
5 * One route: `GET desktop-mode/v1/living-tree/snapshot`. Returns the
6 * compact site DNA (`TreeSnapshot` in the JS types) — aggregate counts
7 * and branch hints — never the full post list. The client turns this
8 * into hormones and never sees rows.
9 *
10 * `siteUrl` + `siteName` + `installEpoch` together form the determinism
11 * seed. The site NAME is deliberately part of it: two different blogs can
12 * share a URL shape (two installs on localhost, staging clones), and
13 * their trees must still be individuals.
14 *
15 * The response is cached in a transient (TTL 6h) and invalidated whenever
16 * content changes (`save_post` / `deleted_post` / `comment_post`). One
17 * field is settled per caller after the cache: `traffic`, when it comes
18 * from Jetpack Stats, see {@see openstation_living_tree_snapshot_for_caller()}.
19 *
20 * @package OpenStation
21 */
22
23 defined( 'ABSPATH' ) || exit;
24
25 /**
26 * Transient key the built snapshot is cached under.
27 *
28 * The VALUE keeps its pre-rebrand spelling on purpose, so live caches
29 * stay addressable. The mismatch between this constant's name and its
30 * value is deliberate — it is NOT a half-finished rename.
31 *
32 * Not to be confused with the `openstation_living_tree_snapshot` filter,
33 * which once shared this string and is now deliberately decoupled.
34 */
35 const OPENSTATION_LIVING_TREE_CACHE_KEY = 'desktop_mode_living_tree_snapshot';
36
37 /** Cache lifetime for the snapshot. */
38 const OPENSTATION_LIVING_TREE_CACHE_TTL = 6 * HOUR_IN_SECONDS;
39
40 /**
41 * Key the cached snapshot keeps its second `traffic` value under: the
42 * one resolved without Jetpack Stats, for a caller outside Jetpack's
43 * stats gate. Never served, see
44 * {@see openstation_living_tree_snapshot_for_caller()}.
45 */
46 const OPENSTATION_LIVING_TREE_GATED_TRAFFIC_KEY = 'trafficWithoutJetpack';
47
48 /**
49 * Whether the current user may read the Living Tree snapshot.
50 *
51 * Defaults to `read` — anyone who can see the admin can see the wallpaper
52 * of their own site. Filterable so a site can widen or restrict it.
53 *
54 * @return bool
55 */
56 function openstation_living_tree_user_can_use() {
57 $can = current_user_can( 'read' );
58
59 /**
60 * Filter whether the current user can read the Living Tree snapshot.
61 *
62 * @param bool $can Default: the `read` capability.
63 */
64 return (bool) apply_filters( 'openstation_living_tree_user_can_use', $can );
65 }
66
67 /**
68 * Register the snapshot route.
69 */
70 function openstation_living_tree_register_routes() {
71 register_rest_route(
72 'desktop-mode/v1',
73 '/living-tree/snapshot',
74 array(
75 'methods' => WP_REST_Server::READABLE,
76 'callback' => 'openstation_living_tree_rest_snapshot',
77 'permission_callback' => 'openstation_living_tree_user_can_use',
78 )
79 );
80 }
81 add_action( 'rest_api_init', 'openstation_living_tree_register_routes' );
82
83 /**
84 * GET /living-tree/snapshot — cached snapshot response.
85 *
86 * @return WP_REST_Response
87 */
88 function openstation_living_tree_rest_snapshot() {
89 $snapshot = get_transient( OPENSTATION_LIVING_TREE_CACHE_KEY );
90 // A cache entry without the gated value was built before it existed,
91 // and could hand a Jetpack total to the wrong caller: build it again.
92 if ( ! is_array( $snapshot ) || ! array_key_exists( OPENSTATION_LIVING_TREE_GATED_TRAFFIC_KEY, $snapshot ) ) {
93 $snapshot = openstation_living_tree_build_cache_entry();
94 set_transient(
95 OPENSTATION_LIVING_TREE_CACHE_KEY,
96 $snapshot,
97 OPENSTATION_LIVING_TREE_CACHE_TTL
98 );
99 }
100
101 return rest_ensure_response( openstation_living_tree_snapshot_for_caller( $snapshot ) );
102 }
103
104 /**
105 * The snapshot as the current caller may read it.
106 *
107 * On a site where Jetpack Stats is counting, `traffic` is the sum of
108 * the daily rows the site-views widget serves only behind Jetpack's
109 * stats gate, and a total over withheld rows gives away what the rows
110 * hide. A caller outside that gate gets the value the cache entry
111 * keeps for them instead, resolved without Jetpack, see
112 * {@see openstation_living_tree_build_cache_entry()}.
113 *
114 * Runs on the way out, after the cache, so the transient stays the
115 * same for every caller and nothing is read again per request.
116 *
117 * @param array $snapshot The cache entry.
118 * @return array The snapshot as served, without the gated value's key.
119 */
120 function openstation_living_tree_snapshot_for_caller( $snapshot ) {
121 $gated = isset( $snapshot[ OPENSTATION_LIVING_TREE_GATED_TRAFFIC_KEY ] )
122 ? (int) $snapshot[ OPENSTATION_LIVING_TREE_GATED_TRAFFIC_KEY ]
123 : 0;
124 unset( $snapshot[ OPENSTATION_LIVING_TREE_GATED_TRAFFIC_KEY ] );
125
126 if ( ! openstation_site_views_user_can_read_jetpack() ) {
127 $snapshot['traffic'] = $gated;
128 }
129 return $snapshot;
130 }
131
132 /**
133 * Build the snapshot as it is cached: the served snapshot plus the
134 * `traffic` a caller outside Jetpack's stats gate gets.
135 *
136 * Where Jetpack Stats is on, that value comes from the traffic ladder
137 * without Jetpack (the post-views meta, then the
138 * `openstation_living_tree_traffic` filter) and goes through the
139 * `openstation_living_tree_snapshot` filter like the served one, so the
140 * filter runs twice per build. Elsewhere the two are the same value.
141 *
142 * Both are worked out here, once per cache build, so serving a caller
143 * outside the gate costs no reads.
144 *
145 * @return array
146 */
147 function openstation_living_tree_build_cache_entry() {
148 $fields = openstation_living_tree_snapshot_fields();
149 $snapshot = openstation_living_tree_filter_snapshot( $fields );
150 $gated = $snapshot['traffic'] ?? 0;
151
152 if ( openstation_site_views_jetpack_stats_active() ) {
153 $fields['traffic'] = openstation_living_tree_traffic( false );
154 $gated = openstation_living_tree_filter_snapshot( $fields )['traffic'] ?? 0;
155 }
156
157 $snapshot[ OPENSTATION_LIVING_TREE_GATED_TRAFFIC_KEY ] = max( 0, (int) $gated );
158 return $snapshot;
159 }
160
161 /**
162 * Invalidate the cached snapshot. Wired to the content-mutation hooks so
163 * the tree re-DNAs on the next load after the site changes.
164 */
165 function openstation_living_tree_flush_cache() {
166 delete_transient( OPENSTATION_LIVING_TREE_CACHE_KEY );
167 }
168 add_action( 'save_post', 'openstation_living_tree_flush_cache' );
169 add_action( 'deleted_post', 'openstation_living_tree_flush_cache' );
170 add_action( 'comment_post', 'openstation_living_tree_flush_cache' );
171
172 /**
173 * Build the compact site DNA snapshot.
174 *
175 * Aggregates only. Every metric is capped / normalised so any topology
176 * yields a well-formed snapshot — the golden rule (WordPress emits
177 * hormones, never geometry) lives here: this function must never leak a
178 * per-post coordinate or identity into the payload.
179 *
180 * @return array The snapshot, matching the JS `TreeSnapshot` shape.
181 */
182 function openstation_living_tree_build_snapshot() {
183 return openstation_living_tree_filter_snapshot( openstation_living_tree_snapshot_fields() );
184 }
185
186 /**
187 * The snapshot's fields as read from the site, before the
188 * `openstation_living_tree_snapshot` filter.
189 *
190 * @return array
191 */
192 function openstation_living_tree_snapshot_fields() {
193 $posts = wp_count_posts( 'post' );
194 $pages = wp_count_posts( 'page' );
195 $comments = wp_count_comments();
196
197 $categories = wp_count_terms( array( 'taxonomy' => 'category' ) );
198 $tags = wp_count_terms( array( 'taxonomy' => 'post_tag' ) );
199
200 return array(
201 'siteUrl' => (string) home_url(),
202 'siteName' => (string) get_bloginfo( 'name' ),
203 'installEpoch' => openstation_living_tree_install_epoch(),
204 'siteAgeDays' => openstation_living_tree_site_age_days(),
205 'totalPosts' => isset( $posts->publish ) ? (int) $posts->publish : 0,
206 'totalPages' => isset( $pages->publish ) ? (int) $pages->publish : 0,
207 'totalCategories' => is_wp_error( $categories ) ? 0 : (int) $categories,
208 'totalTags' => is_wp_error( $tags ) ? 0 : (int) $tags,
209 'totalComments' => isset( $comments->approved ) ? (int) $comments->approved : 0,
210 'activeUsers' => openstation_living_tree_active_users(),
211 'traffic' => openstation_living_tree_traffic(),
212 'seoHealth' => openstation_living_tree_seo_health(),
213 'performance' => openstation_living_tree_performance(),
214 'branches' => openstation_living_tree_branch_dna(),
215 );
216 }
217
218 /**
219 * Run the snapshot filter over a set of fields.
220 *
221 * @param array $snapshot The fields from {@see openstation_living_tree_snapshot_fields()}.
222 * @return array
223 */
224 function openstation_living_tree_filter_snapshot( $snapshot ) {
225 /**
226 * Filter the Living Tree snapshot before it is cached and served.
227 * Keep the shape intact — the JS client validates nothing; it trusts
228 * this contract. Aggregates only: never add per-post identities or
229 * coordinates (the golden rule).
230 *
231 * @param array $snapshot The compact site DNA.
232 */
233 return apply_filters( 'openstation_living_tree_snapshot', $snapshot );
234 }
235