PluginProbe
Noted! / trunk
Noted! vtrunk
2.0.2 2.0.1 trunk 1.0 2.0
noted / includes / Settings.php

Settings.php in Noted! trunk, at includes/Settings.php

377 lines 13.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Noted;
6
7 use Wireframe\App as WireframeApp;
8 use Wireframe\Settings as WireframeSettings;
9
10 /**
11 * Settings boot + access helpers.
12 *
13 * Owns the wp-wireframe configuration, exposes typed setting readers
14 * for the rest of the plugin, and moves the registered admin menu page
15 * from its default top-level location into the Tools submenu.
16 *
17 * Access is split into two independent role gates:
18 * - {@see Settings::userCanView()} — read access (panel + REST GET)
19 * - {@see Settings::userCanEdit()} — write access (REST POST/PUT/DELETE)
20 */
21 final class Settings
22 {
23 public const PAGE_SLUG = 'noted';
24
25 /** Wireframe settings tab: plugin options (default tab). */
26 public const TAB_GENERAL = 'general';
27
28 /** Wireframe settings tab: Markdown syntax reference (`?tab=markdown`). */
29 public const TAB_MARKDOWN = 'markdown';
30
31 /**
32 * Standard WordPress role privilege ranking, most → least powerful.
33 * Used to gate access cleanly without remembering capability names.
34 */
35 private const ROLE_RANK = [
36 'administrator' => 5,
37 'editor' => 4,
38 'author' => 3,
39 'contributor' => 2,
40 'subscriber' => 1,
41 ];
42
43 public function __construct(private Plugin $plugin)
44 {
45 }
46
47 /**
48 * Register WordPress hooks.
49 */
50 public function register(): void
51 {
52 add_action('init', [$this, 'boot']);
53 // Priority 999 runs after wp-wireframe has registered its admin menu.
54 add_action('admin_menu', [$this, 'relocateMenuToTools'], 999);
55 }
56
57 /**
58 * Move the wp-wireframe-registered top-level menu under the Tools menu.
59 */
60 public function relocateMenuToTools(): void
61 {
62 global $menu, $submenu;
63
64 $slug = self::PAGE_SLUG;
65
66 if (is_array($menu)) {
67 foreach ($menu as $key => $item) {
68 if (isset($item[2]) && $item[2] === $slug) {
69 unset($menu[$key]);
70 break;
71 }
72 }
73 }
74
75 $submenu['tools.php'][] = [
76 'Noted!',
77 'manage_options',
78 'admin.php?page=' . $slug,
79 ];
80 }
81
82 /**
83 * Boot the wp-wireframe settings page.
84 */
85 public function boot(): void
86 {
87 if (! class_exists(WireframeApp::class)) {
88 return;
89 }
90
91 WireframeApp::boot([
92 'prefix' => self::PAGE_SLUG,
93 'page_title' => 'Noted!',
94 'option_key' => Plugin::OPTION_KEY,
95 'menu_icon' => 'dashicons-edit-page',
96 'config' => $this->config(),
97 ]);
98 }
99
100 /**
101 * True if the current user can view notes.
102 */
103 public function userCanView(): bool
104 {
105 return $this->userHasRoleAtLeast($this->get('min_role_view', 'administrator'));
106 }
107
108 /**
109 * True if the current user can add / edit / delete notes.
110 */
111 public function userCanEdit(): bool
112 {
113 return $this->userHasRoleAtLeast($this->get('min_role_edit', 'administrator'));
114 }
115
116 /**
117 * True if the current user has the named role or any role above it
118 * in {@see Settings::ROLE_RANK}. Custom roles get rank 0 and are only
119 * granted access when the gate is set to subscriber (the lowest).
120 */
121 public function userHasRoleAtLeast(string $minRole): bool
122 {
123 $user = wp_get_current_user();
124 if (! $user || ! $user->exists()) {
125 return false;
126 }
127
128 $required = self::ROLE_RANK[$minRole] ?? 0;
129 foreach ((array) $user->roles as $role) {
130 $rank = self::ROLE_RANK[$role] ?? 0;
131 if ($rank >= $required) {
132 return true;
133 }
134 }
135 return false;
136 }
137
138 /**
139 * Read a boolean setting with a default fallback.
140 */
141 public function bool(string $key, bool $default = false): bool
142 {
143 return (bool) $this->get($key, $default);
144 }
145
146 /**
147 * Read any setting value via the wp-wireframe facade.
148 */
149 public function get(string $key, mixed $default = null): mixed
150 {
151 if (! class_exists(WireframeSettings::class)) {
152 return $default;
153 }
154 return WireframeSettings::get(Plugin::OPTION_KEY, $key, $default);
155 }
156
157 /**
158 * Build a slug → label list of all editable WordPress roles.
159 *
160 * Uses translate_user_role() so localized role names appear correctly.
161 *
162 * @return array<string, string>
163 */
164 private function roleOptions(): array
165 {
166 $options = [];
167 $roles = function_exists('wp_roles') ? wp_roles()->roles : [];
168 foreach ($roles as $slug => $info) {
169 $name = isset($info['name']) ? (string) $info['name'] : $slug;
170 $options[$slug] = function_exists('translate_user_role')
171 ? translate_user_role($name)
172 : $name;
173 }
174 return $options;
175 }
176
177 /**
178 * Admin URL for the Markdown reference tab (same screen as Noted! settings).
179 */
180 public function markdownDocumentationUrl(): string
181 {
182 return add_query_arg(
183 [
184 'page' => self::PAGE_SLUG,
185 'tab' => self::TAB_MARKDOWN,
186 ],
187 admin_url('admin.php')
188 );
189 }
190
191 /**
192 * Accessible name for the Markdown help control (icon-only link).
193 */
194 public function markdownDocumentationLinkAriaLabel(): string
195 {
196 return sprintf(
197 /* translators: 1: Link purpose, 2: Screen reader hint that the link opens a new browser tab. */
198 __('%1$s %2$s', 'noted'),
199 __('View supported Markdown', 'noted'),
200 __('(opens in a new tab)', 'noted')
201 );
202 }
203
204 /**
205 * Hover/focus tooltip for the Markdown help icon (sighted users).
206 */
207 public function markdownDocumentationIconTooltip(): string
208 {
209 return __(
210 'Limited Markdown Support (titles, bold, italic, lists, & links)',
211 'noted'
212 );
213 }
214
215 /**
216 * Build the settings page schema.
217 *
218 * @return array<string, mixed>
219 */
220 private function config(): array
221 {
222 $roleOptions = $this->roleOptions();
223
224 $generalSections = [
225 [
226 'id' => 'features',
227 'title' => __('Features', 'noted'),
228 'fields' => [
229 [
230 'id' => 'enable_page_notes',
231 'type' => 'toggle',
232 'label' => __('Enable page-level notes', 'noted'),
233 'description' => __('Adds a Notes sidebar to the block editor (and a classic-editor meta box fallback).', 'noted'),
234 'default' => true,
235 ],
236 [
237 'id' => 'enable_block_notes',
238 'type' => 'toggle',
239 'label' => __('Enable block-level notes', 'noted'),
240 'description' => __('Adds a Note field to every block in the editor. Stored inline on the block.', 'noted'),
241 'default' => true,
242 ],
243 [
244 'id' => 'show_global_panel',
245 'type' => 'toggle',
246 'label' => __('Show the floating panel', 'noted'),
247 'description' => __('The admin-bar Noted! button that opens the side panel.', 'noted'),
248 'default' => true,
249 ],
250 [
251 'id' => 'enable_dashboard_widget',
252 'type' => 'toggle',
253 'label' => __('Show pinned notes on the dashboard', 'noted'),
254 'description' => __('Adds a widget to the WordPress dashboard that lists every pinned note.', 'noted'),
255 'default' => true,
256 ],
257 ],
258 ],
259 [
260 'id' => 'access',
261 'title' => __('Access', 'noted'),
262 'description' => __('View grants read-only access. Manage grants full add / edit / delete rights.', 'noted'),
263 'fields' => [
264 [
265 'id' => 'min_role_view',
266 'type' => 'select',
267 'label' => __('Minimum role to view notes', 'noted'),
268 'description' => __('Users at this role and above can see Noted! surfaces.', 'noted'),
269 'default' => 'administrator',
270 'columns' => 6,
271 'args' => ['options' => $roleOptions],
272 ],
273 [
274 'id' => 'min_role_edit',
275 'type' => 'select',
276 'label' => __('Minimum role to manage notes', 'noted'),
277 'description' => __('Users below this role can view notes but cannot add, edit, or delete them.', 'noted'),
278 'default' => 'administrator',
279 'columns' => 6,
280 'args' => ['options' => $roleOptions],
281 ],
282 ],
283 ],
284 [
285 'id' => 'maintenance',
286 'title' => __('Maintenance', 'noted'),
287 'description' => __('Back up, restore, or wipe Noted! data.', 'noted'),
288 'fields' => [
289 [
290 'id' => 'delete_on_uninstall',
291 'type' => 'toggle',
292 'label' => __('Delete all data on uninstall', 'noted'),
293 'description' => __('When the plugin is deleted, remove every note, strip block-level notes, and clear plugin settings. Off by default — safe upgrades.', 'noted'),
294 'default' => false,
295 ],
296 [
297 'id' => 'settings_export',
298 'type' => 'export',
299 'label' => __('Export settings', 'noted'),
300 'description' => __('Download the current settings as JSON.', 'noted'),
301 'columns' => 6,
302 'args' => [
303 'button_label' => __('Download JSON', 'noted'),
304 'filename' => 'noted-settings',
305 ],
306 ],
307 [
308 'id' => 'settings_import',
309 'type' => 'import',
310 'label' => __('Import settings', 'noted'),
311 'description' => __('Upload a previously exported JSON file to restore settings.', 'noted'),
312 'columns' => 6,
313 'args' => [
314 'button_label' => __('Upload JSON', 'noted'),
315 ],
316 ],
317 ],
318 ],
319 ];
320
321 return [
322 'title' => __('Noted! Settings', 'noted'),
323 'subtitle' => __('Control where notes appear and who can see them.', 'noted'),
324 'tabs' => [
325 [
326 'id' => self::TAB_GENERAL,
327 'title' => __('Settings', 'noted'),
328 'sections' => $generalSections,
329 ],
330 [
331 'id' => self::TAB_MARKDOWN,
332 'title' => __('Markdown', 'noted'),
333 'sections' => [
334 [
335 'id' => 'markdown_reference',
336 'title' => __('Supported Markdown in notes', 'noted'),
337 'description' => __(
338 'Use these patterns in page notes, the floating panel, and anywhere else Noted stores a Markdown note body.',
339 'noted'
340 ),
341 'fields' => [
342 [
343 'id' => 'markdown_doc_intro',
344 'type' => 'html',
345 'label' => '',
346 'columns' => 12,
347 'args' => [
348 'variant' => 'info',
349 'content' => wp_kses_post(
350 '<p>'
351 . esc_html__(
352 'Noted converts a small subset of Markdown into safe HTML. It is not a full Markdown specification — just the essentials for clear notes.',
353 'noted'
354 )
355 . '</p>'
356 ),
357 ],
358 ],
359 [
360 'id' => 'markdown_doc_reference',
361 'type' => 'html',
362 'label' => '',
363 'columns' => 12,
364 'args' => [
365 'variant' => 'plain',
366 'content' => MarkdownReference::settingsTabHtml(),
367 ],
368 ],
369 ],
370 ],
371 ],
372 ],
373 ],
374 ];
375 }
376 }
377