PluginProbe ʕ •ᴥ•ʔ
Admin Help Docs / trunk
Admin Help Docs vtrunk
2.0.1.1 trunk 1.4.3.2 2.0.0 2.0.0.1 2.0.0.2 2.0.1
admin-help-docs / inc / tabs / settings.php
admin-help-docs / inc / tabs Last commit date
css 3 months ago js 3 months ago admin-menu.php 3 months ago documentation.php 3 months ago faq.php 3 months ago import.php 3 months ago settings.php 3 months ago support.php 3 months ago
settings.php
1268 lines
1 <?php
2 /**
3 * Settings Tab Loader
4 */
5
6 namespace PluginRx\AdminHelpDocs;
7
8 if ( ! defined( 'ABSPATH' ) ) exit;
9
10 class Settings {
11
12
13 /**
14 * Returns all settings boxes.
15 */
16 public static function setting_boxes() : array {
17 $boxes = [
18 'interface' => __( 'Interface', 'admin-help-docs' ),
19 'content_output' => __( 'Content & Output', 'admin-help-docs' ),
20 'access_control' => __( 'Access Control', 'admin-help-docs' ),
21 'advanced' => __( 'Advanced', 'admin-help-docs' ),
22 ];
23
24 return apply_filters( 'helpdocs_settings_boxes', $boxes );
25 } // End setting_boxes()
26
27
28 /**
29 * Returns all settings fields.
30 */
31 public static function setting_fields( $defaults_only = false ) : array {
32 $themes = [];
33 foreach ( Colors::themes() as $key => $theme ) {
34 $themes[ $key ] = $theme[ 'label' ];
35 }
36 $themes[ 'custom' ] = __( 'Custom (Use the color pickers below to customize)', 'admin-help-docs' );
37
38 $top_placements = [];
39 foreach ( self::top_placements() as $key => $label ) {
40 $top_placements[] = [
41 'value' => $key,
42 'label' => $label,
43 ];
44 }
45
46 $legacy_cap = get_option( 'helpdocs_user_view_cap' );
47 $current_type = get_option( 'helpdocs_user_view_type' );
48 $view_default = ( false !== $legacy_cap && false === $current_type ) ? 'capability' : 'role';
49
50 $fields = [
51 // Interface
52 [
53 'name' => 'menu_title',
54 'label' => __( 'Menu Title', 'admin-help-docs' ),
55 'type' => 'text',
56 'sanitize' => 'sanitize_text_field',
57 'box' => 'interface',
58 'default' => 'Help Docs',
59 ],
60 [
61 'name' => 'dashicon',
62 'label' => __( 'Menu Icon', 'admin-help-docs' ) . ' — <a id="view-dashicons-link" href="https://developer.wordpress.org/resource/dashicons/#editor-help" target="_blank">View Dashicons <span class="dashicons dashicons-external"></span></a>',
63 'type' => 'select',
64 'choices' => Helpers::get_dashicons(),
65 'sanitize' => 'sanitize_text_field',
66 'box' => 'interface',
67 'default' => 'editor-help',
68 ],
69 [
70 'name' => 'menu_position',
71 'label' => __( 'Menu Position', 'admin-help-docs' ),
72 'desc' => __( '1 = Above Dashboard, 2 = Under Dashboard, 999 = Bottom, etc.', 'admin-help-docs' ),
73 'type' => 'number',
74 'sanitize' => 'absint',
75 'box' => 'interface',
76 'default' => Menu::$default_menu_position,
77 ],
78 [
79 'name' => 'page_title',
80 'label' => __( 'Page Title', 'admin-help-docs' ),
81 'type' => 'text',
82 'sanitize' => 'sanitize_text_field',
83 'box' => 'interface',
84 'default' => Bootstrap::name(),
85 ],
86 [
87 'name' => 'logo',
88 'label' => __( 'Page Logo', 'admin-help-docs' ),
89 'desc' => __( 'Preferred size: 100x100 pixels. Accepted formats: jpg | jpeg | png | webp', 'admin-help-docs' ),
90 'type' => 'text',
91 'sanitize' => 'sanitize_text_field',
92 'box' => 'interface',
93 'default' => Helpers::get_default_logo_url(),
94 ],
95 [
96 'name' => 'doc_logo',
97 'label' => __( 'Help Doc Logo', 'admin-help-docs' ),
98 'desc' => __( 'You can also change the logo on all of the help docs, which may differ from the page logo if the background is contrasted. For example a light logo on dark background at the top of the page, and a dark logo on a light background on the help docs themselves. You can also disable the help doc logo from the Content & Output settings on the right. The preferred size of the image is 100x100 pixels. Accepted formats: jpg | jpeg | png | webp', 'admin-help-docs' ),
99 'type' => 'text',
100 'sanitize' => 'sanitize_text_field',
101 'box' => 'interface',
102 'default' => Helpers::get_default_logo_url(),
103 ],
104 [
105 'name' => 'themes',
106 'label' => __( 'Color Theme', 'admin-help-docs' ),
107 'type' => 'select',
108 'choices' => $themes,
109 'sanitize' => 'sanitize_text_field',
110 'box' => 'interface',
111 'default' => 'custom',
112 ],
113 [
114 'name' => 'contact_form',
115 'label' => __( 'Enable Support Contact Form', 'admin-help-docs' ),
116 'desc' => sprintf(
117 /* translators: %s is the current site admin email address. */
118 __( 'Adds a simple contact form within the Help Docs menu, allowing clients to reach out for support directly. Emails will be sent from %s. This feature utilizes wp_mail() for delivery. To ensure reliable inbox placement, using a dedicated provider like WP Mail SMTP (with Brevo) is advised, along with WP Mail Logging for tracking sent messages.', 'admin-help-docs' ),
119 '<strong>' . esc_html( get_option( 'admin_email' ) ) . '</strong>'
120 ),
121 'type' => 'checkbox',
122 'sanitize' => 'sanitize_checkbox',
123 'box' => 'interface',
124 'default' => false,
125 'has_condition' => true,
126 ],
127 [
128 'name' => 'contact_name',
129 'label' => __( 'Support Contact Name', 'admin-help-docs' ),
130 'desc' => __( 'Enter the name that clients can use to reach you. This will be displayed on the contact form page. Can be a business or personal name.', 'admin-help-docs' ),
131 'type' => 'text',
132 'sanitize' => 'sanitize_text_field',
133 'box' => 'interface',
134 'default' => '',
135 'condition' => [ 'contact_form' => true ],
136 ],
137 [
138 'name' => 'contact_emails',
139 'label' => __( 'Support Contact Email(s)', 'admin-help-docs' ),
140 'desc' => __( 'Enter the email address(es) that will receive messages from the contact form. Separate multiple emails with commas.', 'admin-help-docs' ),
141 'type' => 'text',
142 'sanitize' => 'sanitize_text_field',
143 'box' => 'interface',
144 'default' => implode( ', ', Helpers::get_all_admin_emails() ),
145 'condition' => [ 'contact_form' => true ],
146 ],
147 [
148 'name' => 'contact_phone',
149 'label' => __( 'Support Contact Phone Number', 'admin-help-docs' ),
150 'desc' => __( 'Enter a phone number that clients can use to reach you. This will be displayed on the contact form page.', 'admin-help-docs' ),
151 'type' => 'text',
152 'sanitize' => 'sanitize_text_field',
153 'box' => 'interface',
154 'default' => '',
155 'condition' => [ 'contact_form' => true ],
156 ],
157 [
158 'name' => 'admin_bar',
159 'label' => __( 'Enable Admin Bar Menu for Backend', 'admin-help-docs' ),
160 'type' => 'checkbox',
161 'sanitize' => 'sanitize_checkbox',
162 'box' => 'interface',
163 'default' => false,
164 ],
165 [
166 'name' => 'admin_bar_frontend',
167 'label' => __( 'Enable Admin Bar Menu for Frontend', 'admin-help-docs' ),
168 'type' => 'checkbox',
169 'sanitize' => 'sanitize_checkbox',
170 'box' => 'interface',
171 'default' => false,
172 ],
173 [
174 'name' => 'replace_dashboard',
175 'label' => __( 'Replace WordPress Dashboard with a Help Doc', 'admin-help-docs' ),
176 'desc' => __( 'Replace the default WordPress dashboard with a custom help docs page. To add docs, set their locations to "WordPress Dashboard (Replaces Dashboard Entirely)". Warning: Replacing the WordPress dashboard entirely may affect other plugins and functionality. No other widgets or dashboard elements will be displayed.', 'admin-help-docs' ),
177 'type' => 'checkbox',
178 'sanitize' => 'sanitize_checkbox',
179 'box' => 'interface',
180 'default' => false,
181 ],
182 [
183 'name' => 'dashboard_toc',
184 'label' => __( 'Enable Dashboard Table of Contents Widget', 'admin-help-docs' ),
185 'desc' => __( 'Adds a dashboard widget with a table of contents for the docs on the Main Documentation Page.', 'admin-help-docs' ),
186 'type' => 'checkbox',
187 'sanitize' => 'sanitize_checkbox',
188 'box' => 'interface',
189 'default' => false,
190 ],
191 [
192 'name' => 'gutenberg_editor',
193 'label' => __( 'Use Gutenberg Editor', 'admin-help-docs' ),
194 'desc' => __( 'Adds support for the Gutenberg editor for the documentation. Default is the classic editor.', 'admin-help-docs' ),
195 'type' => 'checkbox',
196 'sanitize' => 'sanitize_checkbox',
197 'box' => 'interface',
198 'default' => false,
199 ],
200
201 // Content & Output
202 [
203 'name' => 'admin_bar_include_content',
204 'label' => __( 'Include Doc Content in Admin Bar', 'admin-help-docs' ),
205 'desc' => __( 'Includes a snippet of the document content in the admin bar.', 'admin-help-docs' ),
206 'type' => 'checkbox',
207 'sanitize' => 'sanitize_checkbox',
208 'box' => 'content_output',
209 'default' => false,
210 ],
211 [
212 'name' => 'default_doc',
213 'label' => __( 'Default Document on Main Docs Page', 'admin-help-docs' ),
214 'type' => 'select',
215 'choices' => Helpers::get_main_helpdoc_options(),
216 'sanitize' => 'sanitize_text_field',
217 'box' => 'content_output',
218 ],
219 [
220 'name' => 'hide_doc_meta',
221 'label' => __( 'Hide Document Meta on Main Docs Page', 'admin-help-docs' ),
222 'desc' => __( 'Includes created and last modified dates and authors.', 'admin-help-docs' ),
223 'type' => 'checkbox',
224 'sanitize' => 'sanitize_checkbox',
225 'box' => 'content_output',
226 'default' => false,
227 ],
228 [
229 'name' => 'auto_htoc',
230 'label' => __( 'Auto-Generate Table of Contents on Main Docs Page', 'admin-help-docs' ),
231 'desc' => __( 'Automatically generate a table of contents from headings (H2–H6) at the top of each documentation page.', 'admin-help-docs' ),
232 'type' => 'checkbox',
233 'sanitize' => 'sanitize_checkbox',
234 'box' => 'content_output',
235 'default' => false,
236 ],
237 [
238 'name' => 'curly_quotes',
239 'label' => __( 'Disable Curly Quotes on Main Docs Page', 'admin-help-docs' ),
240 'desc' => __( 'WP automatically converts straight quotes (") to curly quotes (”), which makes sharing code difficult.', 'admin-help-docs' ),
241 'type' => 'checkbox',
242 'sanitize' => 'sanitize_checkbox',
243 'box' => 'content_output',
244 'default' => false,
245 ],
246 [
247 'name' => 'enqueue_frontend_styles',
248 'label' => __( 'Use Frontend Stylesheets on Main Docs Page', 'admin-help-docs' ),
249 'desc' => __( 'Adds support for your frontend styles in the backend by enqueueing them while on the Main Docs Page.', 'admin-help-docs' ),
250 'type' => 'checkbox',
251 'sanitize' => 'sanitize_checkbox',
252 'box' => 'content_output',
253 'default' => false,
254 ],
255 [
256 'name' => 'main_docs_css',
257 'label' => __( 'Main Docs Page CSS', 'admin-help-docs' ),
258 'desc' => __( 'Add CSS here to style the Main Documentation Page specifically. You must use CSS selectors.', 'admin-help-docs' ),
259 'type' => 'textarea',
260 'sanitize' => 'sanitize_custom_css',
261 'box' => 'content_output',
262 'default' => '',
263 ],
264 [
265 'name' => 'include_logo_on_docs',
266 'label' => __( 'Include Logo on Help Docs', 'admin-help-docs' ),
267 'desc' => __( 'Includes the site logo on individual help docs that are not on the Main Docs Page.', 'admin-help-docs' ),
268 'type' => 'checkbox',
269 'sanitize' => 'sanitize_checkbox',
270 'box' => 'content_output',
271 'default' => true,
272 ],
273 [
274 'name' => 'top_location_type',
275 'label' => __( 'Top Location Placement', 'admin-help-docs' ),
276 'type' => 'select',
277 'choices' => $top_placements,
278 'sanitize' => 'sanitize_key',
279 'box' => 'content_output',
280 'default' => 'admin_notices',
281 ],
282 [
283 'name' => 'footer_left',
284 'label' => __( 'Left Footer Text', 'admin-help-docs' ),
285 'desc' => __( 'Supports HTML. Default: "Thank you for creating with WordPress."', 'admin-help-docs' ),
286 'type' => 'textarea',
287 'sanitize' => 'wp_kses_post',
288 'box' => 'content_output',
289 'default' => '<span id="footer-thankyou">Thank you for creating with <a href="https://wordpress.org/">WordPress</a>.</span>',
290 ],
291 [
292 'name' => 'footer_right',
293 'label' => __( 'Right Footer Text', 'admin-help-docs' ),
294 'desc' => __( 'Use <code>{version}</code> to display the current WordPress version', 'admin-help-docs' ),
295 'type' => 'textarea',
296 'sanitize' => 'wp_kses_post',
297 'box' => 'content_output',
298 'default' => 'Version {version}',
299 ],
300
301 // Access & Permissions
302 [
303 'name' => 'api',
304 'label' => __( 'Allow Public by Default', 'admin-help-docs' ),
305 'type' => 'checkbox',
306 'sanitize' => 'sanitize_checkbox',
307 'box' => 'access_control',
308 'default' => false,
309 ],
310 [
311 'name' => 'api_key',
312 'label' => __( 'Public Access API Key (Optional)', 'admin-help-docs' ),
313 'desc' => __( 'If you enable "Allow Public" above or on individual docs, you can optionally require an API key for access. This adds a layer of security by ensuring that only users with the key can import your documentation. To use, generate a key here and enter it into the import on your other site where you are importing the docs. Leave empty to allow public access without a key.', 'admin-help-docs' ),
314 'type' => 'api_key',
315 'sanitize' => 'sanitize_text_field',
316 'box' => 'access_control',
317 'default' => false,
318 ],
319 [
320 'name' => 'user_view_type',
321 'label' => __( 'Requirement Type to View Docs', 'admin-help-docs' ),
322 'type' => 'select',
323 'choices' => [
324 'capability' => __( 'Capability', 'admin-help-docs' ),
325 'role' => __( 'Role', 'admin-help-docs' ),
326 ],
327 'sanitize' => 'sanitize_text_field',
328 'box' => 'access_control',
329 'default' => $view_default,
330 'has_condition' => true,
331 ],
332 [
333 'name' => 'user_view_cap',
334 'label' => __( 'Default Capability Required to View Docs', 'admin-help-docs' ) . ' — <a href="https://wordpress.org/documentation/article/roles-and-capabilities/" target="_blank">View a list of capabilities. <span class="dashicons dashicons-external"></span></a>',
335 'desc' => __( 'Use <code>manage_options</code> for admins only. You can also override this setting on a per-document basis.', 'admin-help-docs' ),
336 'type' => 'text',
337 'sanitize' => 'sanitize_text_field',
338 'box' => 'access_control',
339 'default' => 'manage_options',
340 'condition' => [ 'user_view_type' => 'capability' ],
341 ],
342 [
343 'name' => 'view_roles',
344 'label' => __( 'Additional Default Roles Required to View Docs', 'admin-help-docs' ),
345 'desc' => __( 'Admins can view all docs regardless of this setting. You can also override this setting on a per-document basis.', 'admin-help-docs' ),
346 'type' => 'checkboxes',
347 'choices' => Helpers::get_role_options(),
348 'sanitize' => 'sanitize_checkboxes',
349 'box' => 'access_control',
350 'condition' => [ 'user_view_type' => 'role' ],
351 ],
352 [
353 'name' => 'user_edit_type',
354 'label' => __( 'Requirement Type to Edit Docs', 'admin-help-docs' ),
355 'type' => 'select',
356 'choices' => [
357 'capability' => __( 'Capability', 'admin-help-docs' ),
358 'role' => __( 'Role', 'admin-help-docs' ),
359 ],
360 'sanitize' => 'sanitize_text_field',
361 'box' => 'access_control',
362 'default' => 'role',
363 'has_condition' => true,
364 ],
365 [
366 'name' => 'user_edit_cap',
367 'label' => __( 'Capability Required to Edit Docs', 'admin-help-docs' ) . ' — <a href="https://wordpress.org/documentation/article/roles-and-capabilities/" target="_blank">View a list of capabilities. <span class="dashicons dashicons-external"></span></a>',
368 'desc' => __( 'Use <code>manage_options</code> for admins only.', 'admin-help-docs' ),
369 'type' => 'text',
370 'sanitize' => 'sanitize_text_field',
371 'box' => 'access_control',
372 'default' => 'manage_options',
373 'condition' => [ 'user_edit_type' => 'capability' ],
374 ],
375 [
376 'name' => 'edit_roles',
377 'label' => __( 'Additional Roles Required to Edit Docs', 'admin-help-docs' ),
378 'desc' => __( 'Admins can edit all docs regardless of this setting.', 'admin-help-docs' ),
379 'type' => 'checkboxes',
380 'choices' => Helpers::get_role_options(),
381 'sanitize' => 'sanitize_checkboxes',
382 'box' => 'access_control',
383 'condition' => [ 'user_edit_type' => 'role' ],
384 ],
385
386 // Advanced
387 [
388 'name' => 'remove_on_uninstall',
389 'label' => __( 'Remove All Plugin Data on Uninstall', 'admin-help-docs' ),
390 'desc' => __( 'Deletes all plugin settings and documentation permanently when the plugin is deleted.', 'admin-help-docs' ),
391 'type' => 'checkbox',
392 'sanitize' => 'sanitize_checkbox',
393 'box' => 'advanced',
394 'default' => false,
395 ],
396 [
397 'name' => 'flush_cache',
398 'label' => __( 'System Cache Management', 'admin-help-docs' ),
399 'type' => 'html',
400 'box' => 'advanced',
401 'content' => '<button type="button" id="helpdocs-flush-cache" class="helpdocs-button">' . __( 'Clear Stored Data', 'admin-help-docs' ) . '</button>',
402 ],
403 [
404 'name' => 'upload_download_colors',
405 'label' => __( 'Upload/Download Colors', 'admin-help-docs' ),
406 'desc' => __( 'Export your current color settings as a JSON file for backup or transfer to another site. You can also import color settings from a JSON file exported from this plugin. Note: Importing settings will overwrite your current color settings.', 'admin-help-docs' ),
407 'type' => 'html',
408 'box' => 'advanced',
409 'content' => '<button type="button" id="helpdocs-download-colors-btn" class="helpdocs-button">' . __( 'Download Colors', 'admin-help-docs' ) . '</button> <div id="helpdocs-upload-colors-button"><label for="helpdocs-upload-colors"><span class="helpdocs-button">' . __( 'Upload Colors', 'admin-help-docs' ) . '</span></label><input type="file" id="helpdocs-upload-colors" name="helpdocs-upload-colors" accept=".json"></div> <div id="helpdocs-upload-colors-filename"></div>',
410 ],
411 [
412 'name' => 'upload_download_settings',
413 'label' => __( 'Upload/Download Settings', 'admin-help-docs' ),
414 'desc' => __( 'Export your current settings as a JSON file for backup or transfer to another site. You can also import settings from a JSON file exported from this plugin. Note: Importing settings will overwrite your current settings.', 'admin-help-docs' ),
415 'type' => 'html',
416 'box' => 'advanced',
417 'content' => '<button type="button" id="helpdocs-download-settings-btn" class="helpdocs-button">' . __( 'Download Settings', 'admin-help-docs' ) . '</button> <div id="helpdocs-upload-button"><label for="helpdocs-upload-settings"><span class="helpdocs-button">' . __( 'Upload Settings', 'admin-help-docs' ) . '</span></label><input type="file" id="helpdocs-upload-settings" name="helpdocs-upload-settings" accept=".json"></div> <div id="helpdocs-upload-filename"></div>',
418 ],
419 [
420 'name' => 'reset_settings',
421 'label' => __( 'Reset Settings', 'admin-help-docs' ),
422 'desc' => __( 'This will not delete any documentation, but it will reset all settings to their defaults.', 'admin-help-docs' ),
423 'type' => 'html',
424 'box' => 'advanced',
425 'content' => '<button type="button" id="helpdocs-reset-colors" class="helpdocs-button">' . __( 'Reset All Colors', 'admin-help-docs' ) . '</button> <button type="button" id="helpdocs-reset-settings" class="helpdocs-button">' . __( 'Reset All Settings', 'admin-help-docs' ) . '</button>',
426 ],
427 ];
428
429 // Colors
430 $color_fields = [];
431 $colors = Colors::defaults();
432 foreach ( $colors as $key => $color ) {
433 if ( strpos( $key, 'subheader' ) !== false ) {
434 continue;
435 }
436 $color_fields[] = [
437 'name' => 'color_' . $key,
438 'label' => $color[ 'label' ],
439 'type' => 'color',
440 'sanitize' => 'sanitize_text_field',
441 'box' => 'interface',
442 'default' => $color[ 'color' ],
443 ];
444 }
445
446 $themes_index = false;
447 foreach ( $fields as $index => $field ) {
448 if ( isset( $field[ 'name' ] ) && 'themes' === $field[ 'name' ] ) {
449 $themes_index = $index;
450 break;
451 }
452 }
453
454 if ( false !== $themes_index ) {
455 array_splice( $fields, $themes_index + 1, 0, $color_fields );
456 } else {
457 $fields = array_merge( $fields, $color_fields );
458 }
459
460 // Only get the name, type and default for each field
461 if ( $defaults_only ) {
462 foreach ( $fields as $index => $field ) {
463 $fields[ $index ] = [
464 'name' => $field[ 'name' ],
465 'type' => $field[ 'type' ],
466 'default' => $field[ 'default' ] ?? null,
467 ];
468 }
469
470 }
471
472 return apply_filters( 'helpdocs_settings_fields', $fields );
473 } // End setting_fields()
474
475
476 /**
477 * Returns available placements for top docs.
478 *
479 * @return array
480 */
481 public static function top_placements() : array {
482 return [
483 'admin_notices' => __( 'Just Above Page Title', 'admin-help-docs' ),
484 'in_admin_header' => __( 'Very Top of Page (Above All Content)', 'admin-help-docs' ),
485 ];
486 } // End top_placements()
487
488
489 /**
490 * The single instance of the class
491 *
492 * @var self|null
493 */
494 private static ?Settings $instance = null;
495
496
497 /**
498 * Get the singleton instance
499 *
500 * @return self
501 */
502 public static function instance() : self {
503 return self::$instance ??= new self();
504 } // End instance()
505
506
507 /**
508 * Constructor
509 */
510 private function __construct() {
511 add_action( 'helpdocs_subheader_left', [ $this, 'render_save_reminder' ] );
512 add_action( 'wp_ajax_helpdocs_save_settings', [ $this, 'ajax_save_settings' ] );
513 add_action( 'wp_ajax_helpdocs_flush_cache', [ $this, 'ajax_flush_cache' ] );
514 } // End __construct()
515
516
517 /**
518 * Render a reminder to save settings after changes are made
519 *
520 * @param string $current_tab The current active tab
521 */
522 public function render_save_reminder( $current_tab ) {
523 if ( $current_tab === 'settings' ) {
524 echo '<span id="helpdocs-save-reminder">' . esc_html__( 'Remember to click "Save" after making changes to your settings.', 'admin-help-docs' ) . '</span>';
525 }
526 } // End render_save_reminder()
527
528
529 /**
530 * Render the tab
531 */
532 public function render_tab() {
533 $fields = $this->setting_fields();
534 $boxes = [];
535 $box_labels = $this->setting_boxes();
536
537 // Group fields by box
538 foreach ( $fields as $field ) {
539 $boxes[ $field[ 'box' ] ][] = $field;
540 }
541
542 echo '<div class="helpdocs-settings-grid">';
543 foreach ( $boxes as $box_id => $fields ) {
544 echo '<div class="helpdocs-settings-box">';
545 echo '<div class="helpdocs-settings-header">';
546 echo '<h2>' . esc_html( $box_labels[ $box_id ] ?? 'Unknown Box' ) . '</h2>';
547 echo '</div>';
548 echo '<div class="helpdocs-settings-body">';
549
550 $in_color_group = false;
551 foreach ( $fields as $index => $field ) {
552 $is_color = ( $field[ 'type' ] === 'color' );
553 $render_fn = "render_field_{$field[ 'type' ]}";
554
555 if ( $is_color && ! $in_color_group ) {
556 echo '<div class="helpdocs-color-grid">';
557 $in_color_group = true;
558 }
559
560 if ( ! $is_color && $in_color_group ) {
561 echo '</div>';
562 $in_color_group = false;
563 }
564
565 if ( method_exists( $this, $render_fn ) ) {
566 $this->{$render_fn}( $field );
567 }
568
569 $is_last = ( $index === array_key_last( $fields ) );
570
571 if ( $is_last && $in_color_group ) {
572 echo '</div>';
573 $in_color_group = false;
574 }
575 }
576 echo '</div></div>';
577 }
578 echo '</div>';
579 } // End render_tab()
580
581
582 /**
583 * Get the value of a field, falling back to the default if not set
584 *
585 * @param string $field_name The name of the field
586 * @return mixed The value of the field or its default
587 */
588 public static function get_field_value( $field_name ) {
589 $option = get_option( "helpdocs_{$field_name}", null );
590
591 if ( null !== $option ) {
592 return $option;
593 }
594
595 $all_fields = self::setting_fields();
596 foreach ( $all_fields as $field ) {
597 if ( $field[ 'name' ] === $field_name ) {
598 return $field[ 'default' ] ?? '';
599 }
600 }
601
602 return '';
603 } // End get_field_value()
604
605
606 /**
607 * Sanitize a field value based on the field's specified sanitize callback
608 *
609 * @param array $field The field definition array
610 * @param mixed $value The value to sanitize
611 * @return mixed The sanitized value
612 */
613 public function sanitize_field( $field, $value ) {
614 $sanitize_callback = $field[ 'sanitize' ] ?? null;
615
616 if ( $sanitize_callback && is_callable( [ $this, $sanitize_callback ] ) ) {
617 return $this->$sanitize_callback( $value );
618 } elseif ( is_callable( $sanitize_callback ) ) {
619 return $sanitize_callback( $value );
620 } else {
621 return sanitize_text_field( $value );
622 }
623 } // End sanitize_field()
624
625
626 /**
627 * Render a field label with optional description tooltip
628 *
629 * @param array $field The field definition array
630 * @param string $for Optional ID of the associated input for the label's "for" attribute
631 */
632 public function render_field_label( $field, $for = '' ) {
633 ?>
634 <label <?php echo $for ? 'for="' . esc_attr( $for ) . '"' : ''; ?> class="helpdocs-field-label">
635 <?php echo wp_kses_post( $field[ 'label' ] ); ?>
636
637 <?php if ( ! empty( $field[ 'required' ] ) ) : ?>
638 <span class="helpdocs-required-field" title="<?php esc_attr_e( 'Required', 'admin-help-docs' ); ?>">*</span>
639 <?php endif; ?>
640
641 <?php if ( ! empty( $field[ 'desc' ] ) ) : ?>
642 <span class="helpdocs-tooltip">
643 <span class="dashicons dashicons-editor-help"></span>
644 <span class="helpdocs-tooltip-text"><?php echo wp_kses_post( $field[ 'desc' ] ); ?></span>
645 </span>
646 <?php endif; ?>
647 </label>
648 <?php
649 } // End render_field_label()
650
651
652 /**
653 * Render a text field
654 *
655 * @param array $field The field definition array
656 */
657 public function render_field_text( $field ) {
658 $value = $this->get_field_value( $field[ 'name' ] );
659
660 // If the value is empty/null, use the default if provided
661 if ( ( is_null( $value ) || $value === '' ) && isset( $field[ 'default' ] ) ) {
662 $value = $field[ 'default' ];
663 }
664
665 $value = $this->sanitize_field( $field, $value );
666
667 $conditional_class = '';
668 $data_condition_field = '';
669 $data_condition_value = '';
670
671 if ( ! empty( $field[ 'condition' ] ) ) {
672 $condition_field = key( $field[ 'condition' ] );
673 $expected_value = current( $field[ 'condition' ] );
674 $target_value = $this->get_field_value( $condition_field );
675
676 if ( $target_value != $expected_value ) {
677 $conditional_class = ' condition-hide';
678 }
679
680 $data_condition_field = $condition_field;
681 $data_condition_value = $expected_value;
682 }
683
684 $required = ! empty( $field[ 'required' ] ) ? ' required' : '';
685 ?>
686 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>"
687 class="helpdocs-field<?php echo esc_attr( $conditional_class ); ?>"
688 <?php if ( $data_condition_field && $data_condition_value ) : ?>
689 data-condition-field="<?php echo esc_attr( $data_condition_field ); ?>"
690 data-condition-value="<?php echo esc_attr( $data_condition_value ); ?>"
691 <?php endif; ?>>
692 <?php $this->render_field_label( $field, $field[ 'name' ] ); ?>
693 <input type="text"
694 id="<?php echo esc_attr( $field[ 'name' ] ); ?>"
695 name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>"
696 value="<?php echo esc_attr( $value ); ?>"
697 <?php echo esc_attr( $required ); ?>
698 placeholder="<?php echo isset( $field[ 'default' ] ) ? esc_attr( $field[ 'default' ] ) : ''; ?>">
699 </div>
700 <?php
701 } // End render_field_text()
702
703
704 /**
705 * Render a textarea field
706 *
707 * @param array $field The field definition array
708 */
709 public function render_field_textarea( $field ) {
710 $value = $this->get_field_value( $field[ 'name' ] );
711 $value = $this->sanitize_field( $field, $value );
712
713 $conditional_class = '';
714 $data_condition_field = '';
715 $data_condition_value = '';
716
717 if ( ! empty( $field[ 'condition' ] ) ) {
718 $condition_field = key( $field[ 'condition' ] );
719 $expected_value = current( $field[ 'condition' ] );
720 $target_value = $this->get_field_value( $condition_field );
721
722 if ( $target_value != $expected_value ) {
723 $conditional_class = ' condition-hide';
724 }
725
726 $data_condition_field = $condition_field;
727 $data_condition_value = $expected_value;
728 }
729
730 $required = ! empty( $field[ 'required' ] ) ? ' required' : '';
731
732 $field_id = str_starts_with( $field[ 'name' ], 'footer' ) ? 'helpdocs_' . $field[ 'name' ] : $field[ 'name' ];
733 ?>
734 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>"
735 class="helpdocs-field<?php echo esc_attr( $conditional_class ); ?>"
736 <?php if ( $data_condition_field && $data_condition_value ) : ?>
737 data-condition-field="<?php echo esc_attr( $data_condition_field ); ?>"
738 data-condition-value="<?php echo esc_attr( $data_condition_value ); ?>"
739 <?php endif; ?>>
740 <?php $this->render_field_label( $field, $field[ 'name' ] ); ?>
741 <textarea id="<?php echo esc_attr( $field_id ); ?>"
742 name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>"
743 <?php echo esc_attr( $required ); ?>
744 placeholder="<?php if ( isset( $field[ 'default' ] ) ) { echo esc_attr( $field[ 'default' ] ); } ?>"><?php echo esc_textarea( $value ); ?></textarea>
745 </div>
746 <?php
747 } // End render_field_textarea()
748
749
750 /**
751 * Render a color field
752 *
753 * @param array $field The field definition array
754 */
755 public function render_field_color( $field ) {
756 $value = Colors::get( str_replace( 'color_', '', $field[ 'name' ] ) );
757 $value = $this->sanitize_field( $field, $value );
758 ?>
759 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>" class="helpdocs-field color-picker-field">
760 <label for="<?php echo esc_attr( $field[ 'name' ] ); ?>">
761 <?php echo esc_html( $field[ 'label' ] ); ?>
762 </label>
763 <input type="color"
764 id="<?php echo esc_attr( $field[ 'name' ] ); ?>"
765 name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>"
766 value="<?php echo esc_attr( $value ); ?>">
767 </div>
768 <?php
769 } // End render_field_color()
770
771
772 /**
773 * Render a select field
774 *
775 * @param array $field The field definition array, which should include a 'choices' key with an array of options
776 */
777 public function render_field_select( $field ) {
778 $option = get_option( "helpdocs_{$field[ 'name' ]}", null );
779
780 $value = ( null === $option || '' === $option )
781 ? ( $field[ 'default' ] ?? '' )
782 : $option;
783
784 $value = $this->sanitize_field( $field, $value );
785
786 // Strip dashicons
787 $value = str_replace( 'dashicons-', '', $value );
788
789 $has_condition = isset( $field[ 'has_condition' ] ) && $field[ 'has_condition' ];
790 $conditional_class = $has_condition ? ' has-condition' : '';
791
792 $required = ! empty( $field[ 'required' ] ) ? ' required' : '';
793 ?>
794 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>" class="helpdocs-field">
795 <?php $this->render_field_label( $field, $field[ 'name' ] ); ?>
796 <select id="<?php echo esc_attr( $field[ 'name' ] ); ?>" name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>" class="<?php echo esc_attr( $conditional_class ); ?>"<?php echo esc_attr( $required ); ?>>
797 <?php
798 if ( isset( $field[ 'choices' ] ) && is_array( $field[ 'choices' ] ) ) {
799
800 foreach ( $field[ 'choices' ] as $key => $choice ) {
801
802 if ( is_array( $choice ) ) {
803 $option_value = $choice[ 'value' ] ?? '';
804 $option_label = $choice[ 'label' ] ?? '';
805 } else {
806 $option_value = is_string( $key ) ? $key : $choice;
807 $option_label = $choice;
808 }
809 ?>
810 <option value="<?php echo esc_attr( $option_value ); ?>" <?php selected( $value, $option_value ); ?>>
811 <?php echo esc_html( $option_label ); ?>
812 </option>
813 <?php
814 }
815 }
816 ?>
817 </select>
818 </div>
819 <?php
820 } // End render_field_select()
821
822
823 /**
824 * Render a checkbox field
825 *
826 * @param array $field The field definition array
827 */
828 public function render_field_checkbox( $field ) {
829 $option = get_option( "helpdocs_{$field[ 'name' ]}", null );
830
831 $value = ( null === $option )
832 ? ( $field[ 'default' ] ?? '' )
833 : $option;
834
835 $value = $this->sanitize_field( $field, $value );
836
837 $has_condition = isset( $field[ 'has_condition' ] ) && $field[ 'has_condition' ];
838 $conditional_class = $has_condition ? ' has-condition' : '';
839
840 $required = ! empty( $field[ 'required' ] ) ? ' required' : '';
841 ?>
842 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>" class="helpdocs-field helpdocs-field-checkbox<?php echo esc_attr( $conditional_class ); ?>">
843 <label class="helpdocs-checkbox-label">
844 <input type="checkbox"
845 id="<?php echo esc_attr( $field[ 'name' ] ); ?>"
846 name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>"
847 value="1" <?php checked( $value, '1' ); ?> <?php echo esc_attr( $required ); ?>>
848 <span><?php echo esc_html( $field[ 'label' ] ); ?></span>
849 <?php if ( ! empty( $field[ 'desc' ] ) ) : ?>
850 <span class="helpdocs-tooltip">
851 <span class="dashicons dashicons-editor-help"></span>
852 <span class="helpdocs-tooltip-text"><?php echo wp_kses_post( $field[ 'desc' ] ); ?></span>
853 </span>
854 <?php endif; ?>
855 </label>
856 </div>
857 <?php
858 } // End render_field_checkbox()
859
860
861 /**
862 * Sanitize checkbox
863 *
864 * @param mixed $value The value to sanitize
865 * @return string '1' if checked, '' if not
866 */
867 public function sanitize_checkbox( $value ) {
868 return ( ( $value == '1' || $value == 'yes' ) ? '1' : '' );
869 } // End sanitize_checkbox()
870
871
872 /**
873 * Render a checkboxes field
874 *
875 * @param array $field The field definition array, which should include a 'choices' key with an array of options
876 */
877 public function render_field_checkboxes( $field ) {
878 $values = (array) $this->get_field_value( $field[ 'name' ] );
879 $values = (array) $this->sanitize_field( $field, $values );
880
881 $conditional_class = '';
882 $data_condition_field = '';
883 $data_condition_value = '';
884
885 if ( ! empty( $field[ 'condition' ] ) ) {
886 $condition_field = key( $field[ 'condition' ] );
887 $expected_value = current( $field[ 'condition' ] );
888 $target_value = $this->get_field_value( $condition_field );
889
890 if ( $target_value != $expected_value ) {
891 $conditional_class = ' condition-hide';
892 }
893
894 $data_condition_field = $condition_field;
895 $data_condition_value = $expected_value;
896 }
897 ?>
898 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>"
899 class="helpdocs-field<?php echo esc_attr( $conditional_class ); ?>"
900 <?php if ( $data_condition_field && $data_condition_value ) : ?>
901 data-condition-field="<?php echo esc_attr( $data_condition_field ); ?>"
902 data-condition-value="<?php echo esc_attr( $data_condition_value ); ?>"
903 <?php endif; ?>>
904 <?php $this->render_field_label( $field, $field[ 'name' ] ); ?>
905 <div class="helpdocs-checkboxes">
906 <?php
907 if ( isset( $field[ 'choices' ] ) && is_array( $field[ 'choices' ] ) ) {
908 foreach ( $field[ 'choices' ] as $choice ) :
909 $key = is_array( $choice ) ? ( $choice[ 'value' ] ?? '' ) : ( $choice[ 0 ] ?? '' );
910 $label = is_array( $choice ) ? ( $choice[ 'label' ] ?? '' ) : ( $choice[ 1 ] ?? '' );
911 ?>
912 <label>
913 <input type="checkbox"
914 name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>[]"
915 value="<?php echo esc_attr( $key ); ?>" <?php checked( in_array( $key, $values, true ) || in_array( $key, array_keys( $values ), true ) ); ?>>
916 <?php echo esc_html( $label ); ?>
917 </label>
918 <?php
919 endforeach;
920 }
921 ?>
922 </div>
923 </div>
924 <?php
925 } // End render_field_checkboxes()
926
927
928 /**
929 * Sanitize checkboxes
930 *
931 * @param mixed $values The values to sanitize, expected to be an array of selected keys
932 * @return array An array of sanitized values that are valid choices
933 */
934 private function sanitize_checkboxes( $values ) {
935 if ( ! is_array( $values ) ) {
936 return [];
937 }
938 return array_map( 'strval', $values );
939 } // End sanitize_checkboxes()
940
941
942 /**
943 * Render a file upload field
944 *
945 * @param array $field The field definition array
946 */
947 public function render_field_file( $field ) {
948 $conditional_class = '';
949 $data_condition_field = '';
950 $data_condition_value = '';
951
952 if ( ! empty( $field[ 'condition' ] ) ) {
953 $condition_field = key( $field[ 'condition' ] );
954 $expected_value = current( $field[ 'condition' ] );
955 $target_value = $this->get_field_value( $condition_field );
956
957 if ( $target_value != $expected_value ) {
958 $conditional_class = ' condition-hide';
959 }
960
961 $data_condition_field = $condition_field;
962 $data_condition_value = $expected_value;
963 }
964
965 // Allow file types to be filterable
966 $default_types = '.jpg,.jpeg,.png,.pdf,.zip';
967 $accepted_types = apply_filters( 'helpdocs_support_file_types', $default_types, $field );
968
969 $required = ! empty( $field[ 'required' ] ) ? ' required' : '';
970 ?>
971 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>"
972 class="helpdocs-field helpdocs-field-file<?php echo esc_attr( $conditional_class ); ?>"
973 <?php if ( $data_condition_field && $data_condition_value ) : ?>
974 data-condition-field="<?php echo esc_attr( $data_condition_field ); ?>"
975 data-condition-value="<?php echo esc_attr( $data_condition_value ); ?>"
976 <?php endif; ?>>
977 <?php $this->render_field_label( $field, $field[ 'name' ] ); ?>
978 <input type="file"
979 id="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>" // Added prefix to ID
980 name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>[]"
981 accept="<?php echo esc_attr( $accepted_types ); ?>"
982 <?php echo esc_attr( $required ); ?>
983 multiple>
984 </div>
985 <?php
986 } // End render_field_file()
987
988
989 /**
990 * Sanitize and process file uploads
991 *
992 * @param string $field_name The name of the file input
993 * @return array List of local file paths for wp_mail attachments
994 */
995 public function sanitize_files( $field_name, $max_total_mb = 10 ) {
996 if ( empty( $_FILES[ "helpdocs_$field_name" ] ) ) { // phpcs:ignore
997 return [];
998 }
999
1000 $files = $_FILES[ "helpdocs_$field_name" ]; // phpcs:ignore
1001 $attachments = [];
1002 $total_size = 0;
1003 $max_total_size = $max_total_mb * 1024 * 1024; // Convert MB to bytes
1004
1005 if ( is_array( $files[ 'name' ] ) ) {
1006 // First Pass: Check total size
1007 foreach ( $files[ 'size' ] as $size ) {
1008 $total_size += $size;
1009 }
1010
1011 if ( $total_size > $max_total_size ) {
1012 wp_send_json_error( sprintf( __( 'The total size of attachments exceeds %dMB.', 'admin-help-docs' ), $max_total_mb ) );
1013 }
1014
1015 // Second Pass: Process uploads
1016 foreach ( $files[ 'name' ] as $key => $value ) {
1017 if ( $files[ 'name' ][ $key ] ) {
1018 $file = [
1019 'name' => $files[ 'name' ][ $key ],
1020 'type' => $files[ 'type' ][ $key ],
1021 'tmp_name' => $files[ 'tmp_name' ][ $key ],
1022 'error' => $files[ 'error' ][ $key ],
1023 'size' => $files[ 'size' ][ $key ],
1024 ];
1025
1026 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1027
1028 $upload_overrides = [ 'test_form' => false ];
1029 $movefile = wp_handle_upload( $file, $upload_overrides );
1030
1031 if ( $movefile && ! isset( $movefile[ 'error' ] ) ) {
1032 $attachments[] = $movefile[ 'file' ];
1033 }
1034 }
1035 }
1036 }
1037
1038 return $attachments;
1039 } // End sanitize_files()
1040
1041
1042 /**
1043 * Render an API Key field with Generate, Copy, and Clear actions
1044 * * @param array $field The field definition array
1045 */
1046 public function render_field_api_key( $field ) {
1047 $value = $this->get_field_value( $field[ 'name' ] );
1048
1049 if ( ( is_null( $value ) || $value === '' ) && isset( $field[ 'default' ] ) ) {
1050 $value = $field[ 'default' ];
1051 }
1052
1053 $value = $this->sanitize_field( $field, $value );
1054
1055 $conditional_class = '';
1056 $data_condition_field = '';
1057 $data_condition_value = '';
1058
1059 if ( ! empty( $field[ 'condition' ] ) ) {
1060 $condition_field = key( $field[ 'condition' ] );
1061 $expected_value = current( $field[ 'condition' ] );
1062 $target_value = $this->get_field_value( $condition_field );
1063
1064 if ( $target_value != $expected_value ) {
1065 $conditional_class = ' condition-hide';
1066 }
1067
1068 $data_condition_field = $condition_field;
1069 $data_condition_value = $expected_value;
1070 }
1071 ?>
1072 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>"
1073 class="helpdocs-field<?php echo esc_attr( $conditional_class ); ?>"
1074 <?php if ( $data_condition_field && $data_condition_value ) : ?>
1075 data-condition-field="<?php echo esc_attr( $data_condition_field ); ?>"
1076 data-condition-value="<?php echo esc_attr( $data_condition_value ); ?>"
1077 <?php endif; ?>>
1078
1079 <?php $this->render_field_label( $field, $field[ 'name' ] ); ?>
1080
1081 <div class="helpdocs-api-key-wrapper">
1082 <div id="helpdocs_api_key_display" class="helpdocs-api-key-box <?php echo $value ? 'has-key' : 'no-key'; ?>">
1083 <?php echo $value ? esc_html( $value ) : '<em>' . esc_html__( 'No API Key Generated', 'admin-help-docs' ) . '</em>'; ?>
1084 </div>
1085
1086 <input type="hidden"
1087 id="<?php echo esc_attr( $field[ 'name' ] ); ?>"
1088 name="helpdocs_<?php echo esc_attr( $field[ 'name' ] ); ?>"
1089 value="<?php echo esc_attr( $value ); ?>">
1090
1091 <div class="helpdocs-api-key-actions">
1092 <button type="button" class="helpdocs-button helpdocs-generate-api-key"><?php esc_html_e( 'Generate API Key', 'admin-help-docs' ); ?></button>
1093 <button type="button" class="helpdocs-button helpdocs-copy-api-key" <?php echo ! $value ? 'disabled' : ''; ?>><?php esc_html_e( 'Copy', 'admin-help-docs' ); ?></button>
1094 <button type="button" class="helpdocs-button helpdocs-clear-api-key" <?php echo ! $value ? 'disabled' : ''; ?>><?php esc_html_e( 'Clear', 'admin-help-docs' ); ?></button>
1095 </div>
1096 </div>
1097 </div>
1098 <?php
1099 } // End render_field_api_key()
1100
1101
1102 /**
1103 * Sanitize custom CSS input
1104 *
1105 * @param string $css The CSS input to sanitize
1106 * @return string The sanitized CSS
1107 */
1108 public function sanitize_custom_css( $css ) {
1109 $css = wp_unslash( $css );
1110 $css = wp_strip_all_tags( $css );
1111 return $css;
1112 } // End sanitize_custom_css()
1113
1114
1115 /**
1116 * Render an HTML field (for custom content like buttons)
1117 *
1118 * @param array $field The field definition array, which should include a 'content' key with the HTML to render
1119 */
1120 public function render_field_html( $field ) {
1121 ?>
1122 <div id="helpdocs_field_<?php echo esc_attr( $field[ 'name' ] ); ?>" class="helpdocs-field helpdocs-field-html">
1123 <label>
1124 <?php echo wp_kses_post( $field[ 'label' ] ); ?>
1125 <?php if ( ! empty( $field[ 'desc' ] ) ) : ?>
1126 <span class="helpdocs-tooltip">
1127 <span class="dashicons dashicons-editor-help"></span>
1128 <span class="helpdocs-tooltip-text"><?php echo wp_kses_post( $field[ 'desc' ] ); ?></span>
1129 </span>
1130 <?php endif; ?>
1131 </label>
1132 <div class="helpdocs-html-content">
1133 <?php
1134 $allowed_html = wp_kses_allowed_html( 'post' );
1135
1136 $allowed_html[ 'button' ] = [
1137 'type' => true,
1138 'id' => true,
1139 'class' => true,
1140 ];
1141 $allowed_html[ 'input' ] = [
1142 'type' => true,
1143 'id' => true,
1144 'name' => true,
1145 'accept' => true,
1146 'style' => true,
1147 'value' => true,
1148 'class' => true,
1149 ];
1150 $allowed_html[ 'label' ][ 'for' ] = true;
1151
1152 echo wp_kses( ( $field[ 'content' ] ?? '' ), $allowed_html );
1153 ?>
1154 </div>
1155 </div>
1156 <?php
1157 } // End render_field_html()
1158
1159
1160 /**
1161 * AJAX handler to save settings
1162 */
1163 public function ajax_save_settings() {
1164 check_ajax_referer( 'helpdocs_settings_nonce', 'nonce' );
1165 if ( ! current_user_can( Helpers::admin_role() ) ) {
1166 wp_send_json_error( 'Insufficient permissions.' );
1167 }
1168
1169 $fields = self::setting_fields();
1170
1171 $colors_to_save = [];
1172 $errors = [];
1173
1174 foreach ( $fields as $field ) {
1175 $name = $field[ 'name' ];
1176 $post_key = 'helpdocs_' . $name;
1177
1178 if ( strpos( $name, 'color_' ) === 0 ) {
1179 $key = str_replace( 'color_', '', $name );
1180 if ( isset( $_POST[ 'settings' ][ $post_key ] ) ) {
1181 $raw_value = wp_unslash( $_POST[ 'settings' ][ $post_key ] ); // phpcs:ignore
1182 $colors_to_save[ $key ] = $this->sanitize_field( $field, $raw_value );
1183 }
1184 continue;
1185 }
1186
1187 if ( isset( $_POST[ 'settings' ][ $post_key ] ) ) {
1188 // Sanitize below based on field type, but first get the raw value for checkboxes
1189 $raw_value = wp_unslash( $_POST[ 'settings' ][ $post_key ] ); // phpcs:ignore
1190 } else {
1191 // If it's a checkbox group and nothing is checked, default to empty array
1192 if ( $field[ 'type' ] === 'checkboxes' ) {
1193 $raw_value = [];
1194 } elseif ( $field[ 'type' ] === 'checkbox' ) {
1195 $raw_value = 0;
1196 } else {
1197 continue;
1198 }
1199 }
1200
1201 // Determine type and sanitize accordingly
1202 if ( $field[ 'type' ] === 'checkbox' ) {
1203 $value = $this->sanitize_checkbox( $raw_value );
1204 } elseif ( $field[ 'type' ] === 'checkboxes' ) {
1205 $value = $this->sanitize_checkboxes( (array) $raw_value );
1206 } elseif ( $field[ 'sanitize' ] === 'sanitize_custom_css' ) {
1207 $value = $this->sanitize_custom_css( $raw_value );
1208 } else {
1209 $value = $this->sanitize_field( $field, $raw_value );
1210
1211 // If text field is empty, fall back to default
1212 if ( $field[ 'type' ] === 'text' && $value === '' && isset( $field[ 'default' ] ) ) {
1213 $value = $field[ 'default' ];
1214 }
1215 }
1216
1217 $updated = update_option( $post_key, $value );
1218 if ( $updated === false && get_option( $post_key ) != $value ) {
1219 $errors[] = $name;
1220 }
1221 }
1222
1223 // Before saving all colors, update the CSS variable storage if needed
1224 Colors::convert_color_storage();
1225
1226 if ( ! empty( $colors_to_save ) ) {
1227 $updated_colors = update_option( 'helpdocs_colors', $colors_to_save );
1228 if ( $updated_colors === false && get_option( 'helpdocs_colors' ) !== $colors_to_save ) {
1229 $errors[] = 'colors';
1230 }
1231 }
1232
1233 // Flush the cache so changes show immediately
1234 Helpers::flush_permissions_cache();
1235
1236 if ( empty( $errors ) ) {
1237 wp_send_json_success();
1238 } else {
1239 wp_send_json_error( 'Failed to save: ' . implode( ', ', $errors ) );
1240 }
1241 } // End ajax_save_settings()
1242
1243
1244 /**
1245 * AJAX handler to flush system cache
1246 */
1247 public function ajax_flush_cache() {
1248 check_ajax_referer( 'helpdocs_settings_nonce', 'nonce' );
1249 if ( ! Helpers::user_can_edit() ) {
1250 wp_send_json_error( __( 'You do not have permission to perform this action.', 'admin-help-docs' ) );
1251 }
1252
1253 Helpers::flush_location_cache();
1254
1255 wp_send_json_success( __( 'Cache cleared successfully.', 'admin-help-docs' ) );
1256 } // End ajax_flush_cache()
1257
1258
1259 /**
1260 * Prevent cloning and unserializing
1261 */
1262 public function __clone() {}
1263 public function __wakeup() {}
1264
1265 }
1266
1267
1268 Settings::instance();