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

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