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/notes/rest.php +204 -127 0.9.71.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Pinned notes REST routes.
3 + * OpenStation — Pinned notes REST routes.
4 4 *
5 5 * Routes under `/desktop-mode/v1/notes`:
6 6 *
7 7 * GET /notes List the viewer's own notes
@@ -21,30 +21,27 @@
21 21 * notes" is not a supported operation through this controller.
22 22 *
23 23 * Optimistic concurrency: PATCH accepts an `updatedAtMs` field
24 24 * carrying the client's last-seen modified timestamp; a mismatch
25 - * returns 409 `desktop_mode_notes_conflict` with the server copy in
25 + * returns 409 `openstation_notes_conflict` with the server copy in
26 26 * `data.current` so the client can re-render instead of clobbering.
27 27 *
28 - * @package WPDesktopMode
29 - * @since 0.9.6
28 + * @package OpenStation
30 29 */
31 30
32 31 defined( 'ABSPATH' ) || exit;
33 32
34 33 /**
35 - * Base permission: logged-in + desktop mode enabled.
34 + * Base permission: logged-in + OpenStation enabled.
36 35 *
37 - * @since 0.9.6
38 - *
39 36 * @return true|WP_Error
40 37 */
41 -function desktop_mode_notes_rest_permission() {
38 +function openstation_notes_rest_permission() {
42 39 if ( ! is_user_logged_in() ) {
43 - return new WP_Error( 'desktop_mode_notes_unauthenticated', __( 'You must be logged in.', 'desktop-mode' ), array( 'status' => 401 ) );
40 + return new WP_Error( 'openstation_notes_unauthenticated', __( 'You must be logged in.', 'desktop-mode' ), array( 'status' => 401 ) );
44 41 }
45 - if ( function_exists( 'desktop_mode_is_enabled' ) && ! desktop_mode_is_enabled( get_current_user_id() ) ) {
46 - return new WP_Error( 'desktop_mode_notes_disabled', __( 'Desktop mode is not enabled for this user.', 'desktop-mode' ), array( 'status' => 403 ) );
42 + if ( function_exists( 'openstation_is_enabled' ) && ! openstation_is_enabled( get_current_user_id() ) ) {
43 + return new WP_Error( 'openstation_notes_disabled', __( 'OpenStation is not enabled for this user.', 'desktop-mode' ), array( 'status' => 403 ) );
47 44 }
48 45 return true;
49 46 }
50 47
@@ -49,12 +46,10 @@
49 46 }
50 47
51 48 /**
52 49 * Register the routes.
53 - *
54 - * @since 0.9.6
55 50 */
56 -function desktop_mode_notes_register_rest_routes() {
51 +function openstation_notes_register_rest_routes() {
57 52 $ns = 'desktop-mode/v1';
58 53
59 54 register_rest_route(
60 55 $ns,
@@ -61,15 +56,15 @@
61 56 '/notes',
62 57 array(
63 58 array(
64 59 'methods' => WP_REST_Server::READABLE,
65 - 'permission_callback' => 'desktop_mode_notes_rest_permission',
66 - 'callback' => 'desktop_mode_notes_rest_list',
60 + 'permission_callback' => 'openstation_notes_rest_permission',
61 + 'callback' => 'openstation_notes_rest_list',
67 62 ),
68 63 array(
69 64 'methods' => WP_REST_Server::CREATABLE,
70 - 'permission_callback' => 'desktop_mode_notes_rest_permission',
71 - 'callback' => 'desktop_mode_notes_rest_create',
65 + 'permission_callback' => 'openstation_notes_rest_permission',
66 + 'callback' => 'openstation_notes_rest_create',
72 67 'args' => array(
73 68 'text' => array(
74 69 'type' => 'string',
75 70 'default' => '',
@@ -77,13 +72,22 @@
77 72 ),
78 73 'color' => array(
79 74 'type' => 'string',
80 75 'default' => 'butter',
81 - 'sanitize_callback' => 'desktop_mode_notes_sanitize_color',
76 + 'sanitize_callback' => 'openstation_notes_sanitize_color',
82 77 ),
83 - 'x' => array( 'type' => 'number', 'default' => 0.1 ),
84 - 'y' => array( 'type' => 'number', 'default' => 0.1 ),
85 - 'public' => array( 'type' => 'boolean', 'default' => false ),
78 + 'x' => array(
79 + 'type' => 'number',
80 + 'default' => 0.1,
81 + ),
82 + 'y' => array(
83 + 'type' => 'number',
84 + 'default' => 0.1,
85 + ),
86 + 'public' => array(
87 + 'type' => 'boolean',
88 + 'default' => false,
89 + ),
86 90 'seed' => array(
87 91 'type' => 'integer',
88 92 'default' => 0,
89 93 'sanitize_callback' => 'absint',
@@ -98,15 +102,15 @@
98 102 '/notes/(?P<id>\d+)',
99 103 array(
100 104 array(
101 105 'methods' => WP_REST_Server::EDITABLE,
102 - 'permission_callback' => 'desktop_mode_notes_rest_permission',
103 - 'callback' => 'desktop_mode_notes_rest_update',
106 + 'permission_callback' => 'openstation_notes_rest_permission',
107 + 'callback' => 'openstation_notes_rest_update',
104 108 ),
105 109 array(
106 110 'methods' => WP_REST_Server::DELETABLE,
107 - 'permission_callback' => 'desktop_mode_notes_rest_permission',
108 - 'callback' => 'desktop_mode_notes_rest_delete',
111 + 'permission_callback' => 'openstation_notes_rest_permission',
112 + 'callback' => 'openstation_notes_rest_delete',
109 113 ),
110 114 )
111 115 );
112 116
@@ -114,10 +118,10 @@
114 118 $ns,
115 119 '/notes/(?P<id>\d+)/restore',
116 120 array(
117 121 'methods' => WP_REST_Server::CREATABLE,
118 - 'permission_callback' => 'desktop_mode_notes_rest_permission',
119 - 'callback' => 'desktop_mode_notes_rest_restore',
122 + 'permission_callback' => 'openstation_notes_rest_permission',
123 + 'callback' => 'openstation_notes_rest_restore',
120 124 )
121 125 );
122 126
123 127 register_rest_route(
@@ -124,32 +128,30 @@
124 128 $ns,
125 129 '/notes/(?P<id>\d+)/convert',
126 130 array(
127 131 'methods' => WP_REST_Server::CREATABLE,
128 - 'permission_callback' => 'desktop_mode_notes_rest_permission',
129 - 'callback' => 'desktop_mode_notes_rest_convert',
132 + 'permission_callback' => 'openstation_notes_rest_permission',
133 + 'callback' => 'openstation_notes_rest_convert',
130 134 )
131 135 );
132 136 }
133 -add_action( 'rest_api_init', 'desktop_mode_notes_register_rest_routes' );
137 +add_action( 'rest_api_init', 'openstation_notes_register_rest_routes' );
134 138
135 139 /**
136 140 * Fetch a note post, or a WP_Error when it doesn't exist / isn't a note.
137 141 *
138 - * @since 0.9.6
139 - *
140 142 * @param int $id Post ID.
141 143 * @param bool $allow_trash Whether a trashed note is acceptable (restore path).
142 144 * @return WP_Post|WP_Error
143 145 */
144 -function desktop_mode_notes_get_note( $id, $allow_trash = false ) {
146 +function openstation_notes_get_note( $id, $allow_trash = false ) {
145 147 $post = get_post( (int) $id );
146 - if ( ! $post instanceof WP_Post || DESKTOP_MODE_NOTES_POST_TYPE !== $post->post_type ) {
147 - return new WP_Error( 'desktop_mode_notes_not_found', __( 'Note not found.', 'desktop-mode' ), array( 'status' => 404 ) );
148 + if ( ! $post instanceof WP_Post || OPENSTATION_NOTES_POST_TYPE !== $post->post_type ) {
149 + return new WP_Error( 'openstation_notes_not_found', __( 'Note not found.', 'desktop-mode' ), array( 'status' => 404 ) );
148 150 }
149 151 $allowed = $allow_trash ? array( 'private', 'publish', 'trash' ) : array( 'private', 'publish' );
150 152 if ( ! in_array( $post->post_status, $allowed, true ) ) {
151 - return new WP_Error( 'desktop_mode_notes_not_found', __( 'Note not found.', 'desktop-mode' ), array( 'status' => 404 ) );
153 + return new WP_Error( 'openstation_notes_not_found', __( 'Note not found.', 'desktop-mode' ), array( 'status' => 404 ) );
152 154 }
153 155 return $post;
154 156 }
155 157
@@ -159,16 +161,14 @@
159 161 * Returning 404 (not 403) for other users' PRIVATE notes would leak
160 162 * less, but the id namespace is shared with public notes anyway and
161 163 * a mutation attempt on a visible public note deserves an honest 403.
162 164 *
163 - * @since 0.9.6
164 - *
165 165 * @param WP_Post $post Note post.
166 166 * @return true|WP_Error
167 167 */
168 -function desktop_mode_notes_require_owner( $post ) {
169 - if ( (int) $post->post_author !== get_current_user_id() ) {
170 - return new WP_Error( 'desktop_mode_notes_forbidden', __( 'Only the note owner can change it.', 'desktop-mode' ), array( 'status' => 403 ) );
168 +function openstation_notes_require_owner( $post ) {
169 + if ( get_current_user_id() !== (int) $post->post_author ) {
170 + return new WP_Error( 'openstation_notes_forbidden', __( 'Only the note owner can change it.', 'desktop-mode' ), array( 'status' => 403 ) );
171 171 }
172 172 return true;
173 173 }
174 174
@@ -177,14 +177,12 @@
177 177 *
178 178 * Second precision (WordPress stores no sub-second post dates) — the
179 179 * client treats the value as an opaque token and echoes it back.
180 180 *
181 - * @since 0.9.6
182 - *
183 181 * @param WP_Post $post Post.
184 182 * @return int
185 183 */
186 -function desktop_mode_notes_modified_ms( $post ) {
184 +function openstation_notes_modified_ms( $post ) {
187 185 return (int) get_post_modified_time( 'U', true, $post ) * 1000;
188 186 }
189 187
190 188 /**
@@ -189,14 +187,12 @@
189 187
190 188 /**
191 189 * Serialize a note for the wire.
192 190 *
193 - * @since 0.9.6
194 - *
195 191 * @param WP_Post $post Note post.
196 192 * @return array
197 193 */
198 -function desktop_mode_notes_prepare( $post ) {
194 +function openstation_notes_prepare( $post ) {
199 195 $owner_id = (int) $post->post_author;
200 196 $owner = get_userdata( $owner_id );
201 197
202 198 return array(
@@ -201,11 +197,11 @@
201 197
202 198 return array(
203 199 'id' => (int) $post->ID,
204 200 'text' => (string) get_post_field( 'post_content', $post, 'raw' ),
205 - 'color' => desktop_mode_notes_sanitize_color( get_post_meta( $post->ID, '_wpd_note_color', true ) ),
206 - 'x' => desktop_mode_notes_sanitize_fraction( get_post_meta( $post->ID, '_wpd_note_x', true ) ),
207 - 'y' => desktop_mode_notes_sanitize_fraction( get_post_meta( $post->ID, '_wpd_note_y', true ) ),
201 + 'color' => openstation_notes_sanitize_color( get_post_meta( $post->ID, '_wpd_note_color', true ) ),
202 + 'x' => openstation_notes_sanitize_fraction( get_post_meta( $post->ID, '_wpd_note_x', true ) ),
203 + 'y' => openstation_notes_sanitize_fraction( get_post_meta( $post->ID, '_wpd_note_y', true ) ),
208 204 'z' => (int) get_post_meta( $post->ID, '_wpd_note_z', true ),
209 205 'public' => 'publish' === $post->post_status,
210 206 'seed' => (int) get_post_meta( $post->ID, '_wpd_note_seed', true ),
211 207 'ownerId' => $owner_id,
@@ -211,9 +207,9 @@
211 207 'ownerId' => $owner_id,
212 208 'ownerName' => $owner instanceof WP_User ? (string) $owner->display_name : '',
213 209 'ownerAvatar' => (string) get_avatar_url( $owner_id, array( 'size' => 48 ) ),
214 210 'canEdit' => get_current_user_id() === $owner_id,
215 - 'updatedAtMs' => desktop_mode_notes_modified_ms( $post ),
211 + 'updatedAtMs' => openstation_notes_modified_ms( $post ),
216 212 );
217 213 }
218 214
219 215 /**
@@ -220,14 +216,12 @@
220 216 * Derive the post title from the note text (first non-empty line).
221 217 *
222 218 * Only used for admin-side lists / exports — the shell never shows it.
223 219 *
224 - * @since 0.9.6
225 - *
226 220 * @param string $text Note text.
227 221 * @return string
228 222 */
229 -function desktop_mode_notes_derive_title( $text ) {
223 +function openstation_notes_derive_title( $text ) {
230 224 foreach ( preg_split( '/\r\n|\r|\n/', (string) $text ) as $line ) {
231 225 $line = trim( $line );
232 226 if ( '' !== $line ) {
233 227 return mb_substr( sanitize_text_field( $line ), 0, 80 );
@@ -238,13 +232,11 @@
238 232
239 233 /**
240 234 * GET /notes — own notes (private + publish) ∪ others' publish.
241 235 *
242 - * @since 0.9.6
243 - *
244 236 * @return WP_REST_Response
245 237 */
246 -function desktop_mode_notes_rest_list() {
238 +function openstation_notes_rest_list() {
247 239 $user_id = get_current_user_id();
248 240
249 241 // Newest first: the per-half cap exists as a runaway guard, and
250 242 // when it ever bites it must drop the OLDEST notes — capping an
@@ -252,9 +244,9 @@
252 244 // (and the boot high-water would stop the Heartbeat delta from
253 245 // ever backfilling them).
254 246 $own = new WP_Query(
255 247 array(
256 - 'post_type' => DESKTOP_MODE_NOTES_POST_TYPE,
248 + 'post_type' => OPENSTATION_NOTES_POST_TYPE,
257 249 'post_status' => array( 'private', 'publish' ),
258 250 'author' => $user_id,
259 251 'posts_per_page' => 200,
260 252 'orderby' => 'date',
@@ -264,9 +256,9 @@
264 256 );
265 257
266 258 $public = new WP_Query(
267 259 array(
268 - 'post_type' => DESKTOP_MODE_NOTES_POST_TYPE,
260 + 'post_type' => OPENSTATION_NOTES_POST_TYPE,
269 261 'post_status' => 'publish',
270 262 'author__not_in' => array( $user_id ),
271 263 'posts_per_page' => 200,
272 264 'orderby' => 'date',
@@ -276,9 +268,9 @@
276 268 );
277 269
278 270 $notes = array();
279 271 foreach ( array_merge( (array) $own->posts, (array) $public->posts ) as $post ) {
280 - $notes[] = desktop_mode_notes_prepare( $post );
272 + $notes[] = openstation_notes_prepare( $post );
281 273 }
282 274 wp_reset_postdata();
283 275
284 276 return rest_ensure_response( array( 'notes' => $notes ) );
@@ -286,31 +278,27 @@
286 278
287 279 /**
288 280 * POST /notes.
289 281 *
290 - * @since 0.9.6
291 - *
292 282 * @param WP_REST_Request $request Request.
293 283 * @return WP_REST_Response|WP_Error
294 284 */
295 -function desktop_mode_notes_rest_create( $request ) {
285 +function openstation_notes_rest_create( $request ) {
296 286 /**
297 287 * Filters whether the current user may create a note.
298 288 *
299 - * Notes default to any logged-in desktop-mode user — including
289 + * Notes default to any logged-in openstation user — including
300 290 * publishing PUBLIC notes onto every other user's wallpaper.
301 291 * Sites that want to restrict that (by role, capability, or the
302 292 * request's `public` flag) hook here.
303 293 *
304 - * @since 0.9.6
305 - *
306 294 * @param bool $can_create Whether creation is allowed. Default true.
307 295 * @param int $user_id Current user id.
308 296 * @param WP_REST_Request $request The create request (inspect `public`, `text`, ...).
309 297 */
310 - $can_create = apply_filters( 'desktop_mode_notes_user_can_create', true, get_current_user_id(), $request );
298 + $can_create = apply_filters( 'openstation_notes_user_can_create', true, get_current_user_id(), $request );
311 299 if ( ! $can_create ) {
312 - return new WP_Error( 'desktop_mode_notes_forbidden', __( 'You are not allowed to create notes.', 'desktop-mode' ), array( 'status' => 403 ) );
300 + return new WP_Error( 'openstation_notes_forbidden', __( 'You are not allowed to create notes.', 'desktop-mode' ), array( 'status' => 403 ) );
313 301 }
314 302
315 303 $text = sanitize_textarea_field( (string) $request['text'] );
316 304
@@ -315,12 +303,12 @@
315 303 $text = sanitize_textarea_field( (string) $request['text'] );
316 304
317 305 $post_id = wp_insert_post(
318 306 array(
319 - 'post_type' => DESKTOP_MODE_NOTES_POST_TYPE,
307 + 'post_type' => OPENSTATION_NOTES_POST_TYPE,
320 308 'post_status' => $request['public'] ? 'publish' : 'private',
321 309 'post_author' => get_current_user_id(),
322 - 'post_title' => desktop_mode_notes_derive_title( $text ),
310 + 'post_title' => openstation_notes_derive_title( $text ),
323 311 'post_content' => $text,
324 312 ),
325 313 true
326 314 );
@@ -328,12 +316,12 @@
328 316 $post_id->add_data( array( 'status' => 500 ) );
329 317 return $post_id;
330 318 }
331 319
332 - update_post_meta( $post_id, '_wpd_note_color', desktop_mode_notes_sanitize_color( $request['color'] ) );
333 - update_post_meta( $post_id, '_wpd_note_x', desktop_mode_notes_sanitize_fraction( $request['x'] ) );
334 - update_post_meta( $post_id, '_wpd_note_y', desktop_mode_notes_sanitize_fraction( $request['y'] ) );
335 - update_post_meta( $post_id, '_wpd_note_z', desktop_mode_notes_next_z() );
320 + update_post_meta( $post_id, '_wpd_note_color', openstation_notes_sanitize_color( $request['color'] ) );
321 + update_post_meta( $post_id, '_wpd_note_x', openstation_notes_sanitize_fraction( $request['x'] ) );
322 + update_post_meta( $post_id, '_wpd_note_y', openstation_notes_sanitize_fraction( $request['y'] ) );
323 + update_post_meta( $post_id, '_wpd_note_z', openstation_notes_next_z() );
336 324 // The jitter seed is written ONCE, here — PATCH never touches it,
337 325 // so editing a note's text never re-tilts its paper. The client
338 326 // sends its own text hash (keeps the optimistic render identical);
339 327 // fall back to a server-side hash when absent.
@@ -343,9 +331,9 @@
343 331 $seed = $seed > 0 ? $seed : 1;
344 332 }
345 333 update_post_meta( $post_id, '_wpd_note_seed', $seed );
346 334
347 - return rest_ensure_response( desktop_mode_notes_prepare( get_post( $post_id ) ) );
335 + return rest_ensure_response( openstation_notes_prepare( get_post( $post_id ) ) );
348 336 }
349 337
350 338 /**
351 339 * Next z-order value across all live (non-trashed) notes.
@@ -354,13 +342,11 @@
354 342 * owners stack on the same wall, so a fresh note must land above
355 343 * everyone's papers. Cheap max-of-meta walk — note counts are tiny
356 344 * (a wall of paper, not a database of record).
357 345 *
358 - * @since 0.9.6
359 - *
360 346 * @return int
361 347 */
362 -function desktop_mode_notes_next_z() {
348 +function openstation_notes_next_z() {
363 349 global $wpdb;
364 350 $max = $wpdb->get_var(
365 351 $wpdb->prepare(
366 352 "SELECT MAX( CAST( pm.meta_value AS UNSIGNED ) )
@@ -367,9 +353,9 @@
367 353 FROM {$wpdb->postmeta} pm
368 354 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
369 355 WHERE pm.meta_key = %s AND p.post_type = %s AND p.post_status IN ( 'private', 'publish' )",
370 356 '_wpd_note_z',
371 - DESKTOP_MODE_NOTES_POST_TYPE
357 + OPENSTATION_NOTES_POST_TYPE
372 358 )
373 359 );
374 360 return (int) $max + 1;
375 361 }
@@ -376,19 +362,17 @@
376 362
377 363 /**
378 364 * PATCH /notes/:id — partial update, owner only.
379 365 *
380 - * @since 0.9.6
381 - *
382 366 * @param WP_REST_Request $request Request.
383 367 * @return WP_REST_Response|WP_Error
384 368 */
385 -function desktop_mode_notes_rest_update( $request ) {
386 - $post = desktop_mode_notes_get_note( $request['id'] );
369 +function openstation_notes_rest_update( $request ) {
370 + $post = openstation_notes_get_note( $request['id'] );
387 371 if ( is_wp_error( $post ) ) {
388 372 return $post;
389 373 }
390 - $owner = desktop_mode_notes_require_owner( $post );
374 + $owner = openstation_notes_require_owner( $post );
391 375 if ( is_wp_error( $owner ) ) {
392 376 return $owner;
393 377 }
394 378
@@ -394,15 +378,15 @@
394 378
395 379 // Optimistic concurrency — a stale token means another session
396 380 // (or device) changed the note since this client last saw it.
397 381 $client_ms = $request['updatedAtMs'];
398 - if ( null !== $client_ms && (int) $client_ms !== desktop_mode_notes_modified_ms( $post ) ) {
382 + if ( null !== $client_ms && openstation_notes_modified_ms( $post ) !== (int) $client_ms ) {
399 383 return new WP_Error(
400 - 'desktop_mode_notes_conflict',
384 + 'openstation_notes_conflict',
401 385 __( 'The note was changed by another session.', 'desktop-mode' ),
402 386 array(
403 387 'status' => 409,
404 - 'current' => desktop_mode_notes_prepare( $post ),
388 + 'current' => openstation_notes_prepare( $post ),
405 389 )
406 390 );
407 391 }
408 392
@@ -410,9 +394,9 @@
410 394
411 395 if ( null !== $request['text'] ) {
412 396 $text = sanitize_textarea_field( (string) $request['text'] );
413 397 $update['post_content'] = $text;
414 - $update['post_title'] = desktop_mode_notes_derive_title( $text );
398 + $update['post_title'] = openstation_notes_derive_title( $text );
415 399 }
416 400 if ( null !== $request['public'] ) {
417 401 $update['post_status'] = rest_sanitize_boolean( $request['public'] ) ? 'publish' : 'private';
418 402 }
@@ -417,15 +401,15 @@
417 401 $update['post_status'] = rest_sanitize_boolean( $request['public'] ) ? 'publish' : 'private';
418 402 }
419 403
420 404 if ( null !== $request['color'] ) {
421 - update_post_meta( $post->ID, '_wpd_note_color', desktop_mode_notes_sanitize_color( $request['color'] ) );
405 + update_post_meta( $post->ID, '_wpd_note_color', openstation_notes_sanitize_color( $request['color'] ) );
422 406 }
423 407 if ( null !== $request['x'] ) {
424 - update_post_meta( $post->ID, '_wpd_note_x', desktop_mode_notes_sanitize_fraction( $request['x'] ) );
408 + update_post_meta( $post->ID, '_wpd_note_x', openstation_notes_sanitize_fraction( $request['x'] ) );
425 409 }
426 410 if ( null !== $request['y'] ) {
427 - update_post_meta( $post->ID, '_wpd_note_y', desktop_mode_notes_sanitize_fraction( $request['y'] ) );
411 + update_post_meta( $post->ID, '_wpd_note_y', openstation_notes_sanitize_fraction( $request['y'] ) );
428 412 }
429 413 if ( null !== $request['z'] ) {
430 414 update_post_meta( $post->ID, '_wpd_note_z', absint( $request['z'] ) );
431 415 }
@@ -438,59 +422,60 @@
438 422 $result->add_data( array( 'status' => 500 ) );
439 423 return $result;
440 424 }
441 425
442 - return rest_ensure_response( desktop_mode_notes_prepare( get_post( $post->ID ) ) );
426 + return rest_ensure_response( openstation_notes_prepare( get_post( $post->ID ) ) );
443 427 }
444 428
445 429 /**
446 430 * DELETE /notes/:id — soft-trash, owner only.
447 431 *
448 - * @since 0.9.6
449 - *
450 432 * @param WP_REST_Request $request Request.
451 433 * @return WP_REST_Response|WP_Error
452 434 */
453 -function desktop_mode_notes_rest_delete( $request ) {
454 - $post = desktop_mode_notes_get_note( $request['id'] );
435 +function openstation_notes_rest_delete( $request ) {
436 + $post = openstation_notes_get_note( $request['id'] );
455 437 if ( is_wp_error( $post ) ) {
456 438 return $post;
457 439 }
458 - $owner = desktop_mode_notes_require_owner( $post );
440 + $owner = openstation_notes_require_owner( $post );
459 441 if ( is_wp_error( $owner ) ) {
460 442 return $owner;
461 443 }
462 444
463 445 if ( ! wp_trash_post( $post->ID ) ) {
464 - return new WP_Error( 'desktop_mode_notes_trash_failed', __( 'Could not move the note to the trash.', 'desktop-mode' ), array( 'status' => 500 ) );
446 + return new WP_Error( 'openstation_notes_trash_failed', __( 'Could not move the note to the trash.', 'desktop-mode' ), array( 'status' => 500 ) );
465 447 }
466 448
467 - return rest_ensure_response( array( 'trashed' => true, 'id' => (int) $post->ID ) );
449 + return rest_ensure_response(
450 + array(
451 + 'trashed' => true,
452 + 'id' => (int) $post->ID,
453 + )
454 + );
468 455 }
469 456
470 457 /**
471 458 * POST /notes/:id/restore — untrash (Undo), owner only.
472 459 *
473 - * @since 0.9.6
474 - *
475 460 * @param WP_REST_Request $request Request.
476 461 * @return WP_REST_Response|WP_Error
477 462 */
478 -function desktop_mode_notes_rest_restore( $request ) {
479 - $post = desktop_mode_notes_get_note( $request['id'], true );
463 +function openstation_notes_rest_restore( $request ) {
464 + $post = openstation_notes_get_note( $request['id'], true );
480 465 if ( is_wp_error( $post ) ) {
481 466 return $post;
482 467 }
483 - $owner = desktop_mode_notes_require_owner( $post );
468 + $owner = openstation_notes_require_owner( $post );
484 469 if ( is_wp_error( $owner ) ) {
485 470 return $owner;
486 471 }
487 472 if ( 'trash' !== $post->post_status ) {
488 - return rest_ensure_response( desktop_mode_notes_prepare( $post ) );
473 + return rest_ensure_response( openstation_notes_prepare( $post ) );
489 474 }
490 475
491 476 if ( ! wp_untrash_post( $post->ID ) ) {
492 - return new WP_Error( 'desktop_mode_notes_restore_failed', __( 'Could not restore the note.', 'desktop-mode' ), array( 'status' => 500 ) );
477 + return new WP_Error( 'openstation_notes_restore_failed', __( 'Could not restore the note.', 'desktop-mode' ), array( 'status' => 500 ) );
493 478 }
494 479
495 480 // If this note was trashed by a "convert to post" action, undoing
496 481 // the conversion must also discard the draft it spawned — otherwise
@@ -506,9 +491,9 @@
506 491 wp_trash_post( $converted_post_id );
507 492 }
508 493 }
509 494
510 - return rest_ensure_response( desktop_mode_notes_prepare( get_post( $post->ID ) ) );
495 + return rest_ensure_response( openstation_notes_prepare( get_post( $post->ID ) ) );
511 496 }
512 497
513 498 /**
514 499 * Convert a note's plain text into Gutenberg paragraph-block markup.
@@ -516,14 +501,12 @@
516 501 * Blank lines split paragraphs; single newlines within a paragraph
517 502 * become `<br>`. The result lands clean in the block editor rather
518 503 * than as one classic-HTML blob.
519 504 *
520 - * @since 0.9.6
521 - *
522 505 * @param string $text Note text.
523 506 * @return string Serialized block markup (empty string for empty text).
524 507 */
525 -function desktop_mode_notes_text_to_blocks( $text ) {
508 +function openstation_notes_text_to_blocks( $text ) {
526 509 $text = str_replace( array( "\r\n", "\r" ), "\n", (string) $text );
527 510 $paragraphs = preg_split( '/\n{2,}/', trim( $text ) );
528 511 $blocks = array();
529 512 foreach ( $paragraphs as $paragraph ) {
@@ -542,30 +525,28 @@
542 525 * the note. Owner only, and the owner must be able to author posts.
543 526 *
544 527 * The note is trashed (not hard-deleted) and linked to its new draft
545 528 * via `_wpd_note_converted_post` so the standard restore route can undo
546 - * both sides of the conversion (see `desktop_mode_notes_rest_restore`).
529 + * both sides of the conversion (see `openstation_notes_rest_restore`).
547 530 *
548 - * @since 0.9.6
549 - *
550 531 * @param WP_REST_Request $request Request.
551 532 * @return WP_REST_Response|WP_Error
552 533 */
553 -function desktop_mode_notes_rest_convert( $request ) {
554 - $post = desktop_mode_notes_get_note( $request['id'] );
534 +function openstation_notes_rest_convert( $request ) {
535 + $post = openstation_notes_get_note( $request['id'] );
555 536 if ( is_wp_error( $post ) ) {
556 537 return $post;
557 538 }
558 - $owner = desktop_mode_notes_require_owner( $post );
539 + $owner = openstation_notes_require_owner( $post );
559 540 if ( is_wp_error( $owner ) ) {
560 541 return $owner;
561 542 }
562 543 if ( ! current_user_can( 'edit_posts' ) ) {
563 - return new WP_Error( 'desktop_mode_notes_cannot_create_posts', __( 'You are not allowed to create posts.', 'desktop-mode' ), array( 'status' => 403 ) );
544 + return new WP_Error( 'openstation_notes_cannot_create_posts', __( 'You are not allowed to create posts.', 'desktop-mode' ), array( 'status' => 403 ) );
564 545 }
565 546
566 547 $text = (string) get_post_field( 'post_content', $post, 'raw' );
567 - $title = desktop_mode_notes_derive_title( $text );
548 + $title = openstation_notes_derive_title( $text );
568 549
569 550 /**
570 551 * Filters the arguments used to create the draft post from a note.
571 552 *
@@ -571,22 +552,20 @@
571 552 *
572 553 * Hook here to change the post type/status, assign a category or
573 554 * author, or wrap the body in different block markup.
574 555 *
575 - * @since 0.9.6
576 - *
577 556 * @param array $post_args Args passed to `wp_insert_post()`.
578 557 * @param WP_Post $post The source note.
579 558 * @param WP_REST_Request $request The convert request.
580 559 */
581 560 $post_args = apply_filters(
582 - 'desktop_mode_notes_convert_post_args',
561 + 'openstation_notes_convert_post_args',
583 562 array(
584 563 'post_type' => 'post',
585 564 'post_status' => 'draft',
586 565 'post_author' => (int) $post->post_author,
587 566 'post_title' => $title,
588 - 'post_content' => desktop_mode_notes_text_to_blocks( $text ),
567 + 'post_content' => openstation_notes_text_to_blocks( $text ),
589 568 ),
590 569 $post,
591 570 $request
592 571 );
@@ -603,21 +582,19 @@
603 582 update_post_meta( $post->ID, '_wpd_note_converted_post', (int) $new_post_id );
604 583 if ( ! wp_trash_post( $post->ID ) ) {
605 584 wp_delete_post( $new_post_id, true );
606 585 delete_post_meta( $post->ID, '_wpd_note_converted_post' );
607 - return new WP_Error( 'desktop_mode_notes_convert_failed', __( 'Could not convert the note to a post.', 'desktop-mode' ), array( 'status' => 500 ) );
586 + return new WP_Error( 'openstation_notes_convert_failed', __( 'Could not convert the note to a post.', 'desktop-mode' ), array( 'status' => 500 ) );
608 587 }
609 588
610 589 /**
611 590 * Fires after a note has been converted to a draft post.
612 591 *
613 - * @since 0.9.6
614 - *
615 592 * @param int $new_post_id The draft post id.
616 593 * @param WP_Post $post The source note (now trashed).
617 594 * @param WP_REST_Request $request The convert request.
618 595 */
619 - do_action( 'desktop_mode_notes_converted', (int) $new_post_id, $post, $request );
596 + do_action( 'openstation_notes_converted', (int) $new_post_id, $post, $request );
620 597
621 598 return rest_ensure_response(
622 599 array(
623 600 'noteId' => (int) $post->ID,
@@ -625,4 +602,104 @@
625 602 'editUrl' => (string) get_edit_post_link( $new_post_id, 'raw' ),
626 603 )
627 604 );
628 605 }
606 +
607 +/**
608 + * Whether the current user's desktop would show any pinned notes.
609 + *
610 + * The presence hint the boot config ships as `hasNotes`: the notes
611 + * bundle is presence-gated client-side (`src/notes/sentinel.ts`), so
612 + * a user with no notes never downloads it — and never fires the
613 + * boot-time list request the layer used to make unconditionally.
614 + *
615 + * **Steady-state cost: zero queries.** The computed answer is cached
616 + * in user meta, stamped with a revision the site bumps whenever any
617 + * note changes (`openstation_notes_bump_rev()` below). The revision
618 + * lives in an autoloaded option and the user's meta cache is already
619 + * primed on every admin request, so a boot between note changes
620 + * reads two warm caches and touches the database not at all. Only
621 + * the first boot after a note is created / deleted / re-scoped runs
622 + * the probes again — two `fields => ids`, one-row queries at most:
623 + * anyone's public note first, the user's own private ones second,
624 + * mirroring the visibility rule the list route enforces.
625 + *
626 + * @return bool
627 + */
628 +function openstation_notes_user_has_any() {
629 + $rev = (string) get_option( 'desktop_mode_notes_rev', '0' );
630 + $user_id = get_current_user_id();
631 + $cached = (string) get_user_meta( $user_id, '_desktop_mode_has_notes', true );
632 + if ( '' !== $cached ) {
633 + list( $cached_rev, $cached_value ) = array_pad( explode( ':', $cached, 2 ), 2, '' );
634 + if ( $cached_rev === $rev ) {
635 + return '1' === $cached_value;
636 + }
637 + }
638 +
639 + $public = new WP_Query(
640 + array(
641 + 'post_type' => OPENSTATION_NOTES_POST_TYPE,
642 + 'post_status' => 'publish',
643 + 'posts_per_page' => 1,
644 + 'fields' => 'ids',
645 + 'no_found_rows' => true,
646 + )
647 + );
648 + $has = (bool) $public->posts;
649 + if ( ! $has ) {
650 + $own = new WP_Query(
651 + array(
652 + 'post_type' => OPENSTATION_NOTES_POST_TYPE,
653 + 'post_status' => 'private',
654 + 'author' => $user_id,
655 + 'posts_per_page' => 1,
656 + 'fields' => 'ids',
657 + 'no_found_rows' => true,
658 + )
659 + );
660 + $has = (bool) $own->posts;
661 + }
662 +
663 + update_user_meta( $user_id, '_desktop_mode_has_notes', $rev . ':' . ( $has ? '1' : '0' ) );
664 + return $has;
665 +}
666 +
667 +/**
668 + * Invalidate every user's cached `hasNotes` answer.
669 + *
670 + * One autoloaded revision counter instead of per-user cache deletes:
671 + * a public note's existence changes the answer for EVERY user, and
672 + * enumerating users to clear meta would be the expensive thing this
673 + * cache exists to avoid. Bumping the rev makes all stamped answers
674 + * stale at the cost of one option write per note change — and note
675 + * changes are rare next to boots.
676 + *
677 + * @param int|WP_Post $post Post id or object being changed.
678 + * @return void
679 + */
680 +function openstation_notes_bump_rev( $post ) {
681 + $post = get_post( $post );
682 + if ( ! $post instanceof WP_Post || OPENSTATION_NOTES_POST_TYPE !== $post->post_type ) {
683 + return;
684 + }
685 + update_option( 'desktop_mode_notes_rev', (string) time() . '.' . wp_rand( 0, 999 ), true );
686 +}
687 +
688 +/**
689 + * Every path a note can change through: status moves (create,
690 + * publish/private flips, trash, restore) fire
691 + * `transition_post_status`; hard deletes fire `deleted_post`.
692 + *
693 + * @param string $new_status New status.
694 + * @param string $old_status Old status.
695 + * @param WP_Post $post The post.
696 + * @return void
697 + */
698 +function openstation_notes_bump_rev_on_transition( $new_status, $old_status, $post ) {
699 + if ( $new_status === $old_status ) {
700 + return;
701 + }
702 + openstation_notes_bump_rev( $post );
703 +}
704 +add_action( 'transition_post_status', 'openstation_notes_bump_rev_on_transition', 10, 3 );
705 +add_action( 'deleted_post', 'openstation_notes_bump_rev' );