PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.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 / includes / comments-window / rest.php

rest.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.6, at includes/comments-window/rest.php

394 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Native Comments Window: REST mutation + helper routes.
4 *
5 * Four endpoints under `desktop-mode/v1`:
6 *
7 * - POST /comments/bulk { ids: int[], action: 'approve'|'unapprove'|'spam'|'unspam'|'trash'|'untrash' }
8 * - POST /comments/reply { parent: int, content: string }
9 * - GET /comments/insights/<email>
10 * - GET /comments/counts
11 *
12 * SECURITY POSTURE
13 * ================
14 *
15 * 1. `permission_callback` — broad cap gate
16 * (`moderate_comments`, `edit_posts`).
17 * 2. Per-target re-validation inside the callback —
18 * `current_user_can( 'edit_comment', $id )` per row.
19 *
20 * @package WPDesktopMode
21 * @since 0.8.3
22 */
23
24 defined( 'ABSPATH' ) || exit;
25
26 /**
27 * Allowed bulk actions, mapped to the function that performs them on a single id.
28 *
29 * Each callback returns true on success, false on a soft failure (the
30 * row is skipped) and throws nothing — the bulk endpoint logs misses
31 * but never aborts the batch on a single bad row.
32 *
33 * @since 0.8.3
34 *
35 * @return array<string,callable>
36 */
37 function desktop_mode_comments_window_bulk_action_map() {
38 return array(
39 'approve' => static function ( $id ) {
40 return false !== wp_set_comment_status( $id, 'approve' );
41 },
42 'unapprove' => static function ( $id ) {
43 return false !== wp_set_comment_status( $id, 'hold' );
44 },
45 'spam' => static function ( $id ) {
46 return false !== wp_spam_comment( $id );
47 },
48 'unspam' => static function ( $id ) {
49 return false !== wp_unspam_comment( $id );
50 },
51 'trash' => static function ( $id ) {
52 return false !== wp_trash_comment( $id );
53 },
54 'untrash' => static function ( $id ) {
55 return false !== wp_untrash_comment( $id );
56 },
57 );
58 }
59
60 /**
61 * Register all routes.
62 *
63 * @since 0.8.3
64 */
65 function desktop_mode_comments_window_register_rest_routes() {
66 register_rest_route(
67 'desktop-mode/v1',
68 '/comments/bulk',
69 array(
70 'methods' => WP_REST_Server::CREATABLE,
71 'callback' => 'desktop_mode_comments_window_rest_bulk',
72 'permission_callback' => static function () {
73 return current_user_can( 'moderate_comments' );
74 },
75 'args' => array(
76 'ids' => array(
77 'required' => true,
78 'type' => 'array',
79 'items' => array( 'type' => 'integer' ),
80 ),
81 'action' => array(
82 'required' => true,
83 'type' => 'string',
84 'enum' => array_keys( desktop_mode_comments_window_bulk_action_map() ),
85 ),
86 ),
87 )
88 );
89
90 register_rest_route(
91 'desktop-mode/v1',
92 '/comments/reply',
93 array(
94 'methods' => WP_REST_Server::CREATABLE,
95 'callback' => 'desktop_mode_comments_window_rest_reply',
96 'permission_callback' => static function () {
97 return current_user_can( 'edit_posts' );
98 },
99 'args' => array(
100 'parent' => array(
101 'required' => true,
102 'type' => 'integer',
103 ),
104 'content' => array(
105 'required' => true,
106 'type' => 'string',
107 ),
108 ),
109 )
110 );
111
112 register_rest_route(
113 'desktop-mode/v1',
114 '/comments/insights/(?P<email>[^/]+)',
115 array(
116 'methods' => WP_REST_Server::READABLE,
117 'callback' => 'desktop_mode_comments_window_rest_insights',
118 'permission_callback' => static function () {
119 return current_user_can( 'moderate_comments' );
120 },
121 'args' => array(
122 'email' => array(
123 'required' => true,
124 'type' => 'string',
125 ),
126 ),
127 )
128 );
129
130 register_rest_route(
131 'desktop-mode/v1',
132 '/comments/counts',
133 array(
134 'methods' => WP_REST_Server::READABLE,
135 'callback' => 'desktop_mode_comments_window_rest_counts',
136 'permission_callback' => static function () {
137 return current_user_can( 'edit_posts' );
138 },
139 )
140 );
141 }
142 add_action( 'rest_api_init', 'desktop_mode_comments_window_register_rest_routes' );
143
144 /**
145 * Bulk moderation handler.
146 *
147 * @since 0.8.3
148 *
149 * @param WP_REST_Request $request Request.
150 * @return WP_REST_Response|WP_Error
151 */
152 function desktop_mode_comments_window_rest_bulk( WP_REST_Request $request ) {
153 $ids = array_values( array_filter( array_map( 'intval', (array) $request['ids'] ) ) );
154 $action = (string) $request['action'];
155 $map = desktop_mode_comments_window_bulk_action_map();
156
157 if ( ! isset( $map[ $action ] ) ) {
158 return new WP_Error(
159 'desktop_mode_comments_invalid_action',
160 __( 'Unknown bulk action.', 'desktop-mode' ),
161 array( 'status' => 400 )
162 );
163 }
164
165 $cb = $map[ $action ];
166 $processed = array();
167 $skipped = array();
168
169 foreach ( $ids as $id ) {
170 if ( ! current_user_can( 'edit_comment', $id ) ) {
171 $skipped[] = $id;
172 continue;
173 }
174 if ( $cb( $id ) ) {
175 $processed[] = $id;
176 } else {
177 $skipped[] = $id;
178 }
179 }
180
181 /**
182 * Fires after a Comments-window bulk action runs.
183 *
184 * @since 0.8.3
185 *
186 * @param string $action Action slug.
187 * @param int[] $processed Ids successfully acted on.
188 * @param int[] $skipped Ids skipped (cap fail or soft error).
189 */
190 do_action(
191 'desktop_mode_comments_window_after_bulk',
192 $action,
193 $processed,
194 $skipped
195 );
196
197 return new WP_REST_Response(
198 array(
199 'action' => $action,
200 'processed' => $processed,
201 'skipped' => $skipped,
202 'counts' => desktop_mode_comments_window_counts(),
203 ),
204 200
205 );
206 }
207
208 /**
209 * Inline-reply handler. Wraps `wp_new_comment` with sane defaults so
210 * the client only needs `{ parent, content }`.
211 *
212 * @since 0.8.3
213 *
214 * @param WP_REST_Request $request Request.
215 * @return WP_REST_Response|WP_Error
216 */
217 function desktop_mode_comments_window_rest_reply( WP_REST_Request $request ) {
218 $parent_id = (int) $request['parent'];
219 $content = (string) $request['content'];
220
221 $parent = get_comment( $parent_id );
222 if ( ! $parent instanceof WP_Comment ) {
223 return new WP_Error(
224 'desktop_mode_comments_no_parent',
225 __( 'Parent comment not found.', 'desktop-mode' ),
226 array( 'status' => 404 )
227 );
228 }
229
230 // Per-target re-validation: mirror core's wp_ajax_replyto_comment gate,
231 // which requires edit_post on the comment's post.
232 $post = get_post( (int) $parent->comment_post_ID );
233 if ( ! $post instanceof WP_Post || ! current_user_can( 'edit_post', $post->ID ) ) {
234 return new WP_Error(
235 'desktop_mode_comments_forbidden',
236 __( 'You are not allowed to reply to comments on this post.', 'desktop-mode' ),
237 array( 'status' => 403 )
238 );
239 }
240
241 if ( '' === trim( wp_strip_all_tags( $content ) ) ) {
242 return new WP_Error(
243 'desktop_mode_comments_empty_reply',
244 __( 'Reply cannot be empty.', 'desktop-mode' ),
245 array( 'status' => 400 )
246 );
247 }
248
249 $user = wp_get_current_user();
250 if ( ! $user || ! $user->ID ) {
251 return new WP_Error(
252 'desktop_mode_comments_unauthenticated',
253 __( 'You must be logged in to reply.', 'desktop-mode' ),
254 array( 'status' => 401 )
255 );
256 }
257
258 $comment_data = array(
259 'comment_post_ID' => (int) $parent->comment_post_ID,
260 'comment_parent' => $parent_id,
261 'user_id' => (int) $user->ID,
262 'comment_author' => (string) $user->display_name,
263 'comment_author_email' => (string) $user->user_email,
264 'comment_author_url' => (string) $user->user_url,
265 'comment_content' => $content,
266 'comment_approved' => 1,
267 'comment_type' => 'comment',
268 );
269
270 $new_id = wp_new_comment( wp_slash( $comment_data ), true );
271 if ( is_wp_error( $new_id ) ) {
272 return $new_id;
273 }
274
275 $new = get_comment( $new_id );
276 return new WP_REST_Response(
277 array(
278 'id' => (int) $new_id,
279 'parent' => $parent_id,
280 'content' => $new ? (string) $new->comment_content : $content,
281 'date_gmt' => $new ? (string) $new->comment_date_gmt : '',
282 'author' => $user->display_name,
283 'avatarUrl' => (string) get_avatar_url( (int) $user->ID, array( 'size' => 96 ) ),
284 ),
285 201
286 );
287 }
288
289 /**
290 * Author insights endpoint — drives the side drawer.
291 *
292 * Returns total/approved/pending/spam counts, oldest/newest comment
293 * timestamps, the linked user id (if the email matches a registered
294 * user), and a 0–100 reliability score.
295 *
296 * @since 0.8.3
297 *
298 * @param WP_REST_Request $request Request.
299 * @return WP_REST_Response|WP_Error
300 */
301 function desktop_mode_comments_window_rest_insights( WP_REST_Request $request ) {
302 $email = strtolower( urldecode( (string) $request['email'] ) );
303 if ( '' === $email || ! is_email( $email ) ) {
304 return new WP_Error(
305 'desktop_mode_comments_invalid_email',
306 __( 'Invalid author email.', 'desktop-mode' ),
307 array( 'status' => 400 )
308 );
309 }
310
311 $counts_by_status = array();
312 foreach ( array( 'approve', 'hold', 'spam', 'trash' ) as $status ) {
313 $counts_by_status[ $status ] = (int) get_comments(
314 array(
315 'author_email' => $email,
316 'status' => $status,
317 'count' => true,
318 )
319 );
320 }
321 $total = array_sum( $counts_by_status );
322
323 // Sample the oldest + newest record without loading every row.
324 $oldest = get_comments(
325 array(
326 'author_email' => $email,
327 'status' => 'all',
328 'orderby' => 'comment_date_gmt',
329 'order' => 'ASC',
330 'number' => 1,
331 )
332 );
333 $newest = get_comments(
334 array(
335 'author_email' => $email,
336 'status' => 'all',
337 'orderby' => 'comment_date_gmt',
338 'order' => 'DESC',
339 'number' => 1,
340 )
341 );
342
343 $user = get_user_by( 'email', $email );
344 $reliability = 100;
345 if ( $total > 0 ) {
346 $bad = $counts_by_status['spam'] + $counts_by_status['trash'];
347 $reliability = (int) round( max( 0, min( 100, 100 - ( $bad / $total ) * 100 ) ) );
348 }
349
350 return new WP_REST_Response(
351 array(
352 'email' => $email,
353 'total' => $total,
354 'counts' => $counts_by_status,
355 'oldest' => isset( $oldest[0] ) ? (string) $oldest[0]->comment_date_gmt : null,
356 'newest' => isset( $newest[0] ) ? (string) $newest[0]->comment_date_gmt : null,
357 'userId' => $user ? (int) $user->ID : 0,
358 'userName' => $user ? (string) $user->display_name : '',
359 'reliability' => $reliability,
360 'avatarUrl' => (string) get_avatar_url( $email, array( 'size' => 96 ) ),
361 ),
362 200
363 );
364 }
365
366 /**
367 * Per-status counts. Used by the dock badge + the "N new" pill.
368 *
369 * @since 0.8.3
370 *
371 * @return WP_REST_Response
372 */
373 function desktop_mode_comments_window_rest_counts() {
374 return new WP_REST_Response( desktop_mode_comments_window_counts(), 200 );
375 }
376
377 /**
378 * Internal helper — current comment counts as a flat array.
379 *
380 * @since 0.8.3
381 *
382 * @return array<string,int>
383 */
384 function desktop_mode_comments_window_counts() {
385 $counts = wp_count_comments();
386 return array(
387 'pending' => (int) $counts->moderated,
388 'approved' => (int) $counts->approved,
389 'spam' => (int) $counts->spam,
390 'trash' => (int) $counts->trash,
391 'total' => (int) $counts->total_comments,
392 );
393 }
394