PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.8
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 / seen-intros.php

seen-intros.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.8, at includes/seen-intros.php

259 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — "Seen intros" registry.
4 *
5 * Tracks which one-time introduction dialogs the current user has
6 * already dismissed, so the shell can show a "what's new in this
7 * native app" dialog the first time a ported native window opens
8 * and never bother the user again afterwards.
9 *
10 * Today the surface is the native Posts window. The same key is
11 * intentionally generic — any future ported native app (Pages,
12 * Comments, Users, Plugins, …) registers its own slug and reuses
13 * this storage. OS Settings → Features exposes a "Reset what's-new
14 * dialogs" button that clears the whole list so the user can see
15 * every intro again from scratch.
16 *
17 * Storage shape:
18 * user meta `desktop_mode_seen_intros` → array<string> of slugs.
19 * `[ 'posts' ]`, `[ 'posts', 'pages' ]`, etc. Slug values pass
20 * through `sanitize_key()` and the list is capped at 64 entries
21 * so a runaway client cannot bloat user-meta indefinitely.
22 *
23 * @package WPDesktopMode
24 */
25
26 defined( 'ABSPATH' ) || exit;
27
28 /** User meta key — see file header for shape. */
29 const DESKTOP_MODE_SEEN_INTROS_META_KEY = 'desktop_mode_seen_intros';
30
31 /** Hard cap so a malicious client cannot grow the list unbounded. */
32 const DESKTOP_MODE_SEEN_INTROS_MAX = 64;
33
34 /**
35 * Returns the list of intro slugs the user has dismissed.
36 *
37 * @param int $user_id User ID.
38 * @return string[] Sanitized list (may be empty).
39 */
40 function desktop_mode_get_seen_intros( $user_id ) {
41 $user_id = (int) $user_id;
42 if ( $user_id <= 0 ) {
43 return array();
44 }
45
46 $raw = get_user_meta( $user_id, DESKTOP_MODE_SEEN_INTROS_META_KEY, true );
47 if ( ! is_array( $raw ) ) {
48 return array();
49 }
50
51 return desktop_mode_sanitize_seen_intros( $raw );
52 }
53
54 /**
55 * Whether the user has already dismissed the given intro.
56 *
57 * @param int $user_id User ID.
58 * @param string $slug Intro slug (e.g. `'posts'`).
59 * @return bool
60 */
61 function desktop_mode_has_seen_intro( $user_id, $slug ) {
62 $slug = sanitize_key( (string) $slug );
63 if ( '' === $slug ) {
64 return false;
65 }
66 return in_array( $slug, desktop_mode_get_seen_intros( $user_id ), true );
67 }
68
69 /**
70 * Adds a slug to the user's seen-intros list.
71 *
72 * Idempotent — re-marking an already-seen intro is a no-op that
73 * still returns true.
74 *
75 * @param int $user_id User ID.
76 * @param string $slug Intro slug.
77 * @return bool True on successful write (or no-op), false otherwise.
78 */
79 function desktop_mode_mark_intro_seen( $user_id, $slug ) {
80 $user_id = (int) $user_id;
81 $slug = sanitize_key( (string) $slug );
82 if ( $user_id <= 0 || '' === $slug ) {
83 return false;
84 }
85
86 $current = desktop_mode_get_seen_intros( $user_id );
87 if ( in_array( $slug, $current, true ) ) {
88 return true;
89 }
90
91 $current[] = $slug;
92 $current = array_slice( $current, 0, DESKTOP_MODE_SEEN_INTROS_MAX );
93
94 return false !== update_user_meta(
95 $user_id,
96 DESKTOP_MODE_SEEN_INTROS_META_KEY,
97 $current
98 );
99 }
100
101 /**
102 * Wipes every seen-intro entry for the user. Used by the OS
103 * Settings → Features "Reset what's-new dialogs" button.
104 *
105 * @param int $user_id User ID.
106 * @return bool True on success.
107 */
108 function desktop_mode_clear_seen_intros( $user_id ) {
109 $user_id = (int) $user_id;
110 if ( $user_id <= 0 ) {
111 return false;
112 }
113 return (bool) delete_user_meta( $user_id, DESKTOP_MODE_SEEN_INTROS_META_KEY );
114 }
115
116 /**
117 * Coerces a raw payload to a clean list of slugs.
118 *
119 * @param mixed $raw Raw value.
120 * @return string[]
121 */
122 function desktop_mode_sanitize_seen_intros( $raw ) {
123 if ( ! is_array( $raw ) ) {
124 return array();
125 }
126 $out = array();
127 foreach ( $raw as $entry ) {
128 if ( ! is_string( $entry ) ) {
129 continue;
130 }
131 $slug = sanitize_key( $entry );
132 if ( '' === $slug ) {
133 continue;
134 }
135 $out[] = $slug;
136 }
137 return array_slice( array_values( array_unique( $out ) ), 0, DESKTOP_MODE_SEEN_INTROS_MAX );
138 }
139
140 /**
141 * Registers REST routes for the seen-intros surface.
142 *
143 * Routes:
144 * POST /desktop-mode/v1/intros/seen body: { slug: string }
145 * DELETE /desktop-mode/v1/intros no body — clears the list
146 *
147 * Both return the post-mutation list so the client can refresh its
148 * local snapshot without a follow-up GET.
149 */
150 function desktop_mode_register_seen_intros_routes() {
151 register_rest_route(
152 'desktop-mode/v1',
153 '/intros/seen',
154 array(
155 'methods' => WP_REST_Server::CREATABLE,
156 'callback' => 'desktop_mode_rest_mark_intro_seen',
157 'permission_callback' => 'desktop_mode_rest_seen_intros_permission',
158 'args' => array(
159 'slug' => array(
160 'required' => true,
161 'type' => 'string',
162 ),
163 ),
164 )
165 );
166
167 register_rest_route(
168 'desktop-mode/v1',
169 '/intros',
170 array(
171 'methods' => WP_REST_Server::DELETABLE,
172 'callback' => 'desktop_mode_rest_clear_seen_intros',
173 'permission_callback' => 'desktop_mode_rest_seen_intros_permission',
174 )
175 );
176 }
177 add_action( 'rest_api_init', 'desktop_mode_register_seen_intros_routes' );
178
179 /**
180 * Permission gate for the seen-intros routes.
181 *
182 * In-shell intros (slug `posts`, `pages`, …) are only ever shown to a
183 * user who has already entered Desktop Mode, so they keep the strict
184 * {@see desktop_mode_rest_require_enabled()} gate — `read` alone is
185 * insufficient (every role, Subscriber included, carries `read`).
186 *
187 * The one exception is the first-run welcome dialog
188 * ({@see DESKTOP_MODE_WELCOME_INTRO_SLUG}): it renders in the *classic*
189 * admin precisely when Desktop Mode is NOT enabled, which is the only
190 * state it ever appears in. Gating its dismissal behind
191 * `desktop_mode_rest_require_enabled()` would make the dismissal POST
192 * return 403 every time, so the slug could never be recorded as seen and
193 * the dialog re-rendered on every classic-admin page load. We therefore
194 * let that single slug through for any logged-in `read`-capable account
195 * (the exact audience the dialog is shown to); writing one's own
196 * dismissal flag carries no privileged surface. The DELETE /intros route
197 * ("Reset what's-new dialogs") carries no slug and keeps the strict gate.
198 *
199 * @param WP_REST_Request $request The REST request.
200 * @return true|WP_Error
201 */
202 function desktop_mode_rest_seen_intros_permission( WP_REST_Request $request ) {
203 $slug = sanitize_key( (string) $request->get_param( 'slug' ) );
204 if ( defined( 'DESKTOP_MODE_WELCOME_INTRO_SLUG' ) && DESKTOP_MODE_WELCOME_INTRO_SLUG === $slug ) {
205 if ( ! is_user_logged_in() ) {
206 return new WP_Error(
207 'rest_forbidden',
208 __( 'Authentication required.', 'desktop-mode' ),
209 array( 'status' => 401 )
210 );
211 }
212 if ( ! current_user_can( 'read' ) ) {
213 return new WP_Error(
214 'rest_forbidden',
215 __( 'You are not allowed to do that.', 'desktop-mode' ),
216 array( 'status' => 403 )
217 );
218 }
219 return true;
220 }
221
222 return desktop_mode_rest_require_enabled();
223 }
224
225 /**
226 * REST handler for `POST /desktop-mode/v1/intros/seen`.
227 *
228 * @param WP_REST_Request $request REST request.
229 * @return WP_REST_Response|WP_Error
230 */
231 function desktop_mode_rest_mark_intro_seen( WP_REST_Request $request ) {
232 $user_id = get_current_user_id();
233 $slug = sanitize_key( (string) $request->get_param( 'slug' ) );
234 if ( '' === $slug ) {
235 return new WP_Error(
236 'desktop_mode_invalid_intro_slug',
237 __( 'The `slug` parameter must be a non-empty string.', 'desktop-mode' ),
238 array( 'status' => 400 )
239 );
240 }
241 desktop_mode_mark_intro_seen( $user_id, $slug );
242 return rest_ensure_response(
243 array( 'seenIntros' => desktop_mode_get_seen_intros( $user_id ) )
244 );
245 }
246
247 /**
248 * REST handler for `DELETE /desktop-mode/v1/intros`.
249 *
250 * @return WP_REST_Response
251 */
252 function desktop_mode_rest_clear_seen_intros() {
253 $user_id = get_current_user_id();
254 desktop_mode_clear_seen_intros( $user_id );
255 return rest_ensure_response(
256 array( 'seenIntros' => desktop_mode_get_seen_intros( $user_id ) )
257 );
258 }
259