PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.1
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 1.0.1, at includes/seen-intros.php

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