PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.5
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.5
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 0.9.5, at includes/sticky-notes/heartbeat.php

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