| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation Preferences — the settings window, as an OpenStation app. |
| 4 |
* |
| 5 |
* THE Preferences window: the App Framework rebuild replaced the |
| 6 |
* legacy JS-registered native window and its lazy panel bundle, and |
| 7 |
* claims its id — `desktop-mode-os-settings` is a frozen identifier |
| 8 |
* (see AGENTS.md), so saved sessions, the System tile's flyout row, |
| 9 |
* `wp.os.openOsSettings()` and the default-window marker keep working |
| 10 |
* unchanged. The window is this file; the body is `os-settings.os.ts`, |
| 11 |
* a client view painting every page from the shell's settings store |
| 12 |
* through the public `wp.os` API. |
| 13 |
* |
| 14 |
* The settings are NOT this app's state — they are per-user meta the |
| 15 |
* shell applies before its first paint (`includes/os-settings.php`, |
| 16 |
* `src/settings/`). This app's state is the page; its server surface |
| 17 |
* is the handful of writes that are site truth rather than a |
| 18 |
* preference, plus `data()` for the facts that can change mid-session. |
| 19 |
* |
| 20 |
* (Header kept short on purpose: Plugin Check's direct-access scan |
| 21 |
* reads only the first 50 raw lines, and the guard below must land |
| 22 |
* inside that window.) |
| 23 |
* |
| 24 |
* @package OpenStation |
| 25 |
*/ |
| 26 |
|
| 27 |
namespace OpenStation\Apps\OsSettings; |
| 28 |
|
| 29 |
use OpenStation\App; |
| 30 |
use OpenStation\App\Os; |
| 31 |
use OpenStation\App\State; |
| 32 |
|
| 33 |
// Direct access, unless a standalone host is booting on bare PHP. |
| 34 |
if ( ! defined( 'ABSPATH' ) ) { |
| 35 |
defined( 'OPENSTATION_STANDALONE' ) || exit; |
| 36 |
} |
| 37 |
|
| 38 |
/** The window id — a frozen identifier, see the file header. */ |
| 39 |
const ID = 'desktop-mode-os-settings'; |
| 40 |
|
| 41 |
/** |
| 42 |
* The gear the Preferences window wears — the same eight-toothed |
| 43 |
* annulus the System tile's flyout draws (`src/ui/gear-icon.ts`). |
| 44 |
* Hand-drawn rather than `dashicons-admin-generic`, which is also the |
| 45 |
* fallback every registry hands a plugin that registered without art: |
| 46 |
* the one window that should be findable at a glance would have |
| 47 |
* looked exactly like the tiles nobody bothered to draw. Drawn in |
| 48 |
* `currentColor`, like every other piece of shell art. |
| 49 |
* |
| 50 |
* @return string SVG markup. |
| 51 |
*/ |
| 52 |
function gear_svg() { |
| 53 |
$teeth = ''; |
| 54 |
for ( $i = 0; $i < 8; $i++ ) { |
| 55 |
$teeth .= sprintf( |
| 56 |
'<rect x="28" y="5" width="8" height="12" rx="2" fill="currentColor" transform="rotate(%d 32 32)"/>', |
| 57 |
45 * $i |
| 58 |
); |
| 59 |
} |
| 60 |
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">' . $teeth |
| 61 |
. '<circle cx="32" cy="32" r="15.5" fill="none" stroke="currentColor" stroke-width="9"/></svg>'; |
| 62 |
} |
| 63 |
|
| 64 |
/** |
| 65 |
* Refuse an action that changes SITE truth to anyone who may not. |
| 66 |
* The app itself is open to every shell user; these actions are not. |
| 67 |
* |
| 68 |
* @param Os $os Host handle. |
| 69 |
* @return void |
| 70 |
* @throws \RuntimeException When the acting user lacks `manage_options`. |
| 71 |
*/ |
| 72 |
function require_admin( Os $os ) { |
| 73 |
if ( ! $os->can( 'manage_options' ) ) { |
| 74 |
throw new \RuntimeException( esc_html__( 'You are not allowed to change site-wide options.', 'desktop-mode' ) ); |
| 75 |
} |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* The server facts the client view paints from — the ones that can |
| 80 |
* change while the window is open. The `focus` lifecycle action |
| 81 |
* recomputes them whenever the window regains focus, which is how the |
| 82 |
* AI toggle un-gates after the user connects a provider elsewhere. |
| 83 |
* |
| 84 |
* @param State $state Unused — nothing here depends on the page. |
| 85 |
* @param Os $os Host handle. |
| 86 |
* @return array<string,mixed> |
| 87 |
*/ |
| 88 |
function data( State $state, Os $os ) { |
| 89 |
$admin = $os->can( 'manage_options' ); |
| 90 |
return array( |
| 91 |
'isAdmin' => $admin, |
| 92 |
'canUpload' => $os->can( 'upload_files' ), |
| 93 |
'canManageDesktopThemes' => function_exists( 'openstation_desktop_theme_upload_capability' ) |
| 94 |
&& $os->can( openstation_desktop_theme_upload_capability() ), |
| 95 |
'extendedOptions' => $admin && function_exists( 'openstation_get_extended_options' ) |
| 96 |
? openstation_get_extended_options() |
| 97 |
: null, |
| 98 |
'commentsAi' => $admin && function_exists( 'openstation_comments_ai_is_enabled' ) |
| 99 |
? array( |
| 100 |
'enabled' => openstation_comments_ai_is_enabled(), |
| 101 |
'providerConfigured' => openstation_comments_ai_provider_configured(), |
| 102 |
) |
| 103 |
: null, |
| 104 |
'aiAssistant' => function_exists( 'openstation_ai_assistant_config' ) |
| 105 |
? openstation_ai_assistant_config( $os->auth->user_id() ) |
| 106 |
: null, |
| 107 |
); |
| 108 |
} |
| 109 |
|
| 110 |
/** |
| 111 |
* The comments-AI toggle: a site option, the same write the |
| 112 |
* `POST desktop-mode/v1/comments/ai-settings` route makes. |
| 113 |
* |
| 114 |
* @param State $state Unused. |
| 115 |
* @param Os $os Host handle. |
| 116 |
* @param array<string,mixed> $args `enabled` (bool). |
| 117 |
* @return void |
| 118 |
*/ |
| 119 |
function comments_ai_action( State $state, Os $os, array $args ) { |
| 120 |
require_admin( $os ); |
| 121 |
if ( ! defined( 'OPENSTATION_COMMENTS_AI_OPTION' ) ) { |
| 122 |
return; |
| 123 |
} |
| 124 |
$enabled = ! empty( $args['enabled'] ); |
| 125 |
update_option( OPENSTATION_COMMENTS_AI_OPTION, $enabled, false ); |
| 126 |
/** This action is documented in apps/comments/parts/ai-moderation.php */ |
| 127 |
do_action( 'openstation_comments_ai_toggled', $enabled ); |
| 128 |
} |
| 129 |
|
| 130 |
/** |
| 131 |
* Extended Options: merged over the stored set, then a menu refresh — |
| 132 |
* every option gates a server-side registration (`games` decides |
| 133 |
* whether the games module loads at all), and the shell only learns |
| 134 |
* what the server registers from a fresh payload. |
| 135 |
* |
| 136 |
* @param State $state Unused. |
| 137 |
* @param Os $os Host handle. |
| 138 |
* @param array<string,mixed> $args `options` (array of bools). |
| 139 |
* @return void |
| 140 |
*/ |
| 141 |
function extended_action( State $state, Os $os, array $args ) { |
| 142 |
require_admin( $os ); |
| 143 |
if ( function_exists( 'openstation_save_extended_options' ) ) { |
| 144 |
openstation_save_extended_options( isset( $args['options'] ) && is_array( $args['options'] ) ? $args['options'] : array() ); |
| 145 |
} |
| 146 |
$os->refresh_menu(); |
| 147 |
} |
| 148 |
|
| 149 |
/** |
| 150 |
* "Delete folder sharing data" — drops every shares table, the same |
| 151 |
* destructive cleanup the files REST route performs. |
| 152 |
* |
| 153 |
* @param State $state Unused. |
| 154 |
* @param Os $os Host handle. |
| 155 |
* @return void |
| 156 |
*/ |
| 157 |
function purge_shares_action( State $state, Os $os ) { |
| 158 |
require_admin( $os ); |
| 159 |
if ( ! function_exists( 'openstation_files_rest_purge_sharing_tables' ) ) { |
| 160 |
$os->toast( __( 'Files REST endpoint is not available.', 'desktop-mode' ) ); |
| 161 |
return; |
| 162 |
} |
| 163 |
$response = openstation_files_rest_purge_sharing_tables(); |
| 164 |
$dropped = $response instanceof \WP_REST_Response ? (array) $response->get_data()['dropped'] : array(); |
| 165 |
$os->toast( |
| 166 |
sprintf( |
| 167 |
/* translators: %d: number of tables dropped. */ |
| 168 |
__( 'Folder sharing data deleted. (%d tables)', 'desktop-mode' ), |
| 169 |
count( $dropped ) |
| 170 |
) |
| 171 |
); |
| 172 |
} |
| 173 |
|
| 174 |
return App::define( ID ) |
| 175 |
// Not translated: the product's own name, as the legacy window had it. |
| 176 |
->title( 'OpenStation Preferences' ) |
| 177 |
->icon( gear_svg() ) |
| 178 |
->size( 820, 720 ) |
| 179 |
->min_size( 560, 480 ) |
| 180 |
// No launcher of its own: the System tile's flyout row opens it |
| 181 |
// and answers for it on the rail (`NavItem.answersFor`), exactly |
| 182 |
// as before. `wp.os.openOsSettings()` is the portable opener. |
| 183 |
->placement( 'none' ) |
| 184 |
->capabilities( 'read' ) |
| 185 |
// `data()` is a handful of capability checks and options, so it |
| 186 |
// ships with the window and the pages paint the moment the window |
| 187 |
// opens — as the legacy panel did — instead of behind a spinner for |
| 188 |
// the length of the `mount` round trip. |
| 189 |
->prefetch() |
| 190 |
// The page is the whole declared state; the settings live in the |
| 191 |
// shell's store and reach the view through `wp.os.getOsSettings()`. |
| 192 |
->state( array( 'tab' => 'appearance' ) ) |
| 193 |
// `wp.os.openOsSettings( { tabId } )` lands on the page it named. |
| 194 |
->mount( |
| 195 |
static function ( State $state, Os $os ) { |
| 196 |
$tab = sanitize_key( (string) $os->param( 'tab', '' ) ); |
| 197 |
if ( '' !== $tab ) { |
| 198 |
$state->set( 'tab', $tab ); |
| 199 |
} |
| 200 |
} |
| 201 |
) |
| 202 |
->action( 'extended', __NAMESPACE__ . '\extended_action' ) |
| 203 |
->action( 'comments-ai', __NAMESPACE__ . '\comments_ai_action' ) |
| 204 |
->action( |
| 205 |
'reset-intros', |
| 206 |
static function ( State $state, Os $os ) { |
| 207 |
if ( function_exists( 'openstation_clear_seen_intros' ) ) { |
| 208 |
openstation_clear_seen_intros( $os->auth->user_id() ); |
| 209 |
} |
| 210 |
} |
| 211 |
) |
| 212 |
->action( 'purge-shares', __NAMESPACE__ . '\purge_shares_action' ) |
| 213 |
// Regaining focus re-probes the server facts: nothing to run, the |
| 214 |
// recomputed `data()` is the point. |
| 215 |
->action( 'focus', static function () {} ) |
| 216 |
->data( __NAMESPACE__ . '\data' ) |
| 217 |
// The static facts, shipped once with the window config. |
| 218 |
->config( |
| 219 |
array( |
| 220 |
'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ), |
| 221 |
'desktopThemesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/desktop-themes' ) ), |
| 222 |
'aboutFeedUrl' => esc_url_raw( |
| 223 |
add_query_arg( |
| 224 |
array( |
| 225 |
'action' => 'openstation_about_feed', |
| 226 |
'nonce' => wp_create_nonce( 'openstation_about_feed' ), |
| 227 |
), |
| 228 |
admin_url( 'admin-ajax.php' ) |
| 229 |
) |
| 230 |
), |
| 231 |
'pluginUrl' => esc_url_raw( untrailingslashit( OPENSTATION_URL ) ), |
| 232 |
'pluginVersion' => OPENSTATION_VERSION, |
| 233 |
) |
| 234 |
); |
| 235 |
|