plugin_settings.php

main 186 lines · 6 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Plugin Settings API.
4 *
5 * A plugin that ships plugins/<name>/settings.json gets a configuration page
6 * in the admin panel for free - Admin Panel > Plugins > Settings - instead of
7 * hand-coding a form. Values are stored under the same "plugin:<name>:setting:<key>"
8 * namespace $api->setting() already reads from extensions.php, so a plugin.php
9 * that calls $api->setting('mode') sees exactly what the generated form saved.
10 *
11 * settings.json:
12 * {
13 * "fields": [
14 * {"key": "api_key", "label": "API key", "type": "text", "default": ""},
15 * {"key": "enabled", "label": "Enabled", "type": "bool", "default": "1"},
16 * {"key": "mode", "label": "Mode", "type": "select", "default": "test",
17 * "options": {"test": "Test", "live": "Live"}},
18 * {"key": "max_items", "label": "Max items", "type": "int", "default": "10", "min": 1, "max": 100},
19 * {"key": "notes", "label": "Notes", "type": "textarea", "default": ""},
20 * {"key": "webhook_secret", "label": "Webhook secret", "type": "password", "default": ""}
21 * ]
22 * }
23 *
24 * Supported types: text, textarea, password, bool, int, select, checklist, color.
25 */
26
27const ZNOTE_PLUGIN_SETTINGS_TYPES = array('text', 'textarea', 'password', 'bool', 'int', 'select', 'checklist', 'color');
28
29function znote_plugin_settings_file(string $plugin): string {
30 return ZNOTE_PLUGIN_DIR . '/' . $plugin . '/settings.json';
31}
32
33function znote_plugin_settings_has(string $plugin): bool {
34 $plugin = znote_plugin_sanitize($plugin);
35 return $plugin !== '' && is_file(znote_plugin_settings_file($plugin));
36}
37
38/**
39 * Reads and normalizes settings.json. A malformed file, an unknown type, or a
40 * field key that would not survive ZnoteExtensionApi::settingKey() is dropped
41 * rather than allowed to reach a form or a query.
42 */
43function znote_plugin_settings_schema(string $plugin): array {
44 $plugin = znote_plugin_sanitize($plugin);
45 if ($plugin === '' || !znote_plugin_settings_has($plugin)) {
46 return array();
47 }
48
49 $data = json_decode((string)file_get_contents(znote_plugin_settings_file($plugin)), true);
50 if (!is_array($data) || !isset($data['fields']) || !is_array($data['fields'])) {
51 return array();
52 }
53
54 $fields = array();
55 foreach ($data['fields'] as $field) {
56 if (!is_array($field)) {
57 continue;
58 }
59
60 $key = strtolower(trim((string)($field['key'] ?? '')));
61 $type = strtolower(trim((string)($field['type'] ?? 'text')));
62
63 if ($key === '' || !preg_match('/^[a-z0-9_.-]{1,100}$/', $key) || !in_array($type, ZNOTE_PLUGIN_SETTINGS_TYPES, true)) {
64 continue;
65 }
66
67 $normalized = array(
68 'key' => $key,
69 'type' => $type,
70 'label' => (string)($field['label'] ?? ucwords(str_replace(array('_', '.'), ' ', $key))),
71 'help' => (string)($field['help'] ?? ''),
72 'default' => (string)($field['default'] ?? ''),
73 );
74
75 if (in_array($type, array('select', 'checklist'), true)) {
76 $options = array();
77 foreach ((array)($field['options'] ?? array()) as $value => $label) {
78 $options[(string)$value] = (string)$label;
79 }
80 $normalized['options'] = $options;
81 }
82
83 if ($type === 'int') {
84 $normalized['min'] = array_key_exists('min', $field) ? (int)$field['min'] : null;
85 $normalized['max'] = array_key_exists('max', $field) ? (int)$field['max'] : null;
86 }
87
88 $fields[$key] = $normalized;
89 }
90
91 return $fields;
92}
93
94function znote_plugin_settings_storage_key(string $plugin, string $field): string {
95 return 'plugin:' . $plugin . ':setting:' . $field;
96}
97
98/** Every field's current value: the stored one, or the schema default. */
99function znote_plugin_settings_get(string $plugin): array {
100 $plugin = znote_plugin_sanitize($plugin);
101 $values = array();
102
103 foreach (znote_plugin_settings_schema($plugin) as $key => $field) {
104 $values[$key] = setting(znote_plugin_settings_storage_key($plugin, $key), $field['default']) ?? $field['default'];
105 }
106
107 return $values;
108}
109
110/**
111 * Sanitizes one submitted value against its field definition. Returns the
112 * string to store, or null when the input is invalid for its type - the
113 * caller then leaves the previous value untouched and reports the field.
114 */
115function znote_plugin_settings_sanitize_field(array $field, $raw): ?string {
116 switch ($field['type']) {
117
118 case 'bool':
119 return ($raw !== null && $raw !== '' && $raw !== '0') ? '1' : '0';
120
121 case 'int':
122 if (!is_scalar($raw) || !preg_match('/^-?\d+$/', trim((string)$raw))) {
123 return null;
124 }
125 $value = (int)$raw;
126 if ($field['min'] !== null && $value < $field['min']) {
127 $value = $field['min'];
128 }
129 if ($field['max'] !== null && $value > $field['max']) {
130 $value = $field['max'];
131 }
132 return (string)$value;
133
134 case 'select':
135 $value = (string)$raw;
136 return array_key_exists($value, $field['options']) ? $value : null;
137
138 case 'color':
139 $value = trim((string)$raw);
140 if ($value === '') {
141 return '';
142 }
143 return preg_match('/^#[0-9a-f]{6}$/i', $value) ? $value : null;
144
145 case 'checklist':
146 $chosen = is_array($raw) ? $raw : array();
147 $valid = array_values(array_intersect(array_map('strval', $chosen), array_keys($field['options'])));
148 return implode(',', $valid);
149
150 case 'textarea':
151 case 'password':
152 case 'text':
153 default:
154 return is_scalar($raw) ? (string)$raw : null;
155 }
156}
157
158/**
159 * Validates and stores every field present in $input against the plugin's
160 * schema. A field that fails validation keeps its previous value and its key
161 * is returned in the 'errors' list; everything else is saved.
162 */
163function znote_plugin_settings_save(string $plugin, array $input): array {
164 $plugin = znote_plugin_sanitize($plugin);
165 $schema = znote_plugin_settings_schema($plugin);
166 $errors = array();
167
168 foreach ($schema as $key => $field) {
169 // A checkbox that is off submits nothing at all - treat absence as
170 // false for bool fields, but as "leave alone" for everything else.
171 if (!array_key_exists($key, $input) && $field['type'] !== 'bool' && $field['type'] !== 'checklist') {
172 continue;
173 }
174
175 $sanitized = znote_plugin_settings_sanitize_field($field, $input[$key] ?? null);
176 if ($sanitized === null) {
177 $errors[] = $key;
178 continue;
179 }
180
181 setting_set(znote_plugin_settings_storage_key($plugin, $key), $sanitized);
182 }
183
184 return $errors;
185}
186
Top