PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.1
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
desktop-mode / includes / sticky-notes / heartbeat.php

heartbeat.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.1, at includes/sticky-notes/heartbeat.php

260 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Sticky notes Heartbeat sync.
4 *
5 * Gutenberg's Guidelines experiment owns the `wp_guideline` CPT and
6 * `wp_guideline_type` taxonomy. OpenStation only mirrors sticky
7 * artifacts into the shell: the client subscribes with the sticky term
8 * id, known guideline ids, and a high-water modified timestamp; the
9 * server responds with changed sticky guidelines plus removals.
10 *
11 * @package OpenStation
12 */
13
14 defined( 'ABSPATH' ) || exit;
15
16 const OPENSTATION_STICKY_NOTES_POST_TYPE = 'wp_guideline';
17 const OPENSTATION_STICKY_NOTES_TAXONOMY = 'wp_guideline_type';
18
19 /**
20 * Whether the sticky-notes surface is available on this site.
21 *
22 * Sticky notes are backed by Gutenberg's Guidelines experiment, which
23 * registers the `wp_guideline` CPT and `wp_guideline_type` taxonomy
24 * (exposed at `wp/v2/guidelines` + `wp/v2/wp_guideline_type`) only when
25 * the experiment is enabled — Gutenberg plugin 22.7+, opt-in under
26 * Gutenberg → Experiments. Without it those REST routes return 404, so
27 * the shell skips booting the sticky-notes layer entirely and the
28 * Heartbeat handler short-circuits.
29 *
30 * @return bool True when both the guideline CPT and taxonomy are registered.
31 */
32 function openstation_sticky_notes_is_available() {
33 $available = post_type_exists( OPENSTATION_STICKY_NOTES_POST_TYPE )
34 && taxonomy_exists( OPENSTATION_STICKY_NOTES_TAXONOMY );
35
36 /**
37 * Filters whether the sticky-notes surface is available.
38 *
39 * Lets a site force sticky notes off (return `false`) or wire the
40 * layer up to a different guidelines-compatible backend.
41 *
42 * @param bool $available Whether the guideline CPT + taxonomy are registered.
43 */
44 return (bool) apply_filters( 'openstation_sticky_notes_available', $available );
45 }
46
47 /**
48 * Add sticky-note deltas to the Heartbeat response.
49 *
50 * @param array $response Pre-filtered response.
51 * @param array $data Client-sent payload.
52 * @return array
53 */
54 function openstation_sticky_notes_heartbeat_received( $response, $data ) {
55 if ( ! is_array( $response ) ) {
56 $response = array();
57 }
58 if ( empty( $data['openstation_sticky_notes_subscribe'] ) || ! is_array( $data['openstation_sticky_notes_subscribe'] ) ) {
59 return $response;
60 }
61 if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) {
62 return $response;
63 }
64 if ( ! openstation_sticky_notes_is_available() ) {
65 return $response;
66 }
67
68 $sub = $data['openstation_sticky_notes_subscribe'];
69 $sticky_term_id = isset( $sub['stickyTermId'] ) ? absint( $sub['stickyTermId'] ) : 0;
70 if ( $sticky_term_id <= 0 || ! term_exists( $sticky_term_id, OPENSTATION_STICKY_NOTES_TAXONOMY ) ) {
71 return $response;
72 }
73
74 $known_ids = isset( $sub['knownIds'] ) && is_array( $sub['knownIds'] )
75 ? array_values( array_unique( array_filter( array_map( 'absint', $sub['knownIds'] ) ) ) )
76 : array();
77 $known_ids = array_slice( $known_ids, 0, 500 );
78
79 $version = isset( $sub['version'] ) ? max( 0, (int) $sub['version'] ) : 0;
80
81 $response['openstation_sticky_notes'] = openstation_sticky_notes_compute_heartbeat_delta(
82 $sticky_term_id,
83 $known_ids,
84 $version,
85 100
86 );
87
88 return $response;
89 }
90 add_filter( 'heartbeat_received', 'openstation_sticky_notes_heartbeat_received', 5, 2 );
91
92 /**
93 * Compute sticky-note Heartbeat deltas.
94 *
95 * @param int $sticky_term_id Sticky term id.
96 * @param int[] $known_ids Guideline ids currently known by the client.
97 * @param int $version Last-seen modified timestamp in milliseconds.
98 * @param int $cap Maximum notes to send in one tick.
99 * @return array
100 */
101 function openstation_sticky_notes_compute_heartbeat_delta( $sticky_term_id, $known_ids, $version, $cap ) {
102 $cap = max( 1, (int) $cap );
103 $query_ids = openstation_sticky_notes_query_changed_ids( $sticky_term_id, $version, $cap + 1 );
104 $truncated = count( $query_ids ) > $cap;
105 if ( $truncated ) {
106 $query_ids = array_slice( $query_ids, 0, $cap );
107 }
108
109 $notes = array();
110 foreach ( $query_ids as $post_id ) {
111 if ( ! current_user_can( 'edit_post', $post_id ) ) {
112 continue;
113 }
114 $post = get_post( $post_id );
115 if ( $post instanceof WP_Post ) {
116 $notes[] = openstation_sticky_notes_shape_guideline( $post );
117 }
118 }
119
120 $alive_ids = openstation_sticky_notes_alive_known_ids( $sticky_term_id, $known_ids );
121 $removed = array_values( array_diff( array_map( 'intval', $known_ids ), $alive_ids ) );
122
123 return array(
124 'notes' => $notes,
125 'removed' => $removed,
126 'serverTimeMs' => (int) floor( microtime( true ) * 1000 ),
127 'truncated' => $truncated,
128 );
129 }
130
131 /**
132 * Query sticky guideline ids modified since the client's high-water mark.
133 *
134 * @param int $sticky_term_id Sticky term id.
135 * @param int $version Last-seen modified timestamp in milliseconds.
136 * @param int $limit Max ids to return.
137 * @return int[]
138 */
139 function openstation_sticky_notes_query_changed_ids( $sticky_term_id, $version, $limit ) {
140 $args = array(
141 'post_type' => OPENSTATION_STICKY_NOTES_POST_TYPE,
142 'post_status' => 'private',
143 'posts_per_page' => max( 1, (int) $limit ),
144 'fields' => 'ids',
145 'orderby' => 'modified',
146 'order' => 'ASC',
147 'no_found_rows' => true,
148 'tax_query' => array(
149 array(
150 'taxonomy' => OPENSTATION_STICKY_NOTES_TAXONOMY,
151 'field' => 'term_id',
152 'terms' => array( (int) $sticky_term_id ),
153 ),
154 ),
155 );
156 if ( $version > 0 ) {
157 $args['date_query'] = array(
158 array(
159 'column' => 'post_modified_gmt',
160 'after' => gmdate( 'Y-m-d H:i:s', (int) floor( $version / 1000 ) ),
161 'inclusive' => true,
162 ),
163 );
164 }
165
166 $query = new WP_Query( $args );
167 $ids = array_map( 'intval', (array) $query->posts );
168 wp_reset_postdata();
169
170 return $ids;
171 }
172
173 /**
174 * Return the subset of known ids that still point at visible sticky notes.
175 *
176 * @param int $sticky_term_id Sticky term id.
177 * @param int[] $known_ids Client-known guideline ids.
178 * @return int[]
179 */
180 function openstation_sticky_notes_alive_known_ids( $sticky_term_id, $known_ids ) {
181 $known_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $known_ids ) ) ) );
182 if ( empty( $known_ids ) ) {
183 return array();
184 }
185
186 $query = new WP_Query(
187 array(
188 'post_type' => OPENSTATION_STICKY_NOTES_POST_TYPE,
189 'post_status' => 'private',
190 'post__in' => $known_ids,
191 'posts_per_page' => count( $known_ids ),
192 'fields' => 'ids',
193 'no_found_rows' => true,
194 'tax_query' => array(
195 array(
196 'taxonomy' => OPENSTATION_STICKY_NOTES_TAXONOMY,
197 'field' => 'term_id',
198 'terms' => array( (int) $sticky_term_id ),
199 ),
200 ),
201 )
202 );
203 $ids = array();
204 foreach ( (array) $query->posts as $post_id ) {
205 $post_id = (int) $post_id;
206 if ( current_user_can( 'edit_post', $post_id ) ) {
207 $ids[] = $post_id;
208 }
209 }
210 wp_reset_postdata();
211
212 return $ids;
213 }
214
215 /**
216 * Shape a guideline like the Gutenberg REST endpoint's edit context.
217 *
218 * @param WP_Post $post Guideline post.
219 * @return array
220 */
221 function openstation_sticky_notes_shape_guideline( $post ) {
222 $term_ids = wp_get_object_terms(
223 $post->ID,
224 OPENSTATION_STICKY_NOTES_TAXONOMY,
225 array( 'fields' => 'ids' )
226 );
227 if ( is_wp_error( $term_ids ) ) {
228 $term_ids = array();
229 }
230
231 return array(
232 'id' => (int) $post->ID,
233 'slug' => (string) $post->post_name,
234 'status' => (string) get_post_status( $post ),
235 'title' => array(
236 'raw' => (string) get_post_field( 'post_title', $post, 'raw' ),
237 ),
238 'content' => array(
239 'raw' => (string) get_post_field( 'post_content', $post, 'raw' ),
240 ),
241 'excerpt' => array(
242 'raw' => (string) get_post_field( 'post_excerpt', $post, 'raw' ),
243 ),
244 'modified' => mysql_to_rfc3339( $post->post_modified ),
245 'openstation_modified_ms' => openstation_sticky_notes_modified_ms( $post ),
246 'link' => (string) get_permalink( $post ),
247 'wp_guideline_type' => array_values( array_map( 'intval', (array) $term_ids ) ),
248 );
249 }
250
251 /**
252 * Modified timestamp in milliseconds, derived from GMT post time.
253 *
254 * @param WP_Post $post Post.
255 * @return int
256 */
257 function openstation_sticky_notes_modified_ms( $post ) {
258 return (int) get_post_modified_time( 'U', true, $post ) * 1000;
259 }
260