PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.7
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 / games / rest.php

rest.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.7, at includes/games/rest.php

503 lines 16.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 — Games REST routes.
4 *
5 * Namespace `desktop-mode/v1`:
6 *
7 * GET /games/(?P<game>[a-z0-9_\-]+)/scores Leaderboard (paged).
8 * POST /games/(?P<game>[a-z0-9_\-]+)/scores Submit own score.
9 * GET /games/challenges Challenges involving me.
10 * POST /games/challenges Send a challenge.
11 * POST /games/challenges/(?P<id>\d+)/accept Accept (recipient).
12 * POST /games/challenges/(?P<id>\d+)/decline Decline (recipient).
13 * POST /games/challenges/(?P<id>\d+)/complete Report the run (recipient).
14 * GET /games/users/search Opponent-picker autocomplete.
15 * GET /games/playtime My per-game play-time totals.
16 * POST /games/(?P<game>[a-z0-9_\-]+)/playtime Record own play time.
17 *
18 * Permission: every route requires a logged-in user with desktop
19 * mode enabled and the `read` capability (subscribers play games
20 * too). Unknown game ids 404. Challenge routes additionally verify
21 * party membership; sending gates through the
22 * `desktop_mode_games_can_challenge` filter.
23 *
24 * @package WPDesktopMode
25 * @since 0.9.6
26 */
27
28 defined( 'ABSPATH' ) || exit;
29
30 /**
31 * Base permission gate shared by every games route.
32 *
33 * @since 0.9.6
34 */
35 function desktop_mode_games_rest_permission() {
36 if ( ! is_user_logged_in() ) {
37 return new WP_Error( 'desktop_mode_games_unauthenticated', __( 'You must be logged in.', 'desktop-mode' ), array( 'status' => 401 ) );
38 }
39 if ( function_exists( 'desktop_mode_is_enabled' ) && ! desktop_mode_is_enabled( get_current_user_id() ) ) {
40 return new WP_Error( 'desktop_mode_games_disabled', __( 'Desktop mode is not enabled for this user.', 'desktop-mode' ), array( 'status' => 403 ) );
41 }
42 if ( ! current_user_can( 'read' ) ) {
43 return new WP_Error( 'desktop_mode_games_forbidden', __( 'You cannot use desktop games.', 'desktop-mode' ), array( 'status' => 403 ) );
44 }
45
46 /**
47 * Filters the base games REST permission verdict. Return a
48 * `WP_Error` (or `false`) to lock the whole surface down below
49 * the default logged-in + `read` gate.
50 *
51 * @since 0.9.6
52 *
53 * @param true|false|WP_Error $allowed Default `true`.
54 * @param int $user_id Current user.
55 */
56 $allowed = apply_filters( 'desktop_mode_games_rest_permission', true, get_current_user_id() );
57 if ( is_wp_error( $allowed ) ) {
58 return $allowed;
59 }
60 if ( true !== $allowed ) {
61 return new WP_Error( 'desktop_mode_games_forbidden', __( 'You cannot use desktop games.', 'desktop-mode' ), array( 'status' => 403 ) );
62 }
63 return true;
64 }
65
66 /**
67 * Register the routes.
68 *
69 * @since 0.9.6
70 */
71 function desktop_mode_games_register_rest_routes() {
72 $ns = 'desktop-mode/v1';
73
74 register_rest_route( $ns, '/games/(?P<game>[a-z0-9_\-]+)/scores', array(
75 array(
76 'methods' => WP_REST_Server::READABLE,
77 'permission_callback' => 'desktop_mode_games_rest_permission',
78 'callback' => 'desktop_mode_games_rest_list_scores',
79 'args' => array(
80 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1 ),
81 'per_page' => array( 'type' => 'integer', 'default' => 25, 'minimum' => 1, 'maximum' => 100 ),
82 'orderby' => array( 'type' => 'string', 'default' => 'score', 'enum' => array( 'score', 'created' ) ),
83 'order' => array( 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc' ) ),
84 'user_id' => array( 'type' => 'integer', 'default' => 0 ),
85 ),
86 ),
87 array(
88 'methods' => WP_REST_Server::CREATABLE,
89 'permission_callback' => 'desktop_mode_games_rest_permission',
90 'callback' => 'desktop_mode_games_rest_submit_score',
91 'args' => array(
92 'score' => array( 'type' => 'integer', 'required' => true, 'minimum' => 0 ),
93 'meta' => array( 'type' => 'object', 'default' => array() ),
94 ),
95 ),
96 ) );
97
98 register_rest_route( $ns, '/games/challenges', array(
99 array(
100 'methods' => WP_REST_Server::READABLE,
101 'permission_callback' => 'desktop_mode_games_rest_permission',
102 'callback' => 'desktop_mode_games_rest_list_challenges',
103 'args' => array(
104 'box' => array( 'type' => 'string', 'default' => 'all', 'enum' => array( 'incoming', 'outgoing', 'all' ) ),
105 'state' => array( 'type' => 'string', 'default' => '', 'enum' => array( '', 'pending', 'accepted', 'declined', 'completed' ) ),
106 ),
107 ),
108 array(
109 'methods' => WP_REST_Server::CREATABLE,
110 'permission_callback' => 'desktop_mode_games_rest_permission',
111 'callback' => 'desktop_mode_games_rest_create_challenge',
112 'args' => array(
113 'game' => array( 'type' => 'string', 'required' => true ),
114 'recipient_id' => array( 'type' => 'integer', 'required' => true ),
115 'score' => array( 'type' => 'integer', 'required' => true, 'minimum' => 0 ),
116 'meta' => array( 'type' => 'object', 'default' => array() ),
117 ),
118 ),
119 ) );
120
121 register_rest_route( $ns, '/games/challenges/(?P<id>\d+)/accept', array(
122 'methods' => WP_REST_Server::CREATABLE,
123 'permission_callback' => 'desktop_mode_games_rest_permission',
124 'callback' => 'desktop_mode_games_rest_accept_challenge',
125 ) );
126
127 register_rest_route( $ns, '/games/challenges/(?P<id>\d+)/decline', array(
128 'methods' => WP_REST_Server::CREATABLE,
129 'permission_callback' => 'desktop_mode_games_rest_permission',
130 'callback' => 'desktop_mode_games_rest_decline_challenge',
131 ) );
132
133 register_rest_route( $ns, '/games/challenges/(?P<id>\d+)/complete', array(
134 'methods' => WP_REST_Server::CREATABLE,
135 'permission_callback' => 'desktop_mode_games_rest_permission',
136 'callback' => 'desktop_mode_games_rest_complete_challenge',
137 'args' => array(
138 'score' => array( 'type' => 'integer', 'required' => true, 'minimum' => 0 ),
139 'meta' => array( 'type' => 'object', 'default' => array() ),
140 ),
141 ) );
142
143 register_rest_route( $ns, '/games/playtime', array(
144 'methods' => WP_REST_Server::READABLE,
145 'permission_callback' => 'desktop_mode_games_rest_permission',
146 'callback' => 'desktop_mode_games_rest_get_playtime',
147 ) );
148
149 register_rest_route( $ns, '/games/(?P<game>[a-z0-9_\-]+)/playtime', array(
150 'methods' => WP_REST_Server::CREATABLE,
151 'permission_callback' => 'desktop_mode_games_rest_permission',
152 'callback' => 'desktop_mode_games_rest_record_playtime',
153 'args' => array(
154 'seconds' => array( 'type' => 'integer', 'required' => true, 'minimum' => 1 ),
155 ),
156 ) );
157
158 register_rest_route( $ns, '/games/users/search', array(
159 'methods' => WP_REST_Server::READABLE,
160 'permission_callback' => 'desktop_mode_games_rest_permission',
161 'callback' => 'desktop_mode_games_rest_search_users',
162 'args' => array(
163 'q' => array( 'type' => 'string', 'default' => '' ),
164 'exclude' => array( 'type' => 'string', 'default' => '' ),
165 ),
166 ) );
167 }
168 add_action( 'rest_api_init', 'desktop_mode_games_register_rest_routes' );
169
170 /**
171 * Resolve + validate the `game` path param. 404s unknown ids so the
172 * scores surface doesn't leak which games exist server-side.
173 *
174 * @since 0.9.6
175 * @internal
176 *
177 * @param WP_REST_Request $req Request.
178 * @return string|WP_Error The sanitized game id.
179 */
180 function desktop_mode_games_rest_resolve_game( WP_REST_Request $req ) {
181 $game = sanitize_key( (string) $req->get_param( 'game' ) );
182 if ( '' === $game || ! desktop_mode_games_is_registered( $game ) ) {
183 return new WP_Error(
184 'desktop_mode_unknown_game',
185 __( 'Unknown game.', 'desktop-mode' ),
186 array( 'status' => 404 )
187 );
188 }
189 return $game;
190 }
191
192 /**
193 * GET /games/{game}/scores
194 *
195 * @since 0.9.6
196 */
197 function desktop_mode_games_rest_list_scores( WP_REST_Request $req ) {
198 $game = desktop_mode_games_rest_resolve_game( $req );
199 if ( is_wp_error( $game ) ) {
200 return $game;
201 }
202 $result = desktop_mode_games_get_scores( $game, array(
203 'page' => (int) $req->get_param( 'page' ),
204 'per_page' => (int) $req->get_param( 'per_page' ),
205 'orderby' => (string) $req->get_param( 'orderby' ),
206 'order' => (string) $req->get_param( 'order' ),
207 'user_id' => (int) $req->get_param( 'user_id' ),
208 ) );
209 return rest_ensure_response( array(
210 'scores' => $result['rows'],
211 'total' => $result['total'],
212 ) );
213 }
214
215 /**
216 * POST /games/{game}/scores — always records for the current user;
217 * there is no way to submit a score on someone else's behalf.
218 *
219 * @since 0.9.6
220 */
221 function desktop_mode_games_rest_submit_score( WP_REST_Request $req ) {
222 $game = desktop_mode_games_rest_resolve_game( $req );
223 if ( is_wp_error( $game ) ) {
224 return $game;
225 }
226 $id = desktop_mode_games_save_score(
227 $game,
228 get_current_user_id(),
229 (int) $req->get_param( 'score' ),
230 (array) $req->get_param( 'meta' )
231 );
232 if ( is_wp_error( $id ) ) {
233 return $id;
234 }
235 return rest_ensure_response( array( 'id' => $id ) );
236 }
237
238 /**
239 * GET /games/playtime — the current user's `game id => seconds` map.
240 *
241 * @since 0.9.7
242 */
243 function desktop_mode_games_rest_get_playtime() {
244 $user_id = get_current_user_id();
245 // Day maps are cast per-game so empty buckets JSON-encode as `{}`.
246 $daily = array();
247 foreach ( desktop_mode_games_get_playtime_daily( $user_id ) as $game => $days ) {
248 $daily[ $game ] = (object) $days;
249 }
250 return rest_ensure_response( array(
251 'playtime' => (object) desktop_mode_games_get_playtime( $user_id ),
252 'daily' => (object) $daily,
253 'today' => desktop_mode_games_playtime_today_key(),
254 ) );
255 }
256
257 /**
258 * POST /games/{game}/playtime — always records for the current user;
259 * there is no way to record play time on someone else's behalf.
260 *
261 * @since 0.9.7
262 */
263 function desktop_mode_games_rest_record_playtime( WP_REST_Request $req ) {
264 $game = desktop_mode_games_rest_resolve_game( $req );
265 if ( is_wp_error( $game ) ) {
266 return $game;
267 }
268 $total = desktop_mode_games_add_playtime(
269 $game,
270 get_current_user_id(),
271 (int) $req->get_param( 'seconds' )
272 );
273 if ( is_wp_error( $total ) ) {
274 return $total;
275 }
276 return rest_ensure_response( array( 'total' => $total ) );
277 }
278
279 /**
280 * GET /games/challenges — challenges involving the current user.
281 *
282 * @since 0.9.6
283 */
284 function desktop_mode_games_rest_list_challenges( WP_REST_Request $req ) {
285 $user_id = get_current_user_id();
286 $box = (string) $req->get_param( 'box' );
287 $state = (string) $req->get_param( 'state' );
288
289 $rows = desktop_mode_games_get_challenges_for_user( $user_id, 0, 100 );
290 $out = array();
291 foreach ( $rows as $row ) {
292 if ( 'incoming' === $box && (int) $row['recipient_id'] !== $user_id ) {
293 continue;
294 }
295 if ( 'outgoing' === $box && (int) $row['challenger_id'] !== $user_id ) {
296 continue;
297 }
298 if ( '' !== $state && $row['state'] !== $state ) {
299 continue;
300 }
301 $out[] = desktop_mode_games_shape_challenge( $row );
302 }
303 // Newest change first for the inbox view.
304 $out = array_reverse( $out );
305 return rest_ensure_response( array( 'challenges' => $out ) );
306 }
307
308 /**
309 * POST /games/challenges
310 *
311 * @since 0.9.6
312 */
313 function desktop_mode_games_rest_create_challenge( WP_REST_Request $req ) {
314 $challenger_id = get_current_user_id();
315 $recipient_id = (int) $req->get_param( 'recipient_id' );
316 $game = sanitize_key( (string) $req->get_param( 'game' ) );
317
318 if ( ! desktop_mode_games_is_registered( $game ) ) {
319 return new WP_Error(
320 'desktop_mode_unknown_game',
321 __( 'Unknown game.', 'desktop-mode' ),
322 array( 'status' => 404 )
323 );
324 }
325
326 /**
327 * Filters whether a user may challenge another user. Return
328 * `false` (or a `WP_Error`) to block — e.g. respecting a
329 * do-not-disturb setting or a per-role policy.
330 *
331 * @since 0.9.6
332 *
333 * @param bool|WP_Error $allowed Default `true`.
334 * @param int $challenger_id Sender.
335 * @param int $recipient_id Receiver.
336 * @param string $game Game id.
337 */
338 $allowed = apply_filters( 'desktop_mode_games_can_challenge', true, $challenger_id, $recipient_id, $game );
339 if ( is_wp_error( $allowed ) ) {
340 return $allowed;
341 }
342 if ( true !== $allowed ) {
343 return new WP_Error(
344 'desktop_mode_challenge_blocked',
345 __( 'You cannot challenge this user.', 'desktop-mode' ),
346 array( 'status' => 403 )
347 );
348 }
349
350 $id = desktop_mode_games_create_challenge(
351 $game,
352 $challenger_id,
353 $recipient_id,
354 (int) $req->get_param( 'score' ),
355 (array) $req->get_param( 'meta' )
356 );
357 if ( is_wp_error( $id ) ) {
358 return $id;
359 }
360 $row = desktop_mode_games_get_challenge( $id );
361 return rest_ensure_response( array( 'challenge' => desktop_mode_games_shape_challenge( $row ) ) );
362 }
363
364 /**
365 * Load a challenge and verify the current user is its recipient.
366 *
367 * @since 0.9.6
368 * @internal
369 *
370 * @param WP_REST_Request $req Request.
371 * @return array|WP_Error The raw challenge row.
372 */
373 function desktop_mode_games_rest_resolve_recipient_challenge( WP_REST_Request $req ) {
374 $row = desktop_mode_games_get_challenge( (int) $req->get_param( 'id' ) );
375 if ( ! $row ) {
376 return new WP_Error(
377 'desktop_mode_challenge_not_found',
378 __( 'Challenge not found.', 'desktop-mode' ),
379 array( 'status' => 404 )
380 );
381 }
382 if ( (int) $row['recipient_id'] !== get_current_user_id() ) {
383 return new WP_Error(
384 'desktop_mode_challenge_forbidden',
385 __( 'Only the challenged user can act on this challenge.', 'desktop-mode' ),
386 array( 'status' => 403 )
387 );
388 }
389 return $row;
390 }
391
392 /**
393 * POST /games/challenges/{id}/accept
394 *
395 * @since 0.9.6
396 */
397 function desktop_mode_games_rest_accept_challenge( WP_REST_Request $req ) {
398 $row = desktop_mode_games_rest_resolve_recipient_challenge( $req );
399 if ( is_wp_error( $row ) ) {
400 return $row;
401 }
402 $result = desktop_mode_games_set_challenge_state( (int) $row['id'], 'accepted' );
403 if ( is_wp_error( $result ) ) {
404 return $result;
405 }
406 $updated = desktop_mode_games_get_challenge( (int) $row['id'] );
407 return rest_ensure_response( array( 'challenge' => desktop_mode_games_shape_challenge( $updated ) ) );
408 }
409
410 /**
411 * POST /games/challenges/{id}/decline
412 *
413 * @since 0.9.6
414 */
415 function desktop_mode_games_rest_decline_challenge( WP_REST_Request $req ) {
416 $row = desktop_mode_games_rest_resolve_recipient_challenge( $req );
417 if ( is_wp_error( $row ) ) {
418 return $row;
419 }
420 $result = desktop_mode_games_set_challenge_state( (int) $row['id'], 'declined' );
421 if ( is_wp_error( $result ) ) {
422 return $result;
423 }
424 $updated = desktop_mode_games_get_challenge( (int) $row['id'] );
425 return rest_ensure_response( array( 'challenge' => desktop_mode_games_shape_challenge( $updated ) ) );
426 }
427
428 /**
429 * POST /games/challenges/{id}/complete
430 *
431 * @since 0.9.6
432 */
433 function desktop_mode_games_rest_complete_challenge( WP_REST_Request $req ) {
434 $row = desktop_mode_games_rest_resolve_recipient_challenge( $req );
435 if ( is_wp_error( $row ) ) {
436 return $row;
437 }
438 $updated = desktop_mode_games_complete_challenge(
439 (int) $row['id'],
440 (int) $req->get_param( 'score' ),
441 (array) $req->get_param( 'meta' )
442 );
443 if ( is_wp_error( $updated ) ) {
444 return $updated;
445 }
446 return rest_ensure_response( array( 'challenge' => desktop_mode_games_shape_challenge( $updated ) ) );
447 }
448
449 /**
450 * GET /games/users/search?q=<>&exclude=<csv> — autocomplete for the
451 * opponent picker. Thin sibling of the folder-share picker, gated on
452 * `read` instead of `edit_posts` so subscribers can be challenged.
453 *
454 * @since 0.9.6
455 */
456 function desktop_mode_games_rest_search_users( WP_REST_Request $req ) {
457 $q = trim( (string) $req->get_param( 'q' ) );
458 $exclude = array_filter( array_map( 'intval', explode( ',', (string) $req->get_param( 'exclude' ) ) ) );
459
460 // Always exclude the current viewer — self-challenges are
461 // rejected at create time anyway.
462 $exclude[] = (int) get_current_user_id();
463 $exclude = array_values( array_unique( array_filter( $exclude ) ) );
464
465 $args = array(
466 'number' => 20,
467 'orderby' => 'display_name',
468 'order' => 'ASC',
469 'exclude' => $exclude,
470 'fields' => 'all',
471 );
472 if ( '' !== $q ) {
473 $args['search'] = '*' . $q . '*';
474 $args['search_columns'] = array( 'user_login', 'user_email', 'display_name', 'user_nicename' );
475 }
476
477 /**
478 * Filter the WP_User_Query args used by the opponent picker.
479 *
480 * @since 0.9.6
481 *
482 * @param array $args Default args.
483 * @param array $req Request params (`q`, `exclude`).
484 */
485 $args = (array) apply_filters( 'desktop_mode_games_user_query_args', $args, $req->get_params() );
486
487 $query = new WP_User_Query( $args );
488 $users = $query->get_results();
489 $out = array();
490 foreach ( (array) $users as $user ) {
491 if ( ! user_can( $user, 'read' ) ) {
492 continue;
493 }
494 $out[] = array(
495 'id' => (int) $user->ID,
496 'name' => (string) $user->display_name,
497 'slug' => (string) $user->user_nicename,
498 'avatarUrl' => get_avatar_url( $user->ID, array( 'size' => 48 ) ),
499 );
500 }
501 return rest_ensure_response( array( 'users' => $out ) );
502 }
503