PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / includes / seen-intros.php

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

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