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

461 lines 13.4 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`).
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 );
116 }
117 return rest_ensure_response( $out );
118 }
119
120 /**
121 * GET /nodes
122 *
123 * @since 0.8.2
124 *
125 * @param WP_REST_Request $request
126 * @return WP_REST_Response
127 */
128 function desktop_mode_content_graph_rest_nodes( WP_REST_Request $request ) {
129 $raw = (string) $request->get_param( 'types' );
130 $types = '' === $raw
131 ? wp_list_pluck( desktop_mode_content_graph_post_types(), 'slug' )
132 : array_map( 'trim', explode( ',', $raw ) );
133 $payload = desktop_mode_content_graph_build( (array) $types );
134 return rest_ensure_response( desktop_mode_content_graph_filter_payload_for_user( $payload ) );
135 }
136
137 /**
138 * Strip revision-derived data the current user may not see from a
139 * graph payload before it goes out.
140 *
141 * Revision authorship is edit-level data in core (wp/v2 exposes a
142 * post's revisions only behind `edit_post`), so each node's
143 * `contributor_ids` — distinct revision authors — are emptied for
144 * posts the user cannot `edit_post`. Authors-catalog entries that
145 * were referenced only via stripped contributor ids are removed too.
146 * This runs at response time, not build time, because the cached
147 * payload is shared across users of the same privilege tier.
148 *
149 * @since 0.9.2
150 *
151 * @param array $payload Payload from `desktop_mode_content_graph_build()`.
152 * @return array
153 */
154 function desktop_mode_content_graph_filter_payload_for_user( array $payload ) {
155 if ( empty( $payload['nodes'] ) || ! is_array( $payload['nodes'] ) ) {
156 return $payload;
157 }
158
159 // Bulk-warm the post cache for the cap checks — only nodes that
160 // actually carry contributor ids need an edit_post decision.
161 $check_ids = array();
162 foreach ( $payload['nodes'] as $node ) {
163 if ( ! empty( $node['contributor_ids'] ) && ! empty( $node['id'] ) ) {
164 $check_ids[] = (int) $node['id'];
165 }
166 }
167 if ( ! empty( $check_ids ) && function_exists( '_prime_post_caches' ) ) {
168 _prime_post_caches( $check_ids, false, false );
169 }
170
171 $referenced = array();
172 foreach ( $payload['nodes'] as $i => $node ) {
173 $id = isset( $node['id'] ) ? (int) $node['id'] : 0;
174 $contribs = isset( $node['contributor_ids'] ) && is_array( $node['contributor_ids'] )
175 ? $node['contributor_ids']
176 : array();
177 if ( ! empty( $contribs ) && ! current_user_can( 'edit_post', $id ) ) {
178 $contribs = array();
179 $payload['nodes'][ $i ]['contributor_ids'] = array();
180 }
181 $author_id = isset( $node['author_id'] ) ? (int) $node['author_id'] : 0;
182 if ( $author_id > 0 ) {
183 $referenced[ $author_id ] = true;
184 }
185 foreach ( $contribs as $cid ) {
186 if ( (int) $cid > 0 ) {
187 $referenced[ (int) $cid ] = true;
188 }
189 }
190 }
191
192 if ( isset( $payload['groups']['authors'] ) && is_array( $payload['groups']['authors'] ) ) {
193 $payload['groups']['authors'] = array_intersect_key( $payload['groups']['authors'], $referenced );
194 }
195
196 return $payload;
197 }
198
199 /**
200 * GET /post/<id>
201 *
202 * @since 0.8.2
203 *
204 * @param WP_REST_Request $request
205 * @return WP_REST_Response|WP_Error
206 */
207 function desktop_mode_content_graph_rest_post_detail( WP_REST_Request $request ) {
208 $id = (int) $request['id'];
209 $post = $id > 0 ? get_post( $id ) : null;
210 if ( ! $post ) {
211 return new WP_Error(
212 'desktop_mode_content_graph_post_not_found',
213 __( 'Post not found.', 'desktop-mode' ),
214 array( 'status' => 404 )
215 );
216 }
217 if ( ! current_user_can( 'read_post', $id ) ) {
218 return new WP_Error(
219 'desktop_mode_content_graph_forbidden',
220 __( 'Insufficient permissions.', 'desktop-mode' ),
221 array( 'status' => 403 )
222 );
223 }
224
225 // Revision history (and the identities of who edited the post) is
226 // edit-level data in core — wp/v2 only exposes revisions behind
227 // edit_post. Mirror that: readers get comment-author contributors
228 // only, no revision list.
229 $can_edit = current_user_can( 'edit_post', $post->ID );
230 $author = desktop_mode_content_graph_format_user( (int) $post->post_author );
231 $contributors = desktop_mode_content_graph_collect_contributors( $post, $can_edit );
232 $comments = desktop_mode_content_graph_collect_comments( $post );
233 $categories = desktop_mode_content_graph_collect_terms( $post );
234 $attached = desktop_mode_content_graph_collect_attached_media( $post );
235 $revisions = $can_edit ? desktop_mode_content_graph_collect_revisions( $post ) : array();
236
237 return rest_ensure_response(
238 array(
239 'post' => array(
240 'id' => (int) $post->ID,
241 'type' => $post->post_type,
242 'title' => get_the_title( $post ),
243 'status' => $post->post_status,
244 'slug' => $post->post_name,
245 'edit_url' => (string) get_edit_post_link( $post->ID, 'raw' ),
246 'view_url' => (string) get_permalink( $post ),
247 'date' => mysql2date( 'c', $post->post_date_gmt, false ),
248 'modified' => mysql2date( 'c', $post->post_modified_gmt, false ),
249 ),
250 'author' => $author,
251 'contributors' => $contributors,
252 'comments' => $comments,
253 'categories' => $categories,
254 'attached_media' => $attached,
255 'revisions' => $revisions,
256 )
257 );
258 }
259
260 /**
261 * Format a user record for the side panel.
262 *
263 * @since 0.8.2
264 *
265 * @param int $user_id
266 * @return array|null
267 */
268 function desktop_mode_content_graph_format_user( $user_id ) {
269 $user_id = (int) $user_id;
270 if ( $user_id <= 0 ) {
271 return null;
272 }
273 $user = get_userdata( $user_id );
274 if ( ! $user ) {
275 return null;
276 }
277 return array(
278 'id' => $user_id,
279 'name' => (string) $user->display_name,
280 'slug' => (string) $user->user_nicename,
281 'avatar' => (string) get_avatar_url( $user_id, array( 'size' => 64 ) ),
282 'edit_url' => (string) get_edit_user_link( $user_id ),
283 );
284 }
285
286 /**
287 * Collect contributors: distinct revision authors (excluding the
288 * current author) plus distinct comment authors who have a
289 * registered user account.
290 *
291 * @since 0.8.2
292 *
293 * @param WP_Post $post
294 * @param bool $include_revision_authors Whether to include revision
295 * authors. Pass false for users who cannot `edit_post`
296 * the post — revision authorship is edit-level data;
297 * approved comment authors are public either way.
298 * @return array[]
299 */
300 function desktop_mode_content_graph_collect_contributors( WP_Post $post, $include_revision_authors = true ) {
301 $author_id = (int) $post->post_author;
302 $ids = array();
303 if ( $include_revision_authors ) {
304 $revs = wp_get_post_revisions(
305 $post->ID,
306 array(
307 'posts_per_page' => 100,
308 'fields' => 'ids',
309 )
310 );
311 foreach ( (array) $revs as $rev_id ) {
312 $rev = get_post( $rev_id );
313 if ( $rev && (int) $rev->post_author > 0 && (int) $rev->post_author !== $author_id ) {
314 $ids[ (int) $rev->post_author ] = true;
315 }
316 }
317 }
318 $comment_users = get_comments(
319 array(
320 'post_id' => $post->ID,
321 'status' => 'approve',
322 'fields' => 'ids',
323 )
324 );
325 foreach ( (array) $comment_users as $cid ) {
326 $comment = get_comment( $cid );
327 if ( $comment && (int) $comment->user_id > 0 && (int) $comment->user_id !== $author_id ) {
328 $ids[ (int) $comment->user_id ] = true;
329 }
330 }
331 $out = array();
332 foreach ( array_keys( $ids ) as $uid ) {
333 $entry = desktop_mode_content_graph_format_user( $uid );
334 if ( $entry ) {
335 $out[] = $entry;
336 }
337 }
338 return $out;
339 }
340
341 /**
342 * Collect approved comments (most recent first, capped at 50).
343 *
344 * @since 0.8.2
345 *
346 * @param WP_Post $post
347 * @return array[]
348 */
349 function desktop_mode_content_graph_collect_comments( WP_Post $post ) {
350 $comments = get_comments(
351 array(
352 'post_id' => $post->ID,
353 'status' => 'approve',
354 'number' => 50,
355 'orderby' => 'comment_date_gmt',
356 'order' => 'DESC',
357 )
358 );
359 $out = array();
360 foreach ( $comments as $comment ) {
361 $out[] = array(
362 'id' => (int) $comment->comment_ID,
363 'author' => (string) $comment->comment_author,
364 'user_id' => (int) $comment->user_id,
365 'date' => mysql2date( 'c', $comment->comment_date_gmt, false ),
366 'excerpt' => wp_html_excerpt( wp_strip_all_tags( (string) $comment->comment_content ), 140, '...' ),
367 'edit_url' => (string) admin_url( 'comment.php?action=editcomment&c=' . (int) $comment->comment_ID ),
368 );
369 }
370 return $out;
371 }
372
373 /**
374 * Collect every taxonomy term attached to the post (categories, tags,
375 * and any custom taxonomy registered for the post type).
376 *
377 * @since 0.8.2
378 *
379 * @param WP_Post $post
380 * @return array[]
381 */
382 function desktop_mode_content_graph_collect_terms( WP_Post $post ) {
383 $taxes = get_object_taxonomies( $post->post_type, 'objects' );
384 $out = array();
385 foreach ( $taxes as $tax ) {
386 if ( ! $tax->public && ! $tax->show_ui ) {
387 continue;
388 }
389 $terms = get_the_terms( $post, $tax->name );
390 if ( empty( $terms ) || is_wp_error( $terms ) ) {
391 continue;
392 }
393 foreach ( $terms as $term ) {
394 $out[] = array(
395 'id' => (int) $term->term_id,
396 'name' => (string) $term->name,
397 'slug' => (string) $term->slug,
398 'taxonomy' => (string) $term->taxonomy,
399 'tax_label' => (string) $tax->labels->singular_name,
400 'count' => (int) $term->count,
401 'edit_url' => (string) get_edit_term_link( $term->term_id, $term->taxonomy ),
402 );
403 }
404 }
405 return $out;
406 }
407
408 /**
409 * Collect attached media (anything with this post as its `post_parent`)
410 * plus any media referenced from a `wp:image` block. Returns up to 50.
411 *
412 * @since 0.8.2
413 *
414 * @param WP_Post $post
415 * @return array[]
416 */
417 function desktop_mode_content_graph_collect_attached_media( WP_Post $post ) {
418 $attachments = get_attached_media( '', $post );
419 $out = array();
420 foreach ( $attachments as $att ) {
421 $out[] = array(
422 'id' => (int) $att->ID,
423 'title' => (string) get_the_title( $att ),
424 'mime' => (string) $att->post_mime_type,
425 'thumb' => (string) wp_get_attachment_image_url( $att->ID, 'thumbnail' ),
426 'edit_url' => (string) get_edit_post_link( $att->ID, 'raw' ),
427 );
428 if ( count( $out ) >= 50 ) {
429 break;
430 }
431 }
432 return $out;
433 }
434
435 /**
436 * Collect post revisions (most recent first, capped at 30).
437 *
438 * @since 0.8.2
439 *
440 * @param WP_Post $post
441 * @return array[]
442 */
443 function desktop_mode_content_graph_collect_revisions( WP_Post $post ) {
444 $revs = wp_get_post_revisions(
445 $post->ID,
446 array(
447 'posts_per_page' => 30,
448 )
449 );
450 $out = array();
451 foreach ( $revs as $rev ) {
452 $out[] = array(
453 'id' => (int) $rev->ID,
454 'date' => mysql2date( 'c', $rev->post_date_gmt, false ),
455 'author' => desktop_mode_content_graph_format_user( (int) $rev->post_author ),
456 'edit_url' => (string) admin_url( 'revision.php?revision=' . (int) $rev->ID ),
457 );
458 }
459 return $out;
460 }
461