PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
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 0.8.6 All 33 releases
desktop-mode / includes / content-graph / rest.php

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

449 lines 14.0 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, groups, stats }` tuple. Cached
13 * server-side, see graph-builder.php. `types` omitted means every
14 * registered type; `types=` (present but empty) means none — the
15 * shell sends the latter when every toolbar chip is off.
16 *
17 * GET /post/<id>
18 * Returns the side-panel detail bundle for one post:
19 * { post: {...}, author, contributors, comments, categories,
20 * attached_media, revisions }.
21 *
22 * @package OpenStation
23 */
24
25 defined( 'ABSPATH' ) || exit;
26
27 /**
28 * Capability check shared across every endpoint.
29 *
30 * @return bool
31 */
32 function openstation_content_graph_rest_permission() {
33 return openstation_content_graph_user_can_use();
34 }
35
36 /**
37 * Register the routes.
38 */
39 function openstation_content_graph_register_routes() {
40 register_rest_route(
41 'desktop-mode/v1',
42 '/content-graph/post-types',
43 array(
44 'methods' => WP_REST_Server::READABLE,
45 'callback' => 'openstation_content_graph_rest_post_types',
46 'permission_callback' => 'openstation_content_graph_rest_permission',
47 )
48 );
49 register_rest_route(
50 'desktop-mode/v1',
51 '/content-graph/nodes',
52 array(
53 'methods' => WP_REST_Server::READABLE,
54 'callback' => 'openstation_content_graph_rest_nodes',
55 'permission_callback' => 'openstation_content_graph_rest_permission',
56 'args' => array(
57 // Deliberately no `default`: the dispatcher copies a
58 // registered default into the request before the
59 // callback runs, and `WP_REST_Request::has_param()`
60 // sees it, so a default here would make an omitted
61 // parameter indistinguishable from an explicitly empty
62 // one — and the callback tells those two apart.
63 'types' => array(
64 'description' => 'Comma-separated list of post type slugs to include. Omit for every registered type; pass an empty value for none.',
65 'type' => 'string',
66 ),
67 ),
68 )
69 );
70 register_rest_route(
71 'desktop-mode/v1',
72 '/content-graph/post/(?P<id>\d+)',
73 array(
74 'methods' => WP_REST_Server::READABLE,
75 'callback' => 'openstation_content_graph_rest_post_detail',
76 'permission_callback' => 'openstation_content_graph_rest_permission',
77 'args' => array(
78 'id' => array(
79 'type' => 'integer',
80 'required' => true,
81 ),
82 ),
83 )
84 );
85 }
86 add_action( 'rest_api_init', 'openstation_content_graph_register_routes' );
87
88 /**
89 * GET /post-types
90 *
91 * @return WP_REST_Response
92 */
93 function openstation_content_graph_rest_post_types() {
94 $types = openstation_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 * An omitted `types` parameter selects every registered type; a
125 * present-but-empty one selects none. The route registers no default
126 * for the parameter so the two stay distinguishable through a real
127 * dispatch (see the route registration above).
128 *
129 * @param WP_REST_Request $request
130 * @return WP_REST_Response
131 */
132 function openstation_content_graph_rest_nodes( WP_REST_Request $request ) {
133 $raw = $request->get_param( 'types' );
134 $types = $request->has_param( 'types' )
135 ? array_map( 'trim', explode( ',', (string) $raw ) )
136 : wp_list_pluck( openstation_content_graph_post_types(), 'slug' );
137 $payload = openstation_content_graph_build( (array) $types );
138 return rest_ensure_response( openstation_content_graph_filter_payload_for_user( $payload ) );
139 }
140
141 /**
142 * Strip revision-derived data the current user may not see from a
143 * graph payload before it goes out.
144 *
145 * Revision authorship is edit-level data in core (wp/v2 exposes a
146 * post's revisions only behind `edit_post`), so each node's
147 * `contributor_ids` — distinct revision authors — are emptied for
148 * posts the user cannot `edit_post`. Authors-catalog entries that
149 * were referenced only via stripped contributor ids are removed too.
150 * This runs at response time, not build time, because the cached
151 * payload is shared across users of the same privilege tier.
152 *
153 * @param array $payload Payload from `openstation_content_graph_build()`.
154 * @return array
155 */
156 function openstation_content_graph_filter_payload_for_user( array $payload ) {
157 if ( empty( $payload['nodes'] ) || ! is_array( $payload['nodes'] ) ) {
158 return $payload;
159 }
160
161 // Bulk-warm the post cache for the cap checks — only nodes that
162 // actually carry contributor ids need an edit_post decision.
163 $check_ids = array();
164 foreach ( $payload['nodes'] as $node ) {
165 if ( ! empty( $node['contributor_ids'] ) && ! empty( $node['id'] ) ) {
166 $check_ids[] = (int) $node['id'];
167 }
168 }
169 if ( ! empty( $check_ids ) && function_exists( '_prime_post_caches' ) ) {
170 _prime_post_caches( $check_ids, false, false );
171 }
172
173 $referenced = array();
174 foreach ( $payload['nodes'] as $i => $node ) {
175 $id = isset( $node['id'] ) ? (int) $node['id'] : 0;
176 $contribs = isset( $node['contributor_ids'] ) && is_array( $node['contributor_ids'] )
177 ? $node['contributor_ids']
178 : array();
179 if ( ! empty( $contribs ) && ! current_user_can( 'edit_post', $id ) ) {
180 $contribs = array();
181 $payload['nodes'][ $i ]['contributor_ids'] = array();
182 }
183 $author_id = isset( $node['author_id'] ) ? (int) $node['author_id'] : 0;
184 if ( $author_id > 0 ) {
185 $referenced[ $author_id ] = true;
186 }
187 foreach ( $contribs as $cid ) {
188 if ( (int) $cid > 0 ) {
189 $referenced[ (int) $cid ] = true;
190 }
191 }
192 }
193
194 if ( isset( $payload['groups']['authors'] ) && is_array( $payload['groups']['authors'] ) ) {
195 $payload['groups']['authors'] = array_intersect_key( $payload['groups']['authors'], $referenced );
196 }
197
198 return $payload;
199 }
200
201 /**
202 * GET /post/<id>
203 *
204 * @param WP_REST_Request $request
205 * @return WP_REST_Response|WP_Error
206 */
207 function openstation_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 'openstation_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 'openstation_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 = openstation_content_graph_format_user( (int) $post->post_author );
231 $contributors = openstation_content_graph_collect_contributors( $post, $can_edit );
232 $comments = openstation_content_graph_collect_comments( $post );
233 $categories = openstation_content_graph_collect_terms( $post );
234 $attached = openstation_content_graph_collect_attached_media( $post );
235 $revisions = $can_edit ? openstation_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 * @param int $user_id
264 * @return array|null
265 */
266 function openstation_content_graph_format_user( $user_id ) {
267 $user_id = (int) $user_id;
268 if ( $user_id <= 0 ) {
269 return null;
270 }
271 $user = get_userdata( $user_id );
272 if ( ! $user ) {
273 return null;
274 }
275 return array(
276 'id' => $user_id,
277 'name' => (string) $user->display_name,
278 'slug' => (string) $user->user_nicename,
279 'avatar' => (string) get_avatar_url( $user_id, array( 'size' => 64 ) ),
280 'edit_url' => (string) get_edit_user_link( $user_id ),
281 );
282 }
283
284 /**
285 * Collect contributors: distinct revision authors (excluding the
286 * current author) plus distinct comment authors who have a
287 * registered user account.
288 *
289 * @param WP_Post $post
290 * @param bool $include_revision_authors Whether to include revision
291 * authors. Pass false for users who cannot `edit_post`
292 * the post — revision authorship is edit-level data;
293 * approved comment authors are public either way.
294 * @return array[]
295 */
296 function openstation_content_graph_collect_contributors( WP_Post $post, $include_revision_authors = true ) {
297 $author_id = (int) $post->post_author;
298 $ids = array();
299 if ( $include_revision_authors ) {
300 $revs = wp_get_post_revisions(
301 $post->ID,
302 array(
303 'posts_per_page' => 100,
304 'fields' => 'ids',
305 )
306 );
307 foreach ( (array) $revs as $rev_id ) {
308 $rev = get_post( $rev_id );
309 if ( $rev && (int) $rev->post_author > 0 && (int) $rev->post_author !== $author_id ) {
310 $ids[ (int) $rev->post_author ] = true;
311 }
312 }
313 }
314 $comment_users = get_comments(
315 array(
316 'post_id' => $post->ID,
317 'status' => 'approve',
318 'fields' => 'ids',
319 )
320 );
321 foreach ( (array) $comment_users as $cid ) {
322 $comment = get_comment( $cid );
323 if ( $comment && (int) $comment->user_id > 0 && (int) $comment->user_id !== $author_id ) {
324 $ids[ (int) $comment->user_id ] = true;
325 }
326 }
327 $out = array();
328 foreach ( array_keys( $ids ) as $uid ) {
329 $entry = openstation_content_graph_format_user( $uid );
330 if ( $entry ) {
331 $out[] = $entry;
332 }
333 }
334 return $out;
335 }
336
337 /**
338 * Collect approved comments (most recent first, capped at 50).
339 *
340 * @param WP_Post $post
341 * @return array[]
342 */
343 function openstation_content_graph_collect_comments( WP_Post $post ) {
344 $comments = get_comments(
345 array(
346 'post_id' => $post->ID,
347 'status' => 'approve',
348 'number' => 50,
349 'orderby' => 'comment_date_gmt',
350 'order' => 'DESC',
351 )
352 );
353 $out = array();
354 foreach ( $comments as $comment ) {
355 $out[] = array(
356 'id' => (int) $comment->comment_ID,
357 'author' => (string) $comment->comment_author,
358 'user_id' => (int) $comment->user_id,
359 'date' => mysql2date( 'c', $comment->comment_date_gmt, false ),
360 'excerpt' => wp_html_excerpt( wp_strip_all_tags( (string) $comment->comment_content ), 140, '...' ),
361 'edit_url' => (string) admin_url( 'comment.php?action=editcomment&c=' . (int) $comment->comment_ID ),
362 );
363 }
364 return $out;
365 }
366
367 /**
368 * Collect every taxonomy term attached to the post (categories, tags,
369 * and any custom taxonomy registered for the post type).
370 *
371 * @param WP_Post $post
372 * @return array[]
373 */
374 function openstation_content_graph_collect_terms( WP_Post $post ) {
375 $taxes = get_object_taxonomies( $post->post_type, 'objects' );
376 $out = array();
377 foreach ( $taxes as $tax ) {
378 if ( ! $tax->public && ! $tax->show_ui ) {
379 continue;
380 }
381 $terms = get_the_terms( $post, $tax->name );
382 if ( empty( $terms ) || is_wp_error( $terms ) ) {
383 continue;
384 }
385 foreach ( $terms as $term ) {
386 $out[] = array(
387 'id' => (int) $term->term_id,
388 'name' => (string) $term->name,
389 'slug' => (string) $term->slug,
390 'taxonomy' => (string) $term->taxonomy,
391 'tax_label' => (string) $tax->labels->singular_name,
392 'count' => (int) $term->count,
393 'edit_url' => (string) get_edit_term_link( $term->term_id, $term->taxonomy ),
394 );
395 }
396 }
397 return $out;
398 }
399
400 /**
401 * Collect attached media (anything with this post as its `post_parent`)
402 * plus any media referenced from a `wp:image` block. Returns up to 50.
403 *
404 * @param WP_Post $post
405 * @return array[]
406 */
407 function openstation_content_graph_collect_attached_media( WP_Post $post ) {
408 $attachments = get_attached_media( '', $post );
409 $out = array();
410 foreach ( $attachments as $att ) {
411 $out[] = array(
412 'id' => (int) $att->ID,
413 'title' => (string) get_the_title( $att ),
414 'mime' => (string) $att->post_mime_type,
415 'thumb' => (string) wp_get_attachment_image_url( $att->ID, 'thumbnail' ),
416 'edit_url' => (string) get_edit_post_link( $att->ID, 'raw' ),
417 );
418 if ( count( $out ) >= 50 ) {
419 break;
420 }
421 }
422 return $out;
423 }
424
425 /**
426 * Collect post revisions (most recent first, capped at 30).
427 *
428 * @param WP_Post $post
429 * @return array[]
430 */
431 function openstation_content_graph_collect_revisions( WP_Post $post ) {
432 $revs = wp_get_post_revisions(
433 $post->ID,
434 array(
435 'posts_per_page' => 30,
436 )
437 );
438 $out = array();
439 foreach ( $revs as $rev ) {
440 $out[] = array(
441 'id' => (int) $rev->ID,
442 'date' => mysql2date( 'c', $rev->post_date_gmt, false ),
443 'author' => openstation_content_graph_format_user( (int) $rev->post_author ),
444 'edit_url' => (string) admin_url( 'revision.php?revision=' . (int) $rev->ID ),
445 );
446 }
447 return $out;
448 }
449