[ module keys ] */ public static function module_groups() { return array( 'text' => array( 'text_size', 'line_height', 'letter_spacing', 'readable_font', 'dyslexia_font', 'align_left' ), 'colour' => array( 'contrast_dark', 'contrast_light', 'monochrome', 'low_saturation', 'highlight_links', 'highlight_headings' ), 'focus' => array( 'stop_motion', 'hide_images', 'big_cursor', 'reading_guide', 'reading_mask', 'focus_ring', 'read_aloud' ), ); } /** * Flat list of all module keys. * * @return string[] */ public static function all_modules() { return array_merge( ...array_values( self::module_groups() ) ); } /** * Default settings. * * @return array */ public static function defaults() { return array( 'toolbar' => array( 'enabled' => true, 'position' => 'bottom-right', 'color' => '#1d4ed8', 'icon' => 'person', 'size' => 'm', 'hide_mobile' => false, 'modules' => self::all_modules(), 'profiles' => true, 'shortcut' => true, 'statement_url' => '', 'offset_x' => 20, 'offset_y' => 20, ), 'fixes' => array( 'skip_link' => true, 'skip_target' => '', 'focus_outline' => true, 'focus_color' => '#1d4ed8', 'form_labels' => true, 'zoom' => true, 'new_window' => 'warn', 'strip_titles' => true, 'tabindex' => true, 'read_more' => true, 'media_alt' => true, 'lang_dir' => true, 'empty_search' => true, 'underline_links' => false, 'reduced_motion' => false, ), 'checker' => array( 'post_types' => array( 'post', 'page' ), 'scan_on_save' => true, 'standard' => 'no-private', 'ambiguous_extra' => '', ), 'general' => array( 'delete_on_uninstall' => false, ), ); } /** * All settings merged over the defaults. * * @return array */ public static function all() { if ( null === self::$cache ) { $saved = get_option( self::OPTION, array() ); self::$cache = self::merge( self::defaults(), is_array( $saved ) ? $saved : array() ); } return self::$cache; } /** * One area or one value. * * @param string $area Area key. * @param string|null $key Optional key inside the area. * @return mixed */ public static function get( $area, $key = null ) { $all = self::all(); if ( ! isset( $all[ $area ] ) ) { return null; } if ( null === $key ) { return $all[ $area ]; } return isset( $all[ $area ][ $key ] ) ? $all[ $area ][ $key ] : null; } /** * Save one area after sanitising it. * * @param string $area Area key. * @param array $input Raw input. */ public static function save_area( $area, $input ) { $all = self::all(); $all[ $area ] = self::sanitize_area( $area, is_array( $input ) ? $input : array() ); update_option( self::OPTION, $all ); self::$cache = null; } /** * Replace everything (used by import) after sanitising. * * @param array $input Raw settings. */ public static function save_all( $input ) { $clean = array(); foreach ( array_keys( self::defaults() ) as $area ) { $clean[ $area ] = self::sanitize_area( $area, isset( $input[ $area ] ) && is_array( $input[ $area ] ) ? $input[ $area ] : array() ); } update_option( self::OPTION, $clean ); self::$cache = null; } /** * Forget the per-request cache (tests, imports). */ public static function flush() { self::$cache = null; } /** * Sanitise one area. Checkboxes missing from the input are false. * * @param string $area Area key. * @param array $input Raw input. * @return array */ public static function sanitize_area( $area, $input ) { $defaults = self::defaults(); $out = array(); if ( ! isset( $defaults[ $area ] ) ) { return $out; } foreach ( $defaults[ $area ] as $key => $default ) { $value = isset( $input[ $key ] ) ? $input[ $key ] : null; if ( is_bool( $default ) ) { $out[ $key ] = ! empty( $value ); } elseif ( is_int( $default ) ) { $out[ $key ] = null === $value ? $default : max( 0, min( 200, (int) $value ) ); } elseif ( is_array( $default ) ) { $out[ $key ] = is_array( $value ) ? array_values( array_map( 'sanitize_key', $value ) ) : array(); } else { $out[ $key ] = null === $value ? $default : self::sanitize_string( $area, $key, (string) $value, $default ); } } if ( 'toolbar' === $area ) { $out['modules'] = array_values( array_intersect( $out['modules'], self::all_modules() ) ); } if ( 'checker' === $area ) { $types = get_post_types( array( 'public' => true ) ); $out['post_types'] = array_values( array_intersect( $out['post_types'], array_keys( $types ) ) ); } return $out; } /** * Sanitise a string setting against its allowed values. * * @param string $area Area. * @param string $key Key. * @param string $value Raw value. * @param string $default Default. * @return string */ private static function sanitize_string( $area, $key, $value, $default ) { $choices = array( 'toolbar.position' => array( 'bottom-right', 'bottom-left', 'top-right', 'top-left', 'middle-right', 'middle-left' ), 'toolbar.icon' => array( 'person', 'eye', 'sliders' ), 'toolbar.size' => array( 's', 'm', 'l' ), 'fixes.new_window' => array( 'warn', 'remove', 'off' ), 'checker.standard' => array( 'no-private', 'no-public', 'wcag21aa', 'wcag22aa' ), ); $id = $area . '.' . $key; if ( isset( $choices[ $id ] ) ) { return in_array( $value, $choices[ $id ], true ) ? $value : $default; } if ( in_array( $id, array( 'toolbar.color', 'fixes.focus_color' ), true ) ) { $color = sanitize_hex_color( $value ); return $color ? $color : $default; } if ( 'toolbar.statement_url' === $id ) { return esc_url_raw( $value ); } if ( 'fixes.skip_target' === $id ) { // A simple CSS selector: #id, .class, tag, [role=main]. $value = trim( $value ); return preg_match( '/^[#.\w\-\[\]=" \']{0,80}$/', $value ) ? $value : ''; } if ( 'checker.ambiguous_extra' === $id ) { return sanitize_textarea_field( $value ); } return sanitize_text_field( $value ); } /** * Recursive merge where saved values win and lists are replaced, not merged. * * @param array $defaults Defaults. * @param array $saved Saved. * @return array */ private static function merge( $defaults, $saved ) { foreach ( $defaults as $key => $value ) { if ( ! array_key_exists( $key, $saved ) ) { continue; } if ( is_array( $value ) && self::is_assoc( $value ) && is_array( $saved[ $key ] ) ) { $defaults[ $key ] = self::merge( $value, $saved[ $key ] ); } else { $defaults[ $key ] = $saved[ $key ]; } } return $defaults; } /** * Whether an array has string keys. * * @param array $arr Array. * @return bool */ private static function is_assoc( $arr ) { return array() !== $arr && array_keys( $arr ) !== range( 0, count( $arr ) - 1 ); } }