PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / php / Settings / settings.php

settings.php in Code Snippets 4.0.0-beta.2, at php/Settings/settings.php

468 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This file registers the settings.
4 *
5 * @package Code_Snippets
6 */
7
8 namespace Code_Snippets\Settings;
9
10 use Code_Snippets\Client\Welcome_Client;
11 use Code_Snippets\Controller\Cloud_Search_Controller;
12 use function add_action;
13 use function Code_Snippets\clean_snippets_cache;
14 use function Code_Snippets\flush_versioned_cache_groups;
15 use function Code_Snippets\code_snippets;
16 use function Code_Snippets\Utils\add_self_option;
17 use function Code_Snippets\Utils\get_self_option;
18 use function Code_Snippets\Utils\update_self_option;
19 use const Code_Snippets\CACHE_GROUP;
20
21 const CACHE_KEY = 'code_snippets_settings';
22 const OPTION_GROUP = 'code-snippets';
23 const OPTION_NAME = 'code_snippets_settings';
24
25 /**
26 * Returns 'true' if plugin settings are unified on a multisite installation
27 * under the Network Admin settings menu
28 *
29 * This option is controlled by the "Enable administration menus" setting on the Network Settings menu
30 *
31 * @return bool
32 */
33 function are_settings_unified(): bool {
34 if ( ! is_multisite() ) {
35 return false;
36 }
37
38 $menu_perms = get_site_option( 'menu_items', [] );
39 return empty( $menu_perms['snippets_settings'] );
40 }
41
42 /**
43 * Retrieve the setting values from the database.
44 *
45 * If a setting does not exist in the database, the default value will be returned.
46 *
47 * @return array<string, array<string, mixed>>
48 */
49 function get_settings_values(): array {
50 $settings = wp_cache_get( CACHE_KEY, CACHE_GROUP );
51 if ( $settings ) {
52 return $settings;
53 }
54
55 $settings = Settings_Fields::get_default_values();
56 $saved = get_self_option( are_settings_unified(), OPTION_NAME, [] );
57
58 // Deep merge the saved settings with the default values.
59 foreach ( $settings as $section => $section_fields ) {
60 if ( isset( $saved[ $section ] ) ) {
61 $settings[ $section ] = array_replace( $section_fields, $saved[ $section ] );
62 }
63 }
64
65 wp_cache_set( CACHE_KEY, $settings, CACHE_GROUP );
66 return $settings;
67 }
68
69 /**
70 * Retrieve an individual setting field value
71 *
72 * @param string $section ID of the section the setting belongs to.
73 * @param string $field ID of the setting field.
74 *
75 * @return mixed
76 */
77 function get_setting( string $section, string $field ) {
78 $settings = get_settings_values();
79
80 return $settings[ $section ][ $field ] ?? null;
81 }
82
83 /**
84 * Update a single setting to a new value.
85 *
86 * @param string $section ID of the section the setting belongs to.
87 * @param string $field ID of the setting field.
88 * @param mixed $new_value Setting value. Expected to not be SQL-escaped.
89 *
90 * @return bool False if value was not updated. True if value was updated.
91 */
92 function update_setting( string $section, string $field, $new_value ): bool {
93 $settings = get_settings_values();
94
95 $settings[ $section ][ $field ] = $new_value;
96
97 wp_cache_set( CACHE_KEY, $settings, CACHE_GROUP );
98 return update_self_option( are_settings_unified(), OPTION_NAME, $settings );
99 }
100
101 /**
102 * Render the fields of a settings section, with group headings.
103 *
104 * Mirrors the core `do_settings_fields()`, adding a full-width heading row
105 * above any field carrying a `group_heading` argument. Tabs are long enough
106 * that unbroken rows are hard to scan.
107 *
108 * @param string $page Settings page slug.
109 * @param string $section Settings section identifier.
110 *
111 * @return void
112 */
113 function do_settings_fields_with_headings( string $page, string $section ): void {
114 global $wp_settings_fields;
115
116 if ( ! isset( $wp_settings_fields[ $page ][ $section ] ) ) {
117 return;
118 }
119
120 $seen_headings = [];
121
122 foreach ( (array) $wp_settings_fields[ $page ][ $section ] as $field ) {
123 $heading = $field['args']['group_heading'] ?? '';
124
125 // A heading is skipped if an identical one has already been drawn, so
126 // that a group whose first field is absent does not repeat it.
127 if ( $heading && ! in_array( $heading, $seen_headings, true ) ) {
128 $seen_headings[] = $heading;
129 printf(
130 '<tr class="settings-group-heading"><th colspan="2" scope="colgroup">%s</th></tr>',
131 esc_html( $heading )
132 );
133 }
134
135 $class = empty( $field['args']['class'] ) ? '' : ' class="' . esc_attr( $field['args']['class'] ) . '"';
136
137 echo '<tr' . $class . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above.
138
139 // A field with no title, such as a notice rendered by a callback, spans
140 // the row rather than leaving an empty header cell for a screen reader.
141 if ( '' === (string) $field['title'] ) {
142 echo '<td colspan="2">';
143 call_user_func( $field['callback'], $field['args'] );
144 echo '</td></tr>';
145 continue;
146 }
147
148 if ( ! empty( $field['args']['label_for'] ) ) {
149 printf(
150 '<th scope="row"><label for="%s">%s</label></th>',
151 esc_attr( $field['args']['label_for'] ),
152 esc_html( $field['title'] )
153 );
154 } else {
155 printf( '<th scope="row">%s</th>', esc_html( $field['title'] ) );
156 }
157
158 echo '<td>';
159 call_user_func( $field['callback'], $field['args'] );
160 echo '</td></tr>';
161 }
162 }
163
164 /**
165 * Retrieve the settings sections
166 *
167 * @return array<string, string> Settings sections.
168 */
169 function get_settings_sections(): array {
170 // Tabs are grouped by task rather than by storage section. Values still
171 // save into their original sections; see Settings_Layout.
172 $sections = Settings_Layout::get_available_tabs();
173
174 return apply_filters( 'code_snippets_settings_sections', $sections );
175 }
176
177 /**
178 * Register settings sections, fields, etc
179 */
180 function register_plugin_settings() {
181 if ( ! get_self_option( are_settings_unified(), OPTION_NAME ) ) {
182 add_self_option( are_settings_unified(), OPTION_NAME, Settings_Fields::get_default_values() );
183 }
184
185 $current_settings = get_settings_values();
186
187 // Register the setting.
188 register_setting(
189 OPTION_GROUP,
190 OPTION_NAME,
191 [ 'sanitize_callback' => __NAMESPACE__ . '\\sanitize_settings' ]
192 );
193
194 // Register settings sections.
195 foreach ( get_settings_sections() as $section_id => $section_name ) {
196 add_settings_section( $section_id, $section_name, '__return_empty_string', 'code-snippets' );
197 }
198
199 // Register settings fields. The tab a field appears under comes from the
200 // layout, while the section it saves into stays exactly as it was.
201 $definitions = Settings_Fields::get_field_definitions();
202 $descriptions = Settings_Layout::get_descriptions();
203 $headings = Settings_Layout::get_group_headings();
204
205 foreach ( get_settings_sections() as $tab_id => $tab_name ) {
206 foreach ( Settings_Layout::get_visible_fields( $tab_id, $current_settings ) as $entry ) {
207 list( $section_id, $field_id ) = $entry;
208 $field = $definitions[ $section_id ][ $field_id ];
209
210 // Reword the settings whose description names the control without
211 // saying what it buys you.
212 if ( isset( $descriptions[ $field_id ] ) ) {
213 $field['desc'] = $descriptions[ $field_id ];
214 }
215
216 $field_object = new Setting_Field( $section_id, $field_id, $field );
217
218 // Field types that render a single labelable control get their table
219 // heading turned into a real <label for>, giving the control an
220 // accessible name. Checkboxes render their own labels, and
221 // callback/action fields have no single control to point at.
222 $labelable_types = [ 'text', 'number', 'select' ];
223 $field_args = [];
224
225 if ( isset( $field['type'] ) && in_array( $field['type'], $labelable_types, true ) ) {
226 $field_args['label_for'] = $field_object->element_id;
227 }
228
229 if ( isset( $headings[ $tab_id ][ $field_id ] ) ) {
230 $field_args['group_heading'] = $headings[ $tab_id ][ $field_id ];
231 }
232
233 add_settings_field(
234 $field_id,
235 $field['name'],
236 [ $field_object, 'render' ],
237 'code-snippets',
238 $tab_id,
239 $field_args
240 );
241 }
242 }
243
244 $editor_preview = new Editor_Preview();
245
246 // Add editor preview as a field.
247 add_settings_field(
248 'editor_preview',
249 __( 'Editor Preview', 'code-snippets' ),
250 [ $editor_preview, 'render' ],
251 'code-snippets',
252 'editing'
253 );
254
255 Version_Switch::init();
256 }
257
258 add_action( 'admin_init', __NAMESPACE__ . '\\register_plugin_settings' );
259
260 /**
261 * Determine whether a setting field should be rendered.
262 *
263 * @param array<string, mixed> $field Field definition.
264 * @param array<string, array<string,mixed>> $settings Current settings values.
265 * @param array<string, array<string,mixed>>|null $input Optional raw input values.
266 *
267 * @return bool
268 */
269 function should_render_setting_field( array $field, array $settings, ?array $input = null ): bool {
270 if ( empty( $field['show_if'] ) || ! is_array( $field['show_if'] ) ) {
271 return true;
272 }
273
274 $show_if = array_merge(
275 [
276 'section' => '',
277 'field' => '',
278 'value' => true,
279 ],
280 $field['show_if']
281 );
282
283 $section = is_string( $show_if['section'] ) ? $show_if['section'] : '';
284 $field_id = is_string( $show_if['field'] ) ? $show_if['field'] : '';
285 $expected = $show_if['value'];
286
287 if ( '' === $section || '' === $field_id ) {
288 return true;
289 }
290
291 $actual = null;
292
293 if ( is_array( $input ) && isset( $input[ $section ] ) && is_array( $input[ $section ] ) && array_key_exists( $field_id, $input[ $section ] ) ) {
294 $actual = $input[ $section ][ $field_id ];
295 } elseif ( isset( $settings[ $section ] ) && array_key_exists( $field_id, $settings[ $section ] ) ) {
296 $actual = $settings[ $section ][ $field_id ];
297 }
298
299 if ( is_bool( $expected ) ) {
300 if ( is_bool( $actual ) ) {
301 return $actual === $expected;
302 }
303
304 return ( 'on' === $actual ) === $expected;
305 }
306
307 return $actual === $expected;
308 }
309
310 /**
311 * Sanitize a single setting value.
312 *
313 * @param array<string, mixed> $field Setting field information.
314 * @param mixed $input_value User input setting value, or null if missing.
315 *
316 * @return mixed Sanitized setting value, or null if unset.
317 */
318 function sanitize_setting_value( array $field, $input_value ) {
319 switch ( $field['type'] ) {
320
321 case 'checkbox':
322 return 'on' === $input_value;
323
324 case 'number':
325 return intval( $input_value );
326
327 case 'select':
328 $select_options = array_map( 'strval', array_keys( $field['options'] ) );
329 return in_array( strval( $input_value ), $select_options, true ) ? $input_value : null;
330
331 case 'checkboxes':
332 $results = [];
333
334 if ( ! empty( $input_value ) ) {
335 foreach ( $field['options'] as $option_id => $option_label ) {
336 if ( isset( $input_value[ $option_id ] ) && 'on' === $input_value[ $option_id ] ) {
337 $results[] = $option_id;
338 }
339 }
340 }
341
342 return $results;
343
344 case 'text':
345 case 'hidden':
346 return trim( sanitize_text_field( $input_value ) );
347
348 case 'callback':
349 return isset( $field['sanitize_callback'] ) && is_callable( $field['sanitize_callback'] ) ?
350 call_user_func( $field['sanitize_callback'], $input_value ) :
351 null;
352
353 default:
354 return null;
355 }
356 }
357
358 /**
359 * Process settings actions.
360 *
361 * @param array $input Provided settings input.
362 *
363 * @return array|null New $input value to return, or null to continue with settings update process.
364 */
365 function process_settings_actions( array $input ): ?array {
366
367 if ( isset( $input['reset_settings'] ) ) {
368 add_settings_error(
369 OPTION_NAME,
370 'settings_reset',
371 __( 'All settings have been reset to their defaults.', 'code-snippets' ),
372 'updated'
373 );
374
375 delete_option( 'code_snippets_cloud_settings' );
376 return [];
377 }
378
379 if ( isset( $input['debug']['database_update'] ) ) {
380 code_snippets()->db->create_or_upgrade_tables();
381
382 add_settings_error(
383 OPTION_NAME,
384 'database_update_done',
385 __( 'Successfully performed database table upgrade.', 'code-snippets' ),
386 'updated'
387 );
388 }
389
390 if ( isset( $input['debug']['reset_caches'] ) ) {
391 Welcome_Client::clear_cache();
392 Cloud_Search_Controller::clear_caches();
393 clean_snippets_cache( code_snippets()->db->get_table_name( false ) );
394
395 if ( is_multisite() ) {
396 clean_snippets_cache( code_snippets()->db->get_table_name( true ) );
397 }
398
399 // Deleting known keys cannot reach data written by a different version
400 // of the plugin, which is what needs clearing before a rollback, so the
401 // versioned groups (current, previous and legacy) all go too.
402 flush_versioned_cache_groups( (string) get_option( 'code_snippets_cache_version', '' ) );
403
404 add_settings_error(
405 OPTION_NAME,
406 'snippet_caches_reset',
407 __( 'Successfully reset snippets caches.', 'code-snippets' ),
408 'updated'
409 );
410 }
411
412 return null;
413 }
414
415 /**
416 * Validate the settings
417 *
418 * @param array<string, array<string, mixed>> $input The received settings.
419 *
420 * @return array<string, array<string, mixed>> The validated settings.
421 */
422 function sanitize_settings( array $input ): array {
423 wp_cache_delete( CACHE_KEY, CACHE_GROUP );
424 $result = process_settings_actions( $input );
425
426 if ( ! is_null( $result ) ) {
427 return $result;
428 }
429
430 $settings = get_settings_values();
431 $updated = false;
432
433 // Don't directly loop through $input as it does not include as deselected checkboxes.
434 foreach ( Settings_Fields::get_field_definitions() as $section_id => $fields ) {
435 foreach ( $fields as $field_id => $field ) {
436 if ( ! should_render_setting_field( $field, $settings, $input ) ) {
437 continue;
438 }
439
440 // Fetch the corresponding input value from the posted data.
441 $input_value = $input[ $section_id ][ $field_id ] ?? null;
442 $stored_value = $settings[ $section_id ][ $field_id ] ?? null;
443
444 // Attempt to sanitize the setting value.
445 $sanitized_value = sanitize_setting_value( $field, $input_value );
446
447 if ( ! is_null( $sanitized_value ) && $stored_value !== $sanitized_value ) {
448 $settings[ $section_id ][ $field_id ] = $sanitized_value;
449 $updated = true;
450 }
451 }
452 }
453
454 // Add an updated message.
455 if ( $updated ) {
456 add_settings_error(
457 OPTION_NAME,
458 'settings-saved',
459 __( 'Settings saved.', 'code-snippets' ),
460 'updated'
461 );
462
463 do_action( 'code_snippets/settings_updated', $settings, $input );
464 }
465
466 return $settings;
467 }
468