PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 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 All 35 releases
desktop-mode / apps / station-home / parts / snapshot.php

snapshot.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.11, at apps/station-home/parts/snapshot.php

376 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Station Home — the snapshot model.
4 *
5 * What the window paints, as plain data: the current user's recent
6 * work, the four site instruments, the attention queue, the
7 * capability-aware quick actions and the enabled plugin cards. Every
8 * reader is bounded — one query per number, WordPress's cached update
9 * totals, never a network request — and every string is escaped by
10 * the view, not here. Nothing in this file knows about the window.
11 *
12 * @package OpenStation
13 */
14
15 namespace OpenStation\Apps\StationHome;
16
17 use OpenStation\App\Os;
18 use WP_Query;
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Post types that the current user can edit and that belong in
24 * recent work.
25 *
26 * @param Os $os Host handle.
27 * @return string[]
28 */
29 function editable_post_types( Os $os ) {
30 $types = array();
31 foreach ( get_post_types( array( 'show_ui' => true ), 'objects' ) as $type ) {
32 if ( ! is_object( $type ) || 'attachment' === $type->name ) {
33 continue;
34 }
35 // Core's internal editor records (`wp_navigation`, `wp_block`,
36 // templates, styles) are implementation details rather than work
37 // a person expects to resume from a home screen. Keep Posts and
38 // Pages, then admit public UI-visible custom types.
39 if ( ! in_array( $type->name, array( 'post', 'page' ), true ) && ! $type->public ) {
40 continue;
41 }
42 if ( $os->can( $type->cap->edit_posts ) ) {
43 $types[] = $type->name;
44 }
45 }
46 return array_values( array_unique( $types ) );
47 }
48
49 /**
50 * The current user's five most recently modified editable items.
51 *
52 * @param Os $os Host handle.
53 * @param string[] $post_types Editable post types.
54 * @return array[]
55 */
56 function recent_work( Os $os, array $post_types ) {
57 if ( array() === $post_types ) {
58 return array();
59 }
60
61 $query = new WP_Query(
62 array(
63 'post_type' => $post_types,
64 'post_status' => array( 'draft', 'pending', 'future', 'private', 'publish' ),
65 'author' => $os->auth->user_id(),
66 'posts_per_page' => 5,
67 'orderby' => 'modified',
68 'order' => 'DESC',
69 'ignore_sticky_posts' => true,
70 'no_found_rows' => true,
71 'update_post_meta_cache' => false,
72 'update_post_term_cache' => false,
73 )
74 );
75
76 $items = array();
77 foreach ( $query->posts as $post ) {
78 $edit_url = get_edit_post_link( $post->ID, 'raw' );
79 if ( ! is_string( $edit_url ) || '' === $edit_url ) {
80 continue;
81 }
82
83 $type_object = get_post_type_object( $post->post_type );
84 $status_object = get_post_status_object( $post->post_status );
85 $title = get_the_title( $post );
86 $modified_gmt = (string) get_post_field( 'post_modified_gmt', $post );
87 $modified_iso = '';
88 if ( '' !== $modified_gmt && '0000-00-00 00:00:00' !== $modified_gmt ) {
89 $timestamp = strtotime( $modified_gmt . ' UTC' );
90 if ( false !== $timestamp ) {
91 $modified_iso = gmdate( 'c', $timestamp );
92 }
93 }
94
95 $items[] = array(
96 'id' => (int) $post->ID,
97 'title' => '' !== trim( $title ) ? $title : __( '(Untitled)', 'desktop-mode' ),
98 'typeLabel' => $type_object ? $type_object->labels->singular_name : __( 'Content', 'desktop-mode' ),
99 'icon' => 'page' === $post->post_type ? 'dashicons-admin-page' : 'dashicons-admin-post',
100 'status' => (string) $post->post_status,
101 'statusLabel' => $status_object ? $status_object->label : ucfirst( (string) $post->post_status ),
102 'modifiedGmt' => $modified_iso,
103 'editUrl' => esc_url_raw( $edit_url ),
104 );
105 }
106
107 return $items;
108 }
109
110 /**
111 * Count the current user's drafts across the editable types.
112 *
113 * @param Os $os Host handle.
114 * @param string[] $post_types Editable post types.
115 * @return int
116 */
117 function draft_count( Os $os, array $post_types ) {
118 if ( array() === $post_types ) {
119 return 0;
120 }
121 $query = new WP_Query(
122 array(
123 'post_type' => $post_types,
124 'post_status' => 'draft',
125 'author' => $os->auth->user_id(),
126 'posts_per_page' => 1,
127 'fields' => 'ids',
128 'no_found_rows' => false,
129 )
130 );
131 return (int) $query->found_posts;
132 }
133
134 /**
135 * Count published posts and pages on the site.
136 *
137 * @return int
138 */
139 function published_count() {
140 $total = 0;
141 foreach ( array( 'post', 'page' ) as $post_type ) {
142 $counts = wp_count_posts( $post_type );
143 $total += isset( $counts->publish ) ? (int) $counts->publish : 0;
144 }
145 return $total;
146 }
147
148 /**
149 * Count image attachments whose alternative text is empty or absent —
150 * only for a user who can remediate it.
151 *
152 * @param Os $os Host handle.
153 * @return int
154 */
155 function missing_alt_count( Os $os ) {
156 if ( ! $os->can( 'upload_files' ) ) {
157 return 0;
158 }
159
160 $query = new WP_Query(
161 array(
162 'post_type' => 'attachment',
163 'post_status' => 'inherit',
164 'post_mime_type' => 'image',
165 'posts_per_page' => 1,
166 'fields' => 'ids',
167 'no_found_rows' => false,
168 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- one bounded dashboard count, only for users who can remediate it.
169 'relation' => 'OR',
170 array(
171 'key' => '_wp_attachment_image_alt',
172 'compare' => 'NOT EXISTS',
173 ),
174 array(
175 'key' => '_wp_attachment_image_alt',
176 'value' => '',
177 'compare' => '=',
178 ),
179 ),
180 )
181 );
182 return (int) $query->found_posts;
183 }
184
185 /**
186 * WordPress's cached update totals — no network request is made.
187 *
188 * @param Os $os Host handle.
189 * @return int
190 */
191 function update_count( Os $os ) {
192 if ( ! $os->can( 'update_core' ) && ! $os->can( 'update_plugins' ) && ! $os->can( 'update_themes' ) ) {
193 return 0;
194 }
195
196 if ( ! function_exists( 'wp_get_update_data' ) ) {
197 require_once ABSPATH . 'wp-admin/includes/update.php';
198 }
199 $data = wp_get_update_data();
200 return isset( $data['counts']['total'] ) ? (int) $data['counts']['total'] : 0;
201 }
202
203 /**
204 * The rail's quick actions, gated per capability.
205 *
206 * `url` and `external` actions are links the shell's link interceptor
207 * opens in a window (or, for `external`, a new tab). `native` and
208 * `classic` actions have no URL the interceptor may follow — a native
209 * window has none, and the classic escape is the one admin URL the
210 * interceptor deliberately refuses — so they are buttons that
211 * dispatch `launch`, which turns them into the `open` / `open_url`
212 * effects.
213 *
214 * @param Os $os Host handle.
215 * @return array[]
216 */
217 function quick_actions( Os $os ) {
218 $actions = array();
219 if ( $os->can( 'edit_posts' ) ) {
220 $actions[] = array(
221 'id' => 'new-post',
222 'label' => __( 'New post', 'desktop-mode' ),
223 'icon' => 'dashicons-edit',
224 'kind' => 'url',
225 'url' => esc_url_raw( admin_url( 'post-new.php' ) ),
226 );
227 }
228 if ( $os->can( 'upload_files' ) ) {
229 $actions[] = array(
230 'id' => 'upload-media',
231 'label' => __( 'Upload media', 'desktop-mode' ),
232 'icon' => 'dashicons-upload',
233 'kind' => 'url',
234 'url' => esc_url_raw( admin_url( 'media-new.php' ) ),
235 );
236 }
237 $actions[] = array(
238 'id' => 'view-site',
239 'label' => __( 'View site', 'desktop-mode' ),
240 'icon' => 'dashicons-visibility',
241 'kind' => 'external',
242 'url' => esc_url_raw( home_url( '/' ) ),
243 );
244 if ( function_exists( 'openstation_my_wordpress_user_can_use' ) && openstation_my_wordpress_user_can_use() ) {
245 $actions[] = array(
246 'id' => 'wp-explorer',
247 'label' => __( 'WP Explorer', 'desktop-mode' ),
248 'icon' => 'dashicons-open-folder',
249 'kind' => 'native',
250 'windowId' => 'my-wordpress',
251 );
252 }
253 $actions[] = array(
254 'id' => 'classic-dashboard',
255 'label' => __( 'Classic Dashboard', 'desktop-mode' ),
256 'icon' => 'dashicons-dashboard',
257 'kind' => 'classic',
258 'url' => esc_url_raw( add_query_arg( OPENSTATION_CLASSIC_FLAG, '1', admin_url( 'index.php' ) ) ),
259 );
260 return $actions;
261 }
262
263 /**
264 * The attention queue: pending comments, available updates, images
265 * without alt text — each only when its count is above zero.
266 *
267 * @param int $pending Pending comments the user may moderate.
268 * @param int $updates Available updates the user may apply.
269 * @param int $missing_alt Images without alt text the user may fix.
270 * @return array[]
271 */
272 function attention( $pending, $updates, $missing_alt ) {
273 $queue = array();
274 if ( $pending > 0 ) {
275 $queue[] = array(
276 'id' => 'comments',
277 'icon' => 'dashicons-admin-comments',
278 'count' => $pending,
279 'label' => sprintf(
280 /* translators: %s: pending comment count. */
281 _n( '%s pending comment', '%s pending comments', $pending, 'desktop-mode' ),
282 number_format_i18n( $pending )
283 ),
284 'description' => __( 'Review and reply to keep the conversation moving.', 'desktop-mode' ),
285 'url' => esc_url_raw( admin_url( 'edit-comments.php?comment_status=moderated' ) ),
286 );
287 }
288 if ( $updates > 0 ) {
289 $queue[] = array(
290 'id' => 'updates',
291 'icon' => 'dashicons-update',
292 'count' => $updates,
293 'label' => sprintf(
294 /* translators: %s: update count. */
295 _n( '%s update available', '%s updates available', $updates, 'desktop-mode' ),
296 number_format_i18n( $updates )
297 ),
298 'description' => __( 'Keep WordPress, themes, and plugins current.', 'desktop-mode' ),
299 'url' => esc_url_raw( admin_url( 'update-core.php' ) ),
300 );
301 }
302 if ( $missing_alt > 0 ) {
303 $queue[] = array(
304 'id' => 'missing-alt',
305 'icon' => 'dashicons-format-image',
306 'count' => $missing_alt,
307 'label' => sprintf(
308 /* translators: %s: image count. */
309 _n( '%s image needs alt text', '%s images need alt text', $missing_alt, 'desktop-mode' ),
310 number_format_i18n( $missing_alt )
311 ),
312 'description' => __( 'Add a useful description for people using assistive technology.', 'desktop-mode' ),
313 'url' => esc_url_raw( admin_url( 'upload.php?mode=list' ) ),
314 );
315 }
316 return $queue;
317 }
318
319 /**
320 * Assemble the role-aware snapshot the view paints from.
321 *
322 * @param Os $os Host handle.
323 * @return array<string,mixed>
324 */
325 function snapshot( Os $os ) {
326 $user = wp_get_current_user();
327 $post_types = editable_post_types( $os );
328 $drafts = draft_count( $os, $post_types );
329 $published = published_count();
330 $comment_data = wp_count_comments();
331 $pending = $os->can( 'moderate_comments' ) ? (int) $comment_data->moderated : 0;
332 $updates = update_count( $os );
333 $missing_alt = missing_alt_count( $os );
334 $first_name = trim( (string) get_user_meta( $user->ID, 'first_name', true ) );
335 $cards = openstation_station_home_build_cards(
336 openstation_station_home_get_registered_cards(),
337 openstation_station_home_get_card_preferences( $user->ID )
338 );
339
340 return array(
341 'userName' => '' !== $first_name ? $first_name : $user->display_name,
342 'siteName' => get_bloginfo( 'name' ),
343 'work' => recent_work( $os, $post_types ),
344 'quickActions' => quick_actions( $os ),
345 'metrics' => array(
346 array(
347 'id' => 'drafts',
348 'label' => __( 'Drafts', 'desktop-mode' ),
349 'icon' => 'dashicons-edit',
350 'value' => $drafts,
351 ),
352 array(
353 'id' => 'comments',
354 'label' => __( 'Pending comments', 'desktop-mode' ),
355 'icon' => 'dashicons-admin-comments',
356 'value' => $pending,
357 ),
358 array(
359 'id' => 'updates',
360 'label' => __( 'Updates', 'desktop-mode' ),
361 'icon' => 'dashicons-update',
362 'value' => $updates,
363 ),
364 array(
365 'id' => 'published',
366 'label' => __( 'Published', 'desktop-mode' ),
367 'icon' => 'dashicons-admin-site-alt3',
368 'value' => $published,
369 ),
370 ),
371 'attention' => attention( $pending, $updates, $missing_alt ),
372 'cards' => $cards['cards'],
373 'cardPreferences' => $cards['preferences'],
374 );
375 }
376