PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.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 / apps / my-wordpress / parts / dossiers.php

dossiers.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.6, at apps/my-wordpress/parts/dossiers.php

454 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * My WordPress — navigate-into: the detail folder and its sub-lists.
4 *
5 * Part of the `my-wordpress` app: required by `my-wordpress.os.php`,
6 * same namespace, plain `.php` on purpose — only `*.os.php` files are
7 * app entries to the framework loader. This part owns what a post
8 * OPENS INTO: the relation folders with live counts, each relation's
9 * rows, the right-pane dossier behind a selected row (rendered
10 * through WP Explorer's own stats callbacks), the Edit… modal's
11 * choice lists, and the shared preview-action descriptors.
12 *
13 * @package OpenStation
14 */
15
16 namespace OpenStation\Apps\MyWordPress;
17
18 use OpenStation\App\Os;
19
20 // Direct access, unless a standalone host is booting on bare PHP.
21 if ( ! defined( 'ABSPATH' ) ) {
22 defined( 'OPENSTATION_STANDALONE' ) || exit;
23 }
24
25 /**
26 * The detail FOLDER a post navigates into: the rendered article plus
27 * one folder tile per related surface — Author, Contributors,
28 * Comments, Categories, Tags, Attached media, Revisions — with live
29 * counts. WP Explorer's detail view, as data.
30 *
31 * @param Os $os Host handle.
32 * @param array<string,mixed> $section Section descriptor.
33 * @param int $id Post id.
34 * @return array<string,mixed>|null Null when the post vanished.
35 */
36 function folder( Os $os, array $section, $id ) {
37 $post = get_post( $id );
38 if ( ! $post || $post->post_type !== $section['post_type'] ) {
39 return null;
40 }
41
42 $contributors = function_exists( 'openstation_my_wordpress_post_contributors_payload' )
43 ? openstation_my_wordpress_post_contributors_payload( $id )
44 : array();
45 $categories = get_the_terms( $post, 'category' );
46 $categories = is_array( $categories ) ? $categories : array();
47 $tags = get_the_terms( $post, 'post_tag' );
48 $tags = is_array( $tags ) ? $tags : array();
49 $media_count = count(
50 get_children(
51 array(
52 'post_parent' => $id,
53 'post_type' => 'attachment',
54 'fields' => 'ids',
55 )
56 )
57 ) + ( has_post_thumbnail( $post ) ? 1 : 0 );
58 $comments = (int) get_comments_number( $post );
59 $revisions = wp_revisions_enabled( $post ) ? count( wp_get_post_revisions( $id, array( 'fields' => 'ids' ) ) ) : 0;
60
61 $folders = array();
62 $folders[] = array(
63 'relation' => 'author',
64 'label' => __( 'Author', 'desktop-mode' ),
65 'icon' => 'dashicons-admin-users',
66 'count' => 1,
67 );
68 if ( array() !== $contributors ) {
69 $folders[] = array(
70 'relation' => 'contributors',
71 'label' => __( 'Contributors', 'desktop-mode' ),
72 'icon' => 'dashicons-groups',
73 'count' => count( $contributors ),
74 );
75 }
76 $folders[] = array(
77 'relation' => 'comments',
78 'label' => __( 'Comments', 'desktop-mode' ),
79 'icon' => 'dashicons-admin-comments',
80 'count' => $comments,
81 'disabled' => 0 === $comments && 'closed' === $post->comment_status,
82 );
83 if ( array() !== $categories ) {
84 $folders[] = array(
85 'relation' => 'categories',
86 'label' => __( 'Categories', 'desktop-mode' ),
87 'icon' => 'dashicons-category',
88 'count' => count( $categories ),
89 );
90 }
91 if ( array() !== $tags ) {
92 $folders[] = array(
93 'relation' => 'tags',
94 'label' => __( 'Tags', 'desktop-mode' ),
95 'icon' => 'dashicons-tag',
96 'count' => count( $tags ),
97 );
98 }
99 $folders[] = array(
100 'relation' => 'media',
101 'label' => __( 'Attached media', 'desktop-mode' ),
102 'icon' => 'dashicons-format-image',
103 'count' => $media_count,
104 );
105 $folders[] = array(
106 'relation' => 'revisions',
107 'label' => __( 'Revisions', 'desktop-mode' ),
108 'icon' => 'dashicons-backup',
109 'count' => $revisions,
110 );
111
112 return array(
113 'id' => $id,
114 'title' => '' !== $post->post_title ? (string) $post->post_title : __( '(no title)', 'desktop-mode' ),
115 'status' => (string) $post->post_status,
116 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying Core's own content pipeline, not declaring a hook.
117 'content' => (string) apply_filters( 'the_content', (string) $post->post_content ),
118 'folders' => $folders,
119 );
120 }
121
122 /**
123 * The rows inside one relation sub-folder. Uniform shape: `id`,
124 * `title`, `subtitle`, `icon` | `thumb`, `editUrl`.
125 *
126 * @param Os $os Host handle.
127 * @param array<string,mixed> $section Section descriptor.
128 * @param int $id Post id.
129 * @param string $relation Relation slug.
130 * @return array{label:string,rows:array[]}|null
131 */
132 function sub( Os $os, array $section, $id, $relation ) {
133 $post = get_post( $id );
134 if ( ! $post || $post->post_type !== $section['post_type'] ) {
135 return null;
136 }
137 $rows = array();
138 $label = '';
139
140 $user_row = static function ( $user_id, $name = '', $avatar = '' ) {
141 $user = get_userdata( (int) $user_id );
142 return array(
143 'id' => (int) $user_id,
144 'title' => '' !== $name ? $name : ( $user ? (string) $user->display_name : sprintf( '#%d', $user_id ) ),
145 'subtitle' => $user ? (string) $user->user_email : '',
146 'thumb' => '' !== $avatar ? $avatar : (string) get_avatar_url( (int) $user_id, array( 'size' => 96 ) ),
147 'editUrl' => current_user_can( 'edit_user', (int) $user_id )
148 ? admin_url( 'user-edit.php?user_id=' . (int) $user_id )
149 : '',
150 );
151 };
152
153 switch ( $relation ) {
154 case 'author':
155 $label = __( 'Author', 'desktop-mode' );
156 $rows[] = $user_row( (int) $post->post_author );
157 break;
158 case 'contributors':
159 $label = __( 'Contributors', 'desktop-mode' );
160 if ( function_exists( 'openstation_my_wordpress_post_contributors_payload' ) ) {
161 foreach ( openstation_my_wordpress_post_contributors_payload( $id ) as $person ) {
162 $rows[] = $user_row( $person['userId'], $person['userName'], $person['userAvatarUrl'] );
163 }
164 }
165 break;
166 case 'comments':
167 $label = __( 'Comments', 'desktop-mode' );
168 foreach ( get_comments(
169 array(
170 'post_id' => $id,
171 'number' => 100,
172 )
173 ) as $comment ) {
174 $rows[] = array(
175 'id' => (int) $comment->comment_ID,
176 'title' => (string) $comment->comment_author,
177 'subtitle' => wp_trim_words( wp_strip_all_tags( (string) $comment->comment_content ), 12 ),
178 'icon' => 'dashicons-admin-comments',
179 'editUrl' => current_user_can( 'edit_comment', (int) $comment->comment_ID )
180 ? admin_url( 'comment.php?action=editcomment&c=' . (int) $comment->comment_ID )
181 : '',
182 );
183 }
184 break;
185 case 'categories':
186 case 'tags':
187 $taxonomy = 'tags' === $relation ? 'post_tag' : 'category';
188 $label = 'tags' === $relation ? __( 'Tags', 'desktop-mode' ) : __( 'Categories', 'desktop-mode' );
189 $terms = get_the_terms( $post, $taxonomy );
190 foreach ( is_array( $terms ) ? $terms : array() as $term ) {
191 $rows[] = array(
192 'id' => (int) $term->term_id,
193 'title' => (string) $term->name,
194 'subtitle' => sprintf(
195 /* translators: %s: entry count. */
196 _n( '%s entry', '%s entries', (int) $term->count, 'desktop-mode' ),
197 number_format_i18n( (int) $term->count )
198 ),
199 'icon' => 'tags' === $relation ? 'dashicons-tag' : 'dashicons-category',
200 'editUrl' => current_user_can( 'manage_categories' )
201 ? admin_url( 'term.php?taxonomy=' . $taxonomy . '&tag_ID=' . (int) $term->term_id )
202 : '',
203 );
204 }
205 break;
206 case 'media':
207 $label = __( 'Attached media', 'desktop-mode' );
208 $ids = get_children(
209 array(
210 'post_parent' => $id,
211 'post_type' => 'attachment',
212 'fields' => 'ids',
213 )
214 );
215 $ids = array_map( 'intval', array_keys( $ids ) );
216 if ( has_post_thumbnail( $post ) ) {
217 $ids[] = (int) get_post_thumbnail_id( $post );
218 }
219 foreach ( array_unique( $ids ) as $media_id ) {
220 $media = get_post( $media_id );
221 if ( ! $media ) {
222 continue;
223 }
224 $rows[] = array(
225 'id' => $media_id,
226 'title' => '' !== $media->post_title ? (string) $media->post_title : sprintf( '#%d', $media_id ),
227 'subtitle' => (string) $media->post_mime_type,
228 'thumb' => (string) wp_get_attachment_image_url( $media_id, 'medium' ),
229 'icon' => 'dashicons-format-image',
230 'editUrl' => current_user_can( 'edit_post', $media_id )
231 ? admin_url( 'post.php?post=' . $media_id . '&action=edit' )
232 : '',
233 );
234 }
235 break;
236 case 'revisions':
237 $label = __( 'Revisions', 'desktop-mode' );
238 foreach ( wp_get_post_revisions( $id ) as $revision ) {
239 $rows[] = array(
240 'id' => (int) $revision->ID,
241 'title' => (string) wp_post_revision_title_expanded( $revision, false ),
242 'subtitle' => (string) get_the_author_meta( 'display_name', (int) $revision->post_author ),
243 'icon' => 'dashicons-backup',
244 'editUrl' => current_user_can( 'edit_post', $id )
245 ? admin_url( 'revision.php?revision=' . (int) $revision->ID )
246 : '',
247 );
248 }
249 break;
250 default:
251 return null;
252 }
253
254 return array(
255 'label' => $label,
256 'rows' => $rows,
257 );
258 }
259
260 /**
261 * Invoke one of WP Explorer's stats REST callbacks in-process, with a
262 * synthetic request — the panes render the SAME payloads WP Explorer
263 * renders, filters (`openstation_my_wordpress_term_stats` and
264 * friends) included.
265 *
266 * @param string $callback Function name.
267 * @param array<string,mixed> $params Request params.
268 * @return array<string,mixed>|null Null when unavailable or refused.
269 */
270 function stats_payload( $callback, array $params ) {
271 if ( ! function_exists( $callback ) || ! class_exists( '\WP_REST_Request' ) ) {
272 return null;
273 }
274 $request = new \WP_REST_Request();
275 foreach ( $params as $key => $value ) {
276 $request->set_param( $key, $value );
277 }
278 $payload = call_user_func( $callback, $request );
279 return is_array( $payload ) ? $payload : null;
280 }
281
282 /**
283 * The right-pane dossier for one SELECTED sub-list row, per relation:
284 * the term-stats card for a category or tag (stat tiles, 12-month
285 * activity, first/last post, recent posts), the user dossier + stats
286 * for author/contributors, the comment dossier, the media dossier
287 * with its usage scan, a revision preview.
288 *
289 * @param Os $os Host handle.
290 * @param array<string,mixed> $section Section descriptor.
291 * @param int $post_id Post navigated into.
292 * @param string $relation Relation slug.
293 * @param int $row_id Selected row id.
294 * @return array<string,mixed>|null
295 */
296 function sub_detail( Os $os, array $section, $post_id, $relation, $row_id ) {
297 switch ( $relation ) {
298 case 'categories':
299 case 'tags':
300 $stats = stats_payload(
301 'openstation_my_wordpress_term_stats_callback',
302 array(
303 'taxonomy' => 'tags' === $relation ? 'post_tag' : 'category',
304 'id' => $row_id,
305 )
306 );
307 return $stats ? array(
308 'kind' => 'term',
309 'stats' => $stats,
310 ) : null;
311
312 case 'author':
313 case 'contributors':
314 $user_section = array(
315 'kind' => 'user',
316 'post_type' => '',
317 );
318 $dossier = detail( $os, $user_section, $row_id );
319 if ( ! $dossier ) {
320 return null;
321 }
322 return array(
323 'kind' => 'user',
324 'detail' => $dossier,
325 'stats' => stats_payload( 'openstation_my_wordpress_user_stats_callback', array( 'id' => $row_id ) ),
326 );
327
328 case 'comments':
329 $stats = stats_payload( 'openstation_my_wordpress_comment_stats_callback', array( 'id' => $row_id ) );
330 return $stats ? array(
331 'kind' => 'comment',
332 'stats' => $stats,
333 ) : null;
334
335 case 'media':
336 $media_section = array(
337 'kind' => 'media',
338 'post_type' => 'attachment',
339 'thumbnails' => true,
340 );
341 $dossier = detail( $os, $media_section, $row_id );
342 return $dossier ? array(
343 'kind' => 'media',
344 'detail' => $dossier,
345 ) : null;
346
347 case 'revisions':
348 $revision = wp_get_post_revision( $row_id );
349 if ( ! $revision || (int) $revision->post_parent !== (int) $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
350 return null;
351 }
352 return array(
353 'kind' => 'revision',
354 'title' => (string) wp_post_revision_title_expanded( $revision, false ),
355 'author' => (string) get_the_author_meta( 'display_name', (int) $revision->post_author ),
356 'date' => (string) get_the_date( '', $revision ) . ' ' . get_the_time( '', $revision ),
357 'content' => wp_kses_post( (string) apply_filters( 'the_content', (string) $revision->post_content ) ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core's own content pipeline.
358 );
359 }
360 return null;
361 }
362
363 /**
364 * The choices the Edit… modal offers: the site's authors, the
365 * category tree (in `<os-category-picker>`'s item shape, `parent`
366 * included) and the tag terms the token field suggests from. Only
367 * computed while a post-kind section is open.
368 *
369 * @return array{authors:array[],categories:array[],tags:array[]}
370 */
371 function edit_choices() {
372 $authors = array();
373 foreach ( get_users(
374 array(
375 'capability' => array( 'edit_posts' ),
376 'number' => 100,
377 'orderby' => 'display_name',
378 'fields' => array( 'ID', 'display_name' ),
379 )
380 ) as $user ) {
381 $authors[] = array(
382 'id' => (int) $user->ID,
383 'name' => (string) $user->display_name,
384 );
385 }
386 $categories = array();
387 foreach ( get_terms(
388 array(
389 'taxonomy' => 'category',
390 'hide_empty' => false,
391 'number' => 100,
392 )
393 ) as $term ) {
394 if ( $term instanceof \WP_Term ) {
395 $categories[] = array(
396 'id' => (int) $term->term_id,
397 'name' => (string) $term->name,
398 'parent' => (int) $term->parent,
399 );
400 }
401 }
402 $tags = array();
403 foreach ( get_terms(
404 array(
405 'taxonomy' => 'post_tag',
406 'hide_empty' => false,
407 'number' => 100,
408 )
409 ) as $term ) {
410 if ( $term instanceof \WP_Term ) {
411 $tags[] = array(
412 'id' => (int) $term->term_id,
413 'name' => (string) $term->name,
414 );
415 }
416 }
417 return array(
418 'authors' => $authors,
419 'categories' => $categories,
420 'tags' => $tags,
421 );
422 }
423
424 /**
425 * The preview-action descriptors the acting user may see — the same
426 * `openstation_my_wordpress_preview_actions` pipeline WP Explorer
427 * collects, minus the fields the client does not need.
428 *
429 * @param Os $os Host handle.
430 * @return array<int,array<string,mixed>>
431 */
432 function preview_actions( Os $os ) {
433 if ( ! function_exists( 'openstation_my_wordpress_collect_preview_actions' ) ) {
434 return array();
435 }
436 $out = array();
437 foreach ( (array) openstation_my_wordpress_collect_preview_actions() as $action ) {
438 if ( ! is_array( $action ) || empty( $action['id'] ) ) {
439 continue;
440 }
441 if ( ! empty( $action['capability'] ) && ! $os->can( (string) $action['capability'] ) ) {
442 continue;
443 }
444 $out[] = array(
445 'id' => (string) $action['id'],
446 'label' => (string) ( $action['label'] ?? $action['id'] ),
447 'icon' => (string) ( $action['icon'] ?? '' ),
448 'sections' => array_map( 'strval', (array) ( $action['sections'] ?? array() ) ),
449 'mime' => (string) ( $action['mime'] ?? '' ),
450 );
451 }
452 return $out;
453 }
454