PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.7
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 / notes / rest.php

rest.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.7, at includes/notes/rest.php

629 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Pinned notes REST routes.
4 *
5 * Routes under `/desktop-mode/v1/notes`:
6 *
7 * GET /notes List the viewer's own notes
8 * (private + publish) plus every
9 * other user's public notes.
10 * POST /notes Create a note (author = viewer).
11 * PATCH /notes/(?P<id>\d+) Partial update — owner only.
12 * DELETE /notes/(?P<id>\d+) Soft-trash — owner only.
13 * POST /notes/(?P<id>\d+)/restore Untrash (Undo toast) — owner only.
14 * POST /notes/(?P<id>\d+)/convert Spawn a draft post from the note,
15 * then trash the note — owner only,
16 * requires the `edit_posts` cap.
17 *
18 * Ownership model: a note belongs to its `post_author`, and ONLY the
19 * owner may mutate it — deliberately including administrators. Public
20 * notes are a read-only broadcast surface; "manage other users'
21 * notes" is not a supported operation through this controller.
22 *
23 * Optimistic concurrency: PATCH accepts an `updatedAtMs` field
24 * carrying the client's last-seen modified timestamp; a mismatch
25 * returns 409 `desktop_mode_notes_conflict` with the server copy in
26 * `data.current` so the client can re-render instead of clobbering.
27 *
28 * @package WPDesktopMode
29 * @since 0.9.6
30 */
31
32 defined( 'ABSPATH' ) || exit;
33
34 /**
35 * Base permission: logged-in + desktop mode enabled.
36 *
37 * @since 0.9.6
38 *
39 * @return true|WP_Error
40 */
41 function desktop_mode_notes_rest_permission() {
42 if ( ! is_user_logged_in() ) {
43 return new WP_Error( 'desktop_mode_notes_unauthenticated', __( 'You must be logged in.', 'desktop-mode' ), array( 'status' => 401 ) );
44 }
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 ) );
47 }
48 return true;
49 }
50
51 /**
52 * Register the routes.
53 *
54 * @since 0.9.6
55 */
56 function desktop_mode_notes_register_rest_routes() {
57 $ns = 'desktop-mode/v1';
58
59 register_rest_route(
60 $ns,
61 '/notes',
62 array(
63 array(
64 'methods' => WP_REST_Server::READABLE,
65 'permission_callback' => 'desktop_mode_notes_rest_permission',
66 'callback' => 'desktop_mode_notes_rest_list',
67 ),
68 array(
69 'methods' => WP_REST_Server::CREATABLE,
70 'permission_callback' => 'desktop_mode_notes_rest_permission',
71 'callback' => 'desktop_mode_notes_rest_create',
72 'args' => array(
73 'text' => array(
74 'type' => 'string',
75 'default' => '',
76 'sanitize_callback' => 'sanitize_textarea_field',
77 ),
78 'color' => array(
79 'type' => 'string',
80 'default' => 'butter',
81 'sanitize_callback' => 'desktop_mode_notes_sanitize_color',
82 ),
83 'x' => array( 'type' => 'number', 'default' => 0.1 ),
84 'y' => array( 'type' => 'number', 'default' => 0.1 ),
85 'public' => array( 'type' => 'boolean', 'default' => false ),
86 'seed' => array(
87 'type' => 'integer',
88 'default' => 0,
89 'sanitize_callback' => 'absint',
90 ),
91 ),
92 ),
93 )
94 );
95
96 register_rest_route(
97 $ns,
98 '/notes/(?P<id>\d+)',
99 array(
100 array(
101 'methods' => WP_REST_Server::EDITABLE,
102 'permission_callback' => 'desktop_mode_notes_rest_permission',
103 'callback' => 'desktop_mode_notes_rest_update',
104 ),
105 array(
106 'methods' => WP_REST_Server::DELETABLE,
107 'permission_callback' => 'desktop_mode_notes_rest_permission',
108 'callback' => 'desktop_mode_notes_rest_delete',
109 ),
110 )
111 );
112
113 register_rest_route(
114 $ns,
115 '/notes/(?P<id>\d+)/restore',
116 array(
117 'methods' => WP_REST_Server::CREATABLE,
118 'permission_callback' => 'desktop_mode_notes_rest_permission',
119 'callback' => 'desktop_mode_notes_rest_restore',
120 )
121 );
122
123 register_rest_route(
124 $ns,
125 '/notes/(?P<id>\d+)/convert',
126 array(
127 'methods' => WP_REST_Server::CREATABLE,
128 'permission_callback' => 'desktop_mode_notes_rest_permission',
129 'callback' => 'desktop_mode_notes_rest_convert',
130 )
131 );
132 }
133 add_action( 'rest_api_init', 'desktop_mode_notes_register_rest_routes' );
134
135 /**
136 * Fetch a note post, or a WP_Error when it doesn't exist / isn't a note.
137 *
138 * @since 0.9.6
139 *
140 * @param int $id Post ID.
141 * @param bool $allow_trash Whether a trashed note is acceptable (restore path).
142 * @return WP_Post|WP_Error
143 */
144 function desktop_mode_notes_get_note( $id, $allow_trash = false ) {
145 $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 }
149 $allowed = $allow_trash ? array( 'private', 'publish', 'trash' ) : array( 'private', 'publish' );
150 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 ) );
152 }
153 return $post;
154 }
155
156 /**
157 * Owner gate. Only the note's author may mutate it — including admins.
158 *
159 * Returning 404 (not 403) for other users' PRIVATE notes would leak
160 * less, but the id namespace is shared with public notes anyway and
161 * a mutation attempt on a visible public note deserves an honest 403.
162 *
163 * @since 0.9.6
164 *
165 * @param WP_Post $post Note post.
166 * @return true|WP_Error
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 ) );
171 }
172 return true;
173 }
174
175 /**
176 * Modified timestamp in milliseconds (GMT).
177 *
178 * Second precision (WordPress stores no sub-second post dates) — the
179 * client treats the value as an opaque token and echoes it back.
180 *
181 * @since 0.9.6
182 *
183 * @param WP_Post $post Post.
184 * @return int
185 */
186 function desktop_mode_notes_modified_ms( $post ) {
187 return (int) get_post_modified_time( 'U', true, $post ) * 1000;
188 }
189
190 /**
191 * Serialize a note for the wire.
192 *
193 * @since 0.9.6
194 *
195 * @param WP_Post $post Note post.
196 * @return array
197 */
198 function desktop_mode_notes_prepare( $post ) {
199 $owner_id = (int) $post->post_author;
200 $owner = get_userdata( $owner_id );
201
202 return array(
203 'id' => (int) $post->ID,
204 '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 ) ),
208 'z' => (int) get_post_meta( $post->ID, '_wpd_note_z', true ),
209 'public' => 'publish' === $post->post_status,
210 'seed' => (int) get_post_meta( $post->ID, '_wpd_note_seed', true ),
211 'ownerId' => $owner_id,
212 'ownerName' => $owner instanceof WP_User ? (string) $owner->display_name : '',
213 'ownerAvatar' => (string) get_avatar_url( $owner_id, array( 'size' => 48 ) ),
214 'canEdit' => get_current_user_id() === $owner_id,
215 'updatedAtMs' => desktop_mode_notes_modified_ms( $post ),
216 );
217 }
218
219 /**
220 * Derive the post title from the note text (first non-empty line).
221 *
222 * Only used for admin-side lists / exports — the shell never shows it.
223 *
224 * @since 0.9.6
225 *
226 * @param string $text Note text.
227 * @return string
228 */
229 function desktop_mode_notes_derive_title( $text ) {
230 foreach ( preg_split( '/\r\n|\r|\n/', (string) $text ) as $line ) {
231 $line = trim( $line );
232 if ( '' !== $line ) {
233 return mb_substr( sanitize_text_field( $line ), 0, 80 );
234 }
235 }
236 return __( 'Note', 'desktop-mode' );
237 }
238
239 /**
240 * GET /notes — own notes (private + publish) ∪ others' publish.
241 *
242 * @since 0.9.6
243 *
244 * @return WP_REST_Response
245 */
246 function desktop_mode_notes_rest_list() {
247 $user_id = get_current_user_id();
248
249 // Newest first: the per-half cap exists as a runaway guard, and
250 // when it ever bites it must drop the OLDEST notes — capping an
251 // ascending list would silently hide every recently pinned note
252 // (and the boot high-water would stop the Heartbeat delta from
253 // ever backfilling them).
254 $own = new WP_Query(
255 array(
256 'post_type' => DESKTOP_MODE_NOTES_POST_TYPE,
257 'post_status' => array( 'private', 'publish' ),
258 'author' => $user_id,
259 'posts_per_page' => 200,
260 'orderby' => 'date',
261 'order' => 'DESC',
262 'no_found_rows' => true,
263 )
264 );
265
266 $public = new WP_Query(
267 array(
268 'post_type' => DESKTOP_MODE_NOTES_POST_TYPE,
269 'post_status' => 'publish',
270 'author__not_in' => array( $user_id ),
271 'posts_per_page' => 200,
272 'orderby' => 'date',
273 'order' => 'DESC',
274 'no_found_rows' => true,
275 )
276 );
277
278 $notes = array();
279 foreach ( array_merge( (array) $own->posts, (array) $public->posts ) as $post ) {
280 $notes[] = desktop_mode_notes_prepare( $post );
281 }
282 wp_reset_postdata();
283
284 return rest_ensure_response( array( 'notes' => $notes ) );
285 }
286
287 /**
288 * POST /notes.
289 *
290 * @since 0.9.6
291 *
292 * @param WP_REST_Request $request Request.
293 * @return WP_REST_Response|WP_Error
294 */
295 function desktop_mode_notes_rest_create( $request ) {
296 /**
297 * Filters whether the current user may create a note.
298 *
299 * Notes default to any logged-in desktop-mode user — including
300 * publishing PUBLIC notes onto every other user's wallpaper.
301 * Sites that want to restrict that (by role, capability, or the
302 * request's `public` flag) hook here.
303 *
304 * @since 0.9.6
305 *
306 * @param bool $can_create Whether creation is allowed. Default true.
307 * @param int $user_id Current user id.
308 * @param WP_REST_Request $request The create request (inspect `public`, `text`, ...).
309 */
310 $can_create = apply_filters( 'desktop_mode_notes_user_can_create', true, get_current_user_id(), $request );
311 if ( ! $can_create ) {
312 return new WP_Error( 'desktop_mode_notes_forbidden', __( 'You are not allowed to create notes.', 'desktop-mode' ), array( 'status' => 403 ) );
313 }
314
315 $text = sanitize_textarea_field( (string) $request['text'] );
316
317 $post_id = wp_insert_post(
318 array(
319 'post_type' => DESKTOP_MODE_NOTES_POST_TYPE,
320 'post_status' => $request['public'] ? 'publish' : 'private',
321 'post_author' => get_current_user_id(),
322 'post_title' => desktop_mode_notes_derive_title( $text ),
323 'post_content' => $text,
324 ),
325 true
326 );
327 if ( is_wp_error( $post_id ) ) {
328 $post_id->add_data( array( 'status' => 500 ) );
329 return $post_id;
330 }
331
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() );
336 // The jitter seed is written ONCE, here — PATCH never touches it,
337 // so editing a note's text never re-tilts its paper. The client
338 // sends its own text hash (keeps the optimistic render identical);
339 // fall back to a server-side hash when absent.
340 $seed = absint( $request['seed'] );
341 if ( 0 === $seed ) {
342 $seed = absint( crc32( $text ) ) % 2147483647;
343 $seed = $seed > 0 ? $seed : 1;
344 }
345 update_post_meta( $post_id, '_wpd_note_seed', $seed );
346
347 return rest_ensure_response( desktop_mode_notes_prepare( get_post( $post_id ) ) );
348 }
349
350 /**
351 * Next z-order value across all live (non-trashed) notes.
352 *
353 * Deliberately site-wide, not per-owner: public notes from different
354 * owners stack on the same wall, so a fresh note must land above
355 * everyone's papers. Cheap max-of-meta walk — note counts are tiny
356 * (a wall of paper, not a database of record).
357 *
358 * @since 0.9.6
359 *
360 * @return int
361 */
362 function desktop_mode_notes_next_z() {
363 global $wpdb;
364 $max = $wpdb->get_var(
365 $wpdb->prepare(
366 "SELECT MAX( CAST( pm.meta_value AS UNSIGNED ) )
367 FROM {$wpdb->postmeta} pm
368 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
369 WHERE pm.meta_key = %s AND p.post_type = %s AND p.post_status IN ( 'private', 'publish' )",
370 '_wpd_note_z',
371 DESKTOP_MODE_NOTES_POST_TYPE
372 )
373 );
374 return (int) $max + 1;
375 }
376
377 /**
378 * PATCH /notes/:id — partial update, owner only.
379 *
380 * @since 0.9.6
381 *
382 * @param WP_REST_Request $request Request.
383 * @return WP_REST_Response|WP_Error
384 */
385 function desktop_mode_notes_rest_update( $request ) {
386 $post = desktop_mode_notes_get_note( $request['id'] );
387 if ( is_wp_error( $post ) ) {
388 return $post;
389 }
390 $owner = desktop_mode_notes_require_owner( $post );
391 if ( is_wp_error( $owner ) ) {
392 return $owner;
393 }
394
395 // Optimistic concurrency — a stale token means another session
396 // (or device) changed the note since this client last saw it.
397 $client_ms = $request['updatedAtMs'];
398 if ( null !== $client_ms && (int) $client_ms !== desktop_mode_notes_modified_ms( $post ) ) {
399 return new WP_Error(
400 'desktop_mode_notes_conflict',
401 __( 'The note was changed by another session.', 'desktop-mode' ),
402 array(
403 'status' => 409,
404 'current' => desktop_mode_notes_prepare( $post ),
405 )
406 );
407 }
408
409 $update = array( 'ID' => $post->ID );
410
411 if ( null !== $request['text'] ) {
412 $text = sanitize_textarea_field( (string) $request['text'] );
413 $update['post_content'] = $text;
414 $update['post_title'] = desktop_mode_notes_derive_title( $text );
415 }
416 if ( null !== $request['public'] ) {
417 $update['post_status'] = rest_sanitize_boolean( $request['public'] ) ? 'publish' : 'private';
418 }
419
420 if ( null !== $request['color'] ) {
421 update_post_meta( $post->ID, '_wpd_note_color', desktop_mode_notes_sanitize_color( $request['color'] ) );
422 }
423 if ( null !== $request['x'] ) {
424 update_post_meta( $post->ID, '_wpd_note_x', desktop_mode_notes_sanitize_fraction( $request['x'] ) );
425 }
426 if ( null !== $request['y'] ) {
427 update_post_meta( $post->ID, '_wpd_note_y', desktop_mode_notes_sanitize_fraction( $request['y'] ) );
428 }
429 if ( null !== $request['z'] ) {
430 update_post_meta( $post->ID, '_wpd_note_z', absint( $request['z'] ) );
431 }
432
433 // Always run the post update — even a meta-only PATCH must bump
434 // `post_modified` so the concurrency token advances and the
435 // Heartbeat delta query sees the move.
436 $result = wp_update_post( $update, true );
437 if ( is_wp_error( $result ) ) {
438 $result->add_data( array( 'status' => 500 ) );
439 return $result;
440 }
441
442 return rest_ensure_response( desktop_mode_notes_prepare( get_post( $post->ID ) ) );
443 }
444
445 /**
446 * DELETE /notes/:id — soft-trash, owner only.
447 *
448 * @since 0.9.6
449 *
450 * @param WP_REST_Request $request Request.
451 * @return WP_REST_Response|WP_Error
452 */
453 function desktop_mode_notes_rest_delete( $request ) {
454 $post = desktop_mode_notes_get_note( $request['id'] );
455 if ( is_wp_error( $post ) ) {
456 return $post;
457 }
458 $owner = desktop_mode_notes_require_owner( $post );
459 if ( is_wp_error( $owner ) ) {
460 return $owner;
461 }
462
463 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 ) );
465 }
466
467 return rest_ensure_response( array( 'trashed' => true, 'id' => (int) $post->ID ) );
468 }
469
470 /**
471 * POST /notes/:id/restore — untrash (Undo), owner only.
472 *
473 * @since 0.9.6
474 *
475 * @param WP_REST_Request $request Request.
476 * @return WP_REST_Response|WP_Error
477 */
478 function desktop_mode_notes_rest_restore( $request ) {
479 $post = desktop_mode_notes_get_note( $request['id'], true );
480 if ( is_wp_error( $post ) ) {
481 return $post;
482 }
483 $owner = desktop_mode_notes_require_owner( $post );
484 if ( is_wp_error( $owner ) ) {
485 return $owner;
486 }
487 if ( 'trash' !== $post->post_status ) {
488 return rest_ensure_response( desktop_mode_notes_prepare( $post ) );
489 }
490
491 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 ) );
493 }
494
495 // If this note was trashed by a "convert to post" action, undoing
496 // the conversion must also discard the draft it spawned — otherwise
497 // Undo would leave the note back on the wall AND a stray draft. The
498 // link is written by the convert route (`_wpd_note_converted_post`)
499 // and consumed once here. Only a still-present draft is trashed; a
500 // draft the user already published or trashed themselves is left be.
501 $converted_post_id = (int) get_post_meta( $post->ID, '_wpd_note_converted_post', true );
502 if ( $converted_post_id > 0 ) {
503 delete_post_meta( $post->ID, '_wpd_note_converted_post' );
504 $draft = get_post( $converted_post_id );
505 if ( $draft instanceof WP_Post && 'draft' === $draft->post_status ) {
506 wp_trash_post( $converted_post_id );
507 }
508 }
509
510 return rest_ensure_response( desktop_mode_notes_prepare( get_post( $post->ID ) ) );
511 }
512
513 /**
514 * Convert a note's plain text into Gutenberg paragraph-block markup.
515 *
516 * Blank lines split paragraphs; single newlines within a paragraph
517 * become `<br>`. The result lands clean in the block editor rather
518 * than as one classic-HTML blob.
519 *
520 * @since 0.9.6
521 *
522 * @param string $text Note text.
523 * @return string Serialized block markup (empty string for empty text).
524 */
525 function desktop_mode_notes_text_to_blocks( $text ) {
526 $text = str_replace( array( "\r\n", "\r" ), "\n", (string) $text );
527 $paragraphs = preg_split( '/\n{2,}/', trim( $text ) );
528 $blocks = array();
529 foreach ( $paragraphs as $paragraph ) {
530 $paragraph = trim( $paragraph, "\n" );
531 if ( '' === $paragraph ) {
532 continue;
533 }
534 $html = nl2br( esc_html( $paragraph ), false );
535 $blocks[] = "<!-- wp:paragraph -->\n<p>{$html}</p>\n<!-- /wp:paragraph -->";
536 }
537 return implode( "\n\n", $blocks );
538 }
539
540 /**
541 * POST /notes/:id/convert — spawn a draft post from a note, then trash
542 * the note. Owner only, and the owner must be able to author posts.
543 *
544 * The note is trashed (not hard-deleted) and linked to its new draft
545 * via `_wpd_note_converted_post` so the standard restore route can undo
546 * both sides of the conversion (see `desktop_mode_notes_rest_restore`).
547 *
548 * @since 0.9.6
549 *
550 * @param WP_REST_Request $request Request.
551 * @return WP_REST_Response|WP_Error
552 */
553 function desktop_mode_notes_rest_convert( $request ) {
554 $post = desktop_mode_notes_get_note( $request['id'] );
555 if ( is_wp_error( $post ) ) {
556 return $post;
557 }
558 $owner = desktop_mode_notes_require_owner( $post );
559 if ( is_wp_error( $owner ) ) {
560 return $owner;
561 }
562 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 ) );
564 }
565
566 $text = (string) get_post_field( 'post_content', $post, 'raw' );
567 $title = desktop_mode_notes_derive_title( $text );
568
569 /**
570 * Filters the arguments used to create the draft post from a note.
571 *
572 * Hook here to change the post type/status, assign a category or
573 * author, or wrap the body in different block markup.
574 *
575 * @since 0.9.6
576 *
577 * @param array $post_args Args passed to `wp_insert_post()`.
578 * @param WP_Post $post The source note.
579 * @param WP_REST_Request $request The convert request.
580 */
581 $post_args = apply_filters(
582 'desktop_mode_notes_convert_post_args',
583 array(
584 'post_type' => 'post',
585 'post_status' => 'draft',
586 'post_author' => (int) $post->post_author,
587 'post_title' => $title,
588 'post_content' => desktop_mode_notes_text_to_blocks( $text ),
589 ),
590 $post,
591 $request
592 );
593
594 $new_post_id = wp_insert_post( $post_args, true );
595 if ( is_wp_error( $new_post_id ) ) {
596 $new_post_id->add_data( array( 'status' => 500 ) );
597 return $new_post_id;
598 }
599
600 // Link the note to its draft BEFORE trashing so restore can reverse
601 // both sides. If the trash fails, roll the draft back so a failed
602 // conversion never leaves an orphan draft behind.
603 update_post_meta( $post->ID, '_wpd_note_converted_post', (int) $new_post_id );
604 if ( ! wp_trash_post( $post->ID ) ) {
605 wp_delete_post( $new_post_id, true );
606 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 ) );
608 }
609
610 /**
611 * Fires after a note has been converted to a draft post.
612 *
613 * @since 0.9.6
614 *
615 * @param int $new_post_id The draft post id.
616 * @param WP_Post $post The source note (now trashed).
617 * @param WP_REST_Request $request The convert request.
618 */
619 do_action( 'desktop_mode_notes_converted', (int) $new_post_id, $post, $request );
620
621 return rest_ensure_response(
622 array(
623 'noteId' => (int) $post->ID,
624 'postId' => (int) $new_post_id,
625 'editUrl' => (string) get_edit_post_link( $new_post_id, 'raw' ),
626 )
627 );
628 }
629