| @@ -1,75 +1,79 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — "Seen intros" registry. | |
| 3 | + * OpenStation — "Seen intros" registry. | |
| 4 | 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. | |
| 5 | + * Tracks which one-time announcements the current user has already | |
| 6 | + * dismissed, so each is shown once and never bothers them again. | |
| 9 | 7 | * |
| 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. | |
| 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 | 16 | * |
| 17 | 17 | * Storage shape: |
| 18 | 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. | |
| 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. | |
| 22 | 23 | * |
| 23 | - * @package WPDesktopMode | |
| 24 | - * @since 0.8.0 | |
| 24 | + * @package OpenStation | |
| 25 | 25 | */ |
| 26 | 26 | |
| 27 | 27 | defined( 'ABSPATH' ) || exit; |
| 28 | 28 | |
| 29 | -/** User meta key — see file header for shape. */ | |
| 30 | -const DESKTOP_MODE_SEEN_INTROS_META_KEY = 'desktop_mode_seen_intros'; | |
| 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'; | |
| 31 | 39 | |
| 32 | 40 | /** Hard cap so a malicious client cannot grow the list unbounded. */ |
| 33 | -const DESKTOP_MODE_SEEN_INTROS_MAX = 64; | |
| 41 | +const OPENSTATION_SEEN_INTROS_MAX = 64; | |
| 34 | 42 | |
| 35 | 43 | /** |
| 36 | 44 | * Returns the list of intro slugs the user has dismissed. |
| 37 | 45 | * |
| 38 | - * @since 0.8.0 | |
| 39 | - * | |
| 40 | 46 | * @param int $user_id User ID. |
| 41 | 47 | * @return string[] Sanitized list (may be empty). |
| 42 | 48 | */ |
| 43 | -function desktop_mode_get_seen_intros( $user_id ) { | |
| 49 | +function openstation_get_seen_intros( $user_id ) { | |
| 44 | 50 | $user_id = (int) $user_id; |
| 45 | 51 | if ( $user_id <= 0 ) { |
| 46 | 52 | return array(); |
| 47 | 53 | } |
| 48 | 54 | |
| 49 | - $raw = get_user_meta( $user_id, DESKTOP_MODE_SEEN_INTROS_META_KEY, true ); | |
| 55 | + $raw = get_user_meta( $user_id, OPENSTATION_SEEN_INTROS_META_KEY, true ); | |
| 50 | 56 | if ( ! is_array( $raw ) ) { |
| 51 | 57 | return array(); |
| 52 | 58 | } |
| 53 | 59 | |
| 54 | - return desktop_mode_sanitize_seen_intros( $raw ); | |
| 60 | + return openstation_sanitize_seen_intros( $raw ); | |
| 55 | 61 | } |
| 56 | 62 | |
| 57 | 63 | /** |
| 58 | 64 | * Whether the user has already dismissed the given intro. |
| 59 | 65 | * |
| 60 | - * @since 0.8.0 | |
| 61 | - * | |
| 62 | 66 | * @param int $user_id User ID. |
| 63 | 67 | * @param string $slug Intro slug (e.g. `'posts'`). |
| 64 | 68 | * @return bool |
| 65 | 69 | */ |
| 66 | -function desktop_mode_has_seen_intro( $user_id, $slug ) { | |
| 70 | +function openstation_has_seen_intro( $user_id, $slug ) { | |
| 67 | 71 | $slug = sanitize_key( (string) $slug ); |
| 68 | 72 | if ( '' === $slug ) { |
| 69 | 73 | return false; |
| 70 | 74 | } |
| 71 | - return in_array( $slug, desktop_mode_get_seen_intros( $user_id ), true ); | |
| 75 | + return in_array( $slug, openstation_get_seen_intros( $user_id ), true ); | |
| 72 | 76 | } |
| 73 | 77 | |
| 74 | 78 | /** |
| 75 | 79 | * Adds a slug to the user's seen-intros list. |
| @@ -76,15 +80,13 @@ | ||
| 76 | 80 | * |
| 77 | 81 | * Idempotent — re-marking an already-seen intro is a no-op that |
| 78 | 82 | * still returns true. |
| 79 | 83 | * |
| 80 | - * @since 0.8.0 | |
| 81 | - * | |
| 82 | 84 | * @param int $user_id User ID. |
| 83 | 85 | * @param string $slug Intro slug. |
| 84 | 86 | * @return bool True on successful write (or no-op), false otherwise. |
| 85 | 87 | */ |
| 86 | -function desktop_mode_mark_intro_seen( $user_id, $slug ) { | |
| 88 | +function openstation_mark_intro_seen( $user_id, $slug ) { | |
| 87 | 89 | $user_id = (int) $user_id; |
| 88 | 90 | $slug = sanitize_key( (string) $slug ); |
| 89 | 91 | if ( $user_id <= 0 || '' === $slug ) { |
| 90 | 92 | return false; |
| @@ -89,19 +91,19 @@ | ||
| 89 | 91 | if ( $user_id <= 0 || '' === $slug ) { |
| 90 | 92 | return false; |
| 91 | 93 | } |
| 92 | 94 | |
| 93 | - $current = desktop_mode_get_seen_intros( $user_id ); | |
| 95 | + $current = openstation_get_seen_intros( $user_id ); | |
| 94 | 96 | if ( in_array( $slug, $current, true ) ) { |
| 95 | 97 | return true; |
| 96 | 98 | } |
| 97 | 99 | |
| 98 | 100 | $current[] = $slug; |
| 99 | - $current = array_slice( $current, 0, DESKTOP_MODE_SEEN_INTROS_MAX ); | |
| 101 | + $current = array_slice( $current, 0, OPENSTATION_SEEN_INTROS_MAX ); | |
| 100 | 102 | |
| 101 | 103 | return false !== update_user_meta( |
| 102 | 104 | $user_id, |
| 103 | - DESKTOP_MODE_SEEN_INTROS_META_KEY, | |
| 105 | + OPENSTATION_SEEN_INTROS_META_KEY, | |
| 104 | 106 | $current |
| 105 | 107 | ); |
| 106 | 108 | } |
| 107 | 109 | |
| @@ -108,30 +110,26 @@ | ||
| 108 | 110 | /** |
| 109 | 111 | * Wipes every seen-intro entry for the user. Used by the OS |
| 110 | 112 | * Settings → Features "Reset what's-new dialogs" button. |
| 111 | 113 | * |
| 112 | - * @since 0.8.0 | |
| 113 | - * | |
| 114 | 114 | * @param int $user_id User ID. |
| 115 | 115 | * @return bool True on success. |
| 116 | 116 | */ |
| 117 | -function desktop_mode_clear_seen_intros( $user_id ) { | |
| 117 | +function openstation_clear_seen_intros( $user_id ) { | |
| 118 | 118 | $user_id = (int) $user_id; |
| 119 | 119 | if ( $user_id <= 0 ) { |
| 120 | 120 | return false; |
| 121 | 121 | } |
| 122 | - return (bool) delete_user_meta( $user_id, DESKTOP_MODE_SEEN_INTROS_META_KEY ); | |
| 122 | + return (bool) delete_user_meta( $user_id, OPENSTATION_SEEN_INTROS_META_KEY ); | |
| 123 | 123 | } |
| 124 | 124 | |
| 125 | 125 | /** |
| 126 | 126 | * Coerces a raw payload to a clean list of slugs. |
| 127 | 127 | * |
| 128 | - * @since 0.8.0 | |
| 129 | - * | |
| 130 | 128 | * @param mixed $raw Raw value. |
| 131 | 129 | * @return string[] |
| 132 | 130 | */ |
| 133 | -function desktop_mode_sanitize_seen_intros( $raw ) { | |
| 131 | +function openstation_sanitize_seen_intros( $raw ) { | |
| 134 | 132 | if ( ! is_array( $raw ) ) { |
| 135 | 133 | return array(); |
| 136 | 134 | } |
| 137 | 135 | $out = array(); |
| @@ -144,9 +142,9 @@ | ||
| 144 | 142 | continue; |
| 145 | 143 | } |
| 146 | 144 | $out[] = $slug; |
| 147 | 145 | } |
| 148 | - return array_slice( array_values( array_unique( $out ) ), 0, DESKTOP_MODE_SEEN_INTROS_MAX ); | |
| 146 | + return array_slice( array_values( array_unique( $out ) ), 0, OPENSTATION_SEEN_INTROS_MAX ); | |
| 149 | 147 | } |
| 150 | 148 | |
| 151 | 149 | /** |
| 152 | 150 | * Registers REST routes for the seen-intros surface. |
| @@ -156,19 +154,17 @@ | ||
| 156 | 154 | * DELETE /desktop-mode/v1/intros no body — clears the list |
| 157 | 155 | * |
| 158 | 156 | * Both return the post-mutation list so the client can refresh its |
| 159 | 157 | * local snapshot without a follow-up GET. |
| 160 | - * | |
| 161 | - * @since 0.8.0 | |
| 162 | 158 | */ |
| 163 | -function desktop_mode_register_seen_intros_routes() { | |
| 159 | +function openstation_register_seen_intros_routes() { | |
| 164 | 160 | register_rest_route( |
| 165 | 161 | 'desktop-mode/v1', |
| 166 | 162 | '/intros/seen', |
| 167 | 163 | array( |
| 168 | 164 | 'methods' => WP_REST_Server::CREATABLE, |
| 169 | - 'callback' => 'desktop_mode_rest_mark_intro_seen', | |
| 170 | - 'permission_callback' => 'desktop_mode_rest_seen_intros_permission', | |
| 165 | + 'callback' => 'openstation_rest_mark_intro_seen', | |
| 166 | + 'permission_callback' => 'openstation_rest_seen_intros_permission', | |
| 171 | 167 | 'args' => array( |
| 172 | 168 | 'slug' => array( |
| 173 | 169 | 'required' => true, |
| 174 | 170 | 'type' => 'string', |
| @@ -181,48 +177,81 @@ | ||
| 181 | 177 | 'desktop-mode/v1', |
| 182 | 178 | '/intros', |
| 183 | 179 | array( |
| 184 | 180 | 'methods' => WP_REST_Server::DELETABLE, |
| 185 | - 'callback' => 'desktop_mode_rest_clear_seen_intros', | |
| 186 | - 'permission_callback' => 'desktop_mode_rest_seen_intros_permission', | |
| 181 | + 'callback' => 'openstation_rest_clear_seen_intros', | |
| 182 | + 'permission_callback' => 'openstation_rest_seen_intros_permission', | |
| 187 | 183 | ) |
| 188 | 184 | ); |
| 189 | 185 | } |
| 190 | -add_action( 'rest_api_init', 'desktop_mode_register_seen_intros_routes' ); | |
| 186 | +add_action( 'rest_api_init', 'openstation_register_seen_intros_routes' ); | |
| 191 | 187 | |
| 192 | 188 | /** |
| 193 | - * Permission gate — any logged-in user may manage their own seen- | |
| 194 | - * intros list. | |
| 189 | + * Permission gate for the seen-intros routes. | |
| 195 | 190 | * |
| 196 | - * @since 0.8.0 | |
| 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`). | |
| 197 | 196 | * |
| 198 | - * @return bool | |
| 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 | |
| 199 | 211 | */ |
| 200 | -function desktop_mode_rest_seen_intros_permission() { | |
| 201 | - return is_user_logged_in() && current_user_can( 'read' ); | |
| 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(); | |
| 202 | 233 | } |
| 203 | 234 | |
| 204 | 235 | /** |
| 205 | 236 | * REST handler for `POST /desktop-mode/v1/intros/seen`. |
| 206 | 237 | * |
| 207 | - * @since 0.8.0 | |
| 208 | - * | |
| 209 | 238 | * @param WP_REST_Request $request REST request. |
| 210 | 239 | * @return WP_REST_Response|WP_Error |
| 211 | 240 | */ |
| 212 | -function desktop_mode_rest_mark_intro_seen( WP_REST_Request $request ) { | |
| 241 | +function openstation_rest_mark_intro_seen( WP_REST_Request $request ) { | |
| 213 | 242 | $user_id = get_current_user_id(); |
| 214 | 243 | $slug = sanitize_key( (string) $request->get_param( 'slug' ) ); |
| 215 | 244 | if ( '' === $slug ) { |
| 216 | 245 | return new WP_Error( |
| 217 | - 'desktop_mode_invalid_intro_slug', | |
| 246 | + 'openstation_invalid_intro_slug', | |
| 218 | 247 | __( 'The `slug` parameter must be a non-empty string.', 'desktop-mode' ), |
| 219 | 248 | array( 'status' => 400 ) |
| 220 | 249 | ); |
| 221 | 250 | } |
| 222 | - desktop_mode_mark_intro_seen( $user_id, $slug ); | |
| 251 | + openstation_mark_intro_seen( $user_id, $slug ); | |
| 223 | 252 | return rest_ensure_response( |
| 224 | - array( 'seenIntros' => desktop_mode_get_seen_intros( $user_id ) ) | |
| 253 | + array( 'seenIntros' => openstation_get_seen_intros( $user_id ) ) | |
| 225 | 254 | ); |
| 226 | 255 | } |
| 227 | 256 | |
| 228 | 257 | /** |
| @@ -227,15 +256,13 @@ | ||
| 227 | 256 | |
| 228 | 257 | /** |
| 229 | 258 | * REST handler for `DELETE /desktop-mode/v1/intros`. |
| 230 | 259 | * |
| 231 | - * @since 0.8.0 | |
| 232 | - * | |
| 233 | 260 | * @return WP_REST_Response |
| 234 | 261 | */ |
| 235 | -function desktop_mode_rest_clear_seen_intros() { | |
| 262 | +function openstation_rest_clear_seen_intros() { | |
| 236 | 263 | $user_id = get_current_user_id(); |
| 237 | - desktop_mode_clear_seen_intros( $user_id ); | |
| 264 | + openstation_clear_seen_intros( $user_id ); | |
| 238 | 265 | return rest_ensure_response( |
| 239 | - array( 'seenIntros' => desktop_mode_get_seen_intros( $user_id ) ) | |
| 266 | + array( 'seenIntros' => openstation_get_seen_intros( $user_id ) ) | |
| 240 | 267 | ); |
| 241 | 268 | } |