PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.6
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 / content-graph / rest.php

rest.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.6, at includes/content-graph/rest.php

462 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Content Graph: REST routes.
4 *
5 * Three endpoints under `desktop-mode/v1/content-graph`:
6 *
7 * GET /post-types
8 * Lists the types eligible for the graph (`slug`, `label`, `icon`,
9 * `count`, `taxonomies`).
10 *
11 * GET /nodes?types=post,page,...
12 * Returns the full `{ nodes, edges, stats }` tuple. Cached server-
13 * side, see graph-builder.php.
14 *
15 * GET /post/<id>
16 * Returns the side-panel detail bundle for one post:
17 * { post: {...}, author, contributors, comments, categories,
18 * attached_media, revisions }.
19 *
20 * @package WPDesktopMode
21 * @since 0.8.2
22 */
23
24 defined( 'ABSPATH' ) || exit;
25
26 /**
27 * Capability check shared across every endpoint.
28 *
29 * @since 0.8.2
30 *
31 * @return bool
32 */
33 function desktop_mode_content_graph_rest_permission() {
34 return desktop_mode_content_graph_user_can_use();
35 }
36
37 /**
38 * Register the routes.
39 *
40 * @since 0.8.2
41 */
42 function desktop_mode_content_graph_register_routes() {
43 register_rest_route(
44 'desktop-mode/v1',
45 '/content-graph/post-types',
46 array(
47 'methods' => WP_REST_Server::READABLE,
48 'callback' => 'desktop_mode_content_graph_rest_post_types',
49 'permission_callback' => 'desktop_mode_content_graph_rest_permission',
50 )
51 );
52 register_rest_route(
53 'desktop-mode/v1',
54 '/content-graph/nodes',
55 array(
56 'methods' => WP_REST_Server::READABLE,
57 'callback' => 'desktop_mode_content_graph_rest_nodes',
58 'permission_callback' => 'desktop_mode_content_graph_rest_permission',
59 'args' => array(
60 'types' => array(
61 'description' => 'Comma-separated list of post type slugs to include.',
62 'type' => 'string',
63 'default' => '',
64 ),
65 ),
66 )
67 );
68 register_rest_route(
69 'desktop-mode/v1',
70 '/content-graph/post/(?P<id>\d+)',
71 array(
72 'methods' => WP_REST_Server::READABLE,
73 'callback' => 'desktop_mode_content_graph_rest_post_detail',
74 'permission_callback' => 'desktop_mode_content_graph_rest_permission',
75 'args' => array(
76 'id' => array(
77 'type' => 'integer',
78 'required' => true,
79 ),
80 ),
81 )
82 );
83 }
84 add_action( 'rest_api_init', 'desktop_mode_content_graph_register_routes' );
85
86 /**
87 * GET /post-types
88 *
89 * @since 0.8.2
90 *
91 * @return WP_REST_Response
92 */
93 function desktop_mode_content_graph_rest_post_types() {
94 $types = desktop_mode_content_graph_post_types();
95 $out = array();
96 foreach ( $types as $entry ) {
97 $slug = isset( $entry['slug'] ) ? (string) $entry['slug'] : '';
98 // 'readable' scopes the private bucket to posts the current
99 // user can actually read (others' private posts require the
100 // type's read_private_posts capability), keeping the filter-bar
101 // counts consistent with the rows /nodes returns.
102 $counts = $slug ? wp_count_posts( $slug, 'readable' ) : null;
103 $count = 0;
104 if ( $counts && isset( $counts->publish ) ) {
105 $count = (int) $counts->publish;
106 if ( isset( $counts->private ) ) {
107 $count += (int) $counts->private;
108 }
109 }
110 $out[] = array(
111 'slug' => $slug,
112 'label' => isset( $entry['label'] ) ? (string) $entry['label'] : $slug,
113 'icon' => isset( $entry['icon'] ) ? (string) $entry['icon'] : 'dashicons-admin-post',
114 'count' => $count,
115 'taxonomies' => $entry['taxonomies'],
116 );
117 }
118 return rest_ensure_response( $out );
119 }
120
121 /**
122 * GET /nodes
123 *
124 * @since 0.8.2
125 *
126 * @param WP_REST_Request $request
127 * @return WP_REST_Response
128 */
129 function desktop_mode_content_graph_rest_nodes( WP_REST_Request $request ) {
130 $raw = (string) $request->get_param( 'types' );
131 $types = '' === $raw
132 ? wp_list_pluck( desktop_mode_content_graph_post_types(), 'slug' )
133 : array_map( 'trim', explode( ',', $raw ) );
134 $payload = desktop_mode_content_graph_build( (array) $types );
135 return rest_ensure_response( desktop_mode_content_graph_filter_payload_for_user( $payload ) );
136 }
137
138 /**
139 * Strip revision-derived data the current user may not see from a
140 * graph payload before it goes out.
141 *
142 * Revision authorship is edit-level data in core (wp/v2 exposes a
143 * post's revisions only behind `edit_post`), so each node's
144 * `contributor_ids` — distinct revision authors — are emptied for
145 * posts the user cannot `edit_post`. Authors-catalog entries that
146 * were referenced only via stripped contributor ids are removed too.
147 * This runs at response time, not build time, because the cached
148 * payload is shared across users of the same privilege tier.
149 *
150 * @since 0.9.2
151 *
152 * @param array $payload Payload from `desktop_mode_content_graph_build()`.
153 * @return array
154 */
155 function desktop_mode_content_graph_filter_payload_for_user( array $payload ) {
156 if ( empty( $payload['nodes'] ) || ! is_array( $payload['nodes'] ) ) {
157 return $payload;
158 }
159
160 // Bulk-warm the post cache for the cap checks — only nodes that
161 // actually carry contributor ids need an edit_post decision.
162 $check_ids = array();
163 foreach ( $payload['nodes'] as $node ) {
164 if ( ! empty( $node['contributor_ids'] ) && ! empty( $node['id'] ) ) {
165 $check_ids[] = (int) $node['id'];
166 }
167 }
168 if ( ! empty( $check_ids ) && function_exists( '_prime_post_caches' ) ) {
169 _prime_post_caches( $check_ids, false, false );
170 }
171
172 $referenced = array();
173 foreach ( $payload['nodes'] as $i => $node ) {
174 $id = isset( $node['id'] ) ? (int) $node['id'] : 0;
175 $contribs = isset( $node['contributor_ids'] ) && is_array( $node['contributor_ids'] )
176 ? $node['contributor_ids']
177 : array();
178 if ( ! empty( $contribs ) && ! current_user_can( 'edit_post', $id ) ) {
179 $contribs = array();
180 $payload['nodes'][ $i ]['contributor_ids'] = array();
181 }
182 $author_id = isset( $node['author_id'] ) ? (int) $node['author_id'] : 0;
183 if ( $author_id > 0 ) {
184 $referenced[ $author_id ] = true;
185 }
186 foreach ( $contribs as $cid ) {
187 if ( (int) $cid > 0 ) {
188 $referenced[ (int) $cid ] = true;
189 }
190 }
191 }
192
193 if ( isset( $payload['groups']['authors'] ) && is_array( $payload['groups']['authors'] ) ) {
194 $payload['groups']['authors'] = array_intersect_key( $payload['groups']['authors'], $referenced );
195 }
196
197 return $payload;
198 }
199
200 /**
201 * GET /post/<id>
202 *
203 * @since 0.8.2
204 *
205 * @param WP_REST_Request $request
206 * @return WP_REST_Response|WP_Error
207 */
208 function desktop_mode_content_graph_rest_post_detail( WP_REST_Request $request ) {
209 $id = (int) $request['id'];
210 $post = $id > 0 ? get_post( $id ) : null;
211 if ( ! $post ) {
212 return new WP_Error(
213 'desktop_mode_content_graph_post_not_found',
214 __( 'Post not found.', 'desktop-mode' ),
215 array( 'status' => 404 )
216 );
217 }
218 if ( ! current_user_can( 'read_post', $id ) ) {
219 return new WP_Error(
220 'desktop_mode_content_graph_forbidden',
221 __( 'Insufficient permissions.', 'desktop-mode' ),
222 array( 'status' => 403 )
223 );
224 }
225
226 // Revision history (and the identities of who edited the post) is
227 // edit-level data in core — wp/v2 only exposes revisions behind
228 // edit_post. Mirror that: readers get comment-author contributors
229 // only, no revision list.
230 $can_edit = current_user_can( 'edit_post', $post->ID );
231 $author = desktop_mode_content_graph_format_user( (int) $post->post_author );
232 $contributors = desktop_mode_content_graph_collect_contributors( $post, $can_edit );
233 $comments = desktop_mode_content_graph_collect_comments( $post );
234 $categories = desktop_mode_content_graph_collect_terms( $post );
235 $attached = desktop_mode_content_graph_collect_attached_media( $post );
236 $revisions = $can_edit ? desktop_mode_content_graph_collect_revisions( $post ) : array();
237
238 return rest_ensure_response(
239 array(
240 'post' => array(
241 'id' => (int) $post->ID,
242 'type' => $post->post_type,
243 'title' => get_the_title( $post ),
244 'status' => $post->post_status,
245 'slug' => $post->post_name,
246 'edit_url' => (string) get_edit_post_link( $post->ID, 'raw' ),
247 'view_url' => (string) get_permalink( $post ),
248 'date' => mysql2date( 'c', $post->post_date_gmt, false ),
249 'modified' => mysql2date( 'c', $post->post_modified_gmt, false ),
250 ),
251 'author' => $author,
252 'contributors' => $contributors,
253 'comments' => $comments,
254 'categories' => $categories,
255 'attached_media' => $attached,
256 'revisions' => $revisions,
257 )
258 );
259 }
260
261 /**
262 * Format a user record for the side panel.
263 *
264 * @since 0.8.2
265 *
266 * @param int $user_id
267 * @return array|null
268 */
269 function desktop_mode_content_graph_format_user( $user_id ) {
270 $user_id = (int) $user_id;
271 if ( $user_id <= 0 ) {
272 return null;
273 }
274 $user = get_userdata( $user_id );
275 if ( ! $user ) {
276 return null;
277 }
278 return array(
279 'id' => $user_id,
280 'name' => (string) $user->display_name,
281 'slug' => (string) $user->user_nicename,
282 'avatar' => (string) get_avatar_url( $user_id, array( 'size' => 64 ) ),
283 'edit_url' => (string) get_edit_user_link( $user_id ),
284 );
285 }
286
287 /**
288 * Collect contributors: distinct revision authors (excluding the
289 * current author) plus distinct comment authors who have a
290 * registered user account.
291 *
292 * @since 0.8.2
293 *
294 * @param WP_Post $post
295 * @param bool $include_revision_authors Whether to include revision
296 * authors. Pass false for users who cannot `edit_post`
297 * the post — revision authorship is edit-level data;
298 * approved comment authors are public either way.
299 * @return array[]
300 */
301 function desktop_mode_content_graph_collect_contributors( WP_Post $post, $include_revision_authors = true ) {
302 $author_id = (int) $post->post_author;
303 $ids = array();
304 if ( $include_revision_authors ) {
305 $revs = wp_get_post_revisions(
306 $post->ID,
307 array(
308 'posts_per_page' => 100,
309 'fields' => 'ids',
310 )
311 );
312 foreach ( (array) $revs as $rev_id ) {
313 $rev = get_post( $rev_id );
314 if ( $rev && (int) $rev->post_author > 0 && (int) $rev->post_author !== $author_id ) {
315 $ids[ (int) $rev->post_author ] = true;
316 }
317 }
318 }
319 $comment_users = get_comments(
320 array(
321 'post_id' => $post->ID,
322 'status' => 'approve',
323 'fields' => 'ids',
324 )
325 );
326 foreach ( (array) $comment_users as $cid ) {
327 $comment = get_comment( $cid );
328 if ( $comment && (int) $comment->user_id > 0 && (int) $comment->user_id !== $author_id ) {
329 $ids[ (int) $comment->user_id ] = true;
330 }
331 }
332 $out = array();
333 foreach ( array_keys( $ids ) as $uid ) {
334 $entry = desktop_mode_content_graph_format_user( $uid );
335 if ( $entry ) {
336 $out[] = $entry;
337 }
338 }
339 return $out;
340 }
341
342 /**
343 * Collect approved comments (most recent first, capped at 50).
344 *
345 * @since 0.8.2
346 *
347 * @param WP_Post $post
348 * @return array[]
349 */
350 function desktop_mode_content_graph_collect_comments( WP_Post $post ) {
351 $comments = get_comments(
352 array(
353 'post_id' => $post->ID,
354 'status' => 'approve',
355 'number' => 50,
356 'orderby' => 'comment_date_gmt',
357 'order' => 'DESC',
358 )
359 );
360 $out = array();
361 foreach ( $comments as $comment ) {
362 $out[] = array(
363 'id' => (int) $comment->comment_ID,
364 'author' => (string) $comment->comment_author,
365 'user_id' => (int) $comment->user_id,
366 'date' => mysql2date( 'c', $comment->comment_date_gmt, false ),
367 'excerpt' => wp_html_excerpt( wp_strip_all_tags( (string) $comment->comment_content ), 140, '...' ),
368 'edit_url' => (string) admin_url( 'comment.php?action=editcomment&c=' . (int) $comment->comment_ID ),
369 );
370 }
371 return $out;
372 }
373
374 /**
375 * Collect every taxonomy term attached to the post (categories, tags,
376 * and any custom taxonomy registered for the post type).
377 *
378 * @since 0.8.2
379 *
380 * @param WP_Post $post
381 * @return array[]
382 */
383 function desktop_mode_content_graph_collect_terms( WP_Post $post ) {
384 $taxes = get_object_taxonomies( $post->post_type, 'objects' );
385 $out = array();
386 foreach ( $taxes as $tax ) {
387 if ( ! $tax->public && ! $tax->show_ui ) {
388 continue;
389 }
390 $terms = get_the_terms( $post, $tax->name );
391 if ( empty( $terms ) || is_wp_error( $terms ) ) {
392 continue;
393 }
394 foreach ( $terms as $term ) {
395 $out[] = array(
396 'id' => (int) $term->term_id,
397 'name' => (string) $term->name,
398 'slug' => (string) $term->slug,
399 'taxonomy' => (string) $term->taxonomy,
400 'tax_label' => (string) $tax->labels->singular_name,
401 'count' => (int) $term->count,
402 'edit_url' => (string) get_edit_term_link( $term->term_id, $term->taxonomy ),
403 );
404 }
405 }
406 return $out;
407 }
408
409 /**
410 * Collect attached media (anything with this post as its `post_parent`)
411 * plus any media referenced from a `wp:image` block. Returns up to 50.
412 *
413 * @since 0.8.2
414 *
415 * @param WP_Post $post
416 * @return array[]
417 */
418 function desktop_mode_content_graph_collect_attached_media( WP_Post $post ) {
419 $attachments = get_attached_media( '', $post );
420 $out = array();
421 foreach ( $attachments as $att ) {
422 $out[] = array(
423 'id' => (int) $att->ID,
424 'title' => (string) get_the_title( $att ),
425 'mime' => (string) $att->post_mime_type,
426 'thumb' => (string) wp_get_attachment_image_url( $att->ID, 'thumbnail' ),
427 'edit_url' => (string) get_edit_post_link( $att->ID, 'raw' ),
428 );
429 if ( count( $out ) >= 50 ) {
430 break;
431 }
432 }
433 return $out;
434 }
435
436 /**
437 * Collect post revisions (most recent first, capped at 30).
438 *
439 * @since 0.8.2
440 *
441 * @param WP_Post $post
442 * @return array[]
443 */
444 function desktop_mode_content_graph_collect_revisions( WP_Post $post ) {
445 $revs = wp_get_post_revisions(
446 $post->ID,
447 array(
448 'posts_per_page' => 30,
449 )
450 );
451 $out = array();
452 foreach ( $revs as $rev ) {
453 $out[] = array(
454 'id' => (int) $rev->ID,
455 'date' => mysql2date( 'c', $rev->post_date_gmt, false ),
456 'author' => desktop_mode_content_graph_format_user( (int) $rev->post_author ),
457 'edit_url' => (string) admin_url( 'revision.php?revision=' . (int) $rev->ID ),
458 );
459 }
460 return $out;
461 }
462