|null */ private static $settings = null; /** * Runtime cache for the defaults. * * @since 0.1 * @var array|null */ private static $defaults = null; /** * Returns the setting schema. * * Each entry supports: `type`, `default`, `min`, `max`, `step` and `choices`. * * @since 0.1 * @return array> */ public static function schema() { $schema = array( // Query. 'post_type' => array( 'type' => 'post_type', 'default' => 'post', ), 'posts_per_page' => array( 'type' => 'int', 'default' => 6, 'min' => 1, 'max' => 48, ), 'orderby' => array( 'type' => 'choice', 'default' => 'date', 'choices' => array( 'date', 'modified', 'title', 'menu_order', 'comment_count', 'rand', 'ID' ), ), 'order' => array( 'type' => 'choice', 'default' => 'DESC', 'choices' => array( 'ASC', 'DESC' ), ), 'pagination' => array( 'type' => 'choice', 'default' => 'none', 'choices' => array( 'none', 'numbers', 'prev_next', 'load_more' ), ), // Layout. 'columns' => array( 'type' => 'int', 'default' => 3, 'min' => 1, 'max' => 6, ), 'columns_tablet' => array( 'type' => 'int', 'default' => 2, 'min' => 1, 'max' => 4, ), 'columns_mobile' => array( 'type' => 'int', 'default' => 1, 'min' => 1, 'max' => 2, ), 'gap' => array( 'type' => 'int', 'default' => 28, 'min' => 0, 'max' => 80, ), 'image_size' => array( 'type' => 'image_size', 'default' => 'large', ), 'image_ratio' => array( 'type' => 'choice', 'default' => '16-10', 'choices' => array( '16-9', '16-10', '3-2', '4-3', '1-1', '3-4', 'auto' ), ), // Card elements. 'show_image' => array( 'type' => 'bool', 'default' => 1, ), 'image_fallback' => array( 'type' => 'bool', 'default' => 1, ), 'show_category' => array( 'type' => 'bool', 'default' => 1, ), 'show_title' => array( 'type' => 'bool', 'default' => 1, ), 'show_excerpt' => array( 'type' => 'bool', 'default' => 1, ), 'show_date' => array( 'type' => 'bool', 'default' => 1, ), 'show_author' => array( 'type' => 'bool', 'default' => 0, ), 'show_read_more' => array( 'type' => 'bool', 'default' => 1, ), // Card text. 'title_tag' => array( 'type' => 'choice', 'default' => 'h3', 'choices' => array( 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div' ), ), 'excerpt_length' => array( 'type' => 'int', 'default' => 20, 'min' => 0, 'max' => 100, ), 'read_more_text' => array( 'type' => 'text', 'default' => 'Read more', ), // Card styling. 'card_layout' => array( 'type' => 'choice', 'default' => 'stacked', 'choices' => array( 'stacked', 'horizontal', 'overlay' ), ), 'card_bg' => array( 'type' => 'color', 'default' => '#ffffff', 'allow_empty' => true, ), 'card_border_color' => array( 'type' => 'color', 'default' => '#e6e6ef', ), 'card_border_width' => array( 'type' => 'int', 'default' => 1, 'min' => 0, 'max' => 4, ), 'card_radius' => array( 'type' => 'int', 'default' => 14, 'min' => 0, 'max' => 40, ), 'card_padding' => array( 'type' => 'int', 'default' => 22, 'min' => 0, 'max' => 48, ), 'card_shadow' => array( 'type' => 'choice', 'default' => 'soft', 'choices' => array( 'none', 'soft', 'medium', 'strong' ), ), 'hover_effect' => array( 'type' => 'choice', 'default' => 'lift', 'choices' => array( 'lift', 'shadow', 'border', 'none' ), ), 'card_border_hover' => array( 'type' => 'color', 'default' => '', 'allow_empty' => true, ), 'text_align' => array( 'type' => 'choice', 'default' => 'left', 'choices' => array( 'left', 'center' ), ), 'content_gap' => array( 'type' => 'int', 'default' => 10, 'min' => 0, 'max' => 32, ), // Image styling. 'image_radius' => array( 'type' => 'int', 'default' => 0, 'min' => 0, 'max' => 40, ), 'image_hover' => array( 'type' => 'choice', 'default' => 'zoom', 'choices' => array( 'zoom', 'none', 'grayscale' ), ), 'media_width' => array( 'type' => 'int', 'default' => 40, 'min' => 25, 'max' => 60, ), 'overlay_opacity' => array( 'type' => 'int', 'default' => 70, 'min' => 50, 'max' => 95, ), // Typography and colours. 'title_color' => array( 'type' => 'color', 'default' => '#17172b', ), 'title_hover_color' => array( 'type' => 'color', 'default' => '#4f46e5', ), 'title_size' => array( 'type' => 'int', 'default' => 19, 'min' => 12, 'max' => 40, ), 'title_weight' => array( 'type' => 'choice', 'default' => '600', 'choices' => array( '400', '500', '600', '700', '800' ), ), 'title_line_height' => array( 'type' => 'int', 'default' => 135, 'min' => 100, 'max' => 200, ), 'title_lines' => array( 'type' => 'int', 'default' => 0, 'min' => 0, 'max' => 5, ), 'meta_color' => array( 'type' => 'color', 'default' => '#6b6b80', ), 'meta_size' => array( 'type' => 'int', 'default' => 13, 'min' => 10, 'max' => 20, ), 'excerpt_color' => array( 'type' => 'color', 'default' => '#4a4a5f', ), 'excerpt_size' => array( 'type' => 'int', 'default' => 15, 'min' => 12, 'max' => 24, ), 'excerpt_line_height' => array( 'type' => 'int', 'default' => 160, 'min' => 120, 'max' => 220, ), 'excerpt_lines' => array( 'type' => 'int', 'default' => 0, 'min' => 0, 'max' => 8, ), // Category labels. 'term_style' => array( 'type' => 'choice', 'default' => 'text', 'choices' => array( 'text', 'badge' ), ), 'term_color' => array( 'type' => 'color', 'default' => '', 'allow_empty' => true, ), // Links and read more. 'accent_color' => array( 'type' => 'color', 'default' => '#4f46e5', ), 'button_style' => array( 'type' => 'choice', 'default' => 'link', 'choices' => array( 'link', 'button', 'outline' ), ), 'button_size' => array( 'type' => 'choice', 'default' => 'medium', 'choices' => array( 'small', 'medium', 'large' ), ), 'button_bg' => array( 'type' => 'color', 'default' => '#4f46e5', ), 'button_bg_hover' => array( 'type' => 'color', 'default' => '#4338ca', ), 'button_color' => array( 'type' => 'color', 'default' => '#ffffff', ), 'button_radius' => array( 'type' => 'int', 'default' => 8, 'min' => 0, 'max' => 40, ), 'pagination_align' => array( 'type' => 'choice', 'default' => 'center', 'choices' => array( 'left', 'center', 'right' ), ), // Advanced. 'disable_css' => array( 'type' => 'bool', 'default' => 0, ), 'delete_data' => array( 'type' => 'bool', 'default' => 0, ), ); /** * Filters the setting schema. * * Use this to add a setting (types: bool, int, text, color, choice, * post_type, image_size). Added keys are sanitized like core ones. A * colour field may set 'allow_empty' => true to accept an empty value. * * @since 0.1 * * @param array> $schema Setting schema. */ return apply_filters( 'mmpg_settings_schema', $schema ); } /** * Returns the default value for every setting. * * @since 0.1 * @return array */ public static function defaults() { if ( null !== self::$defaults ) { return self::$defaults; } $defaults = array(); foreach ( self::schema() as $key => $field ) { $defaults[ $key ] = isset( $field['default'] ) ? $field['default'] : ''; } self::$defaults = $defaults; return self::$defaults; } /** * Returns all settings, stored values merged over the defaults. * * @since 0.1 * @return array */ public static function all() { if ( null !== self::$settings ) { return self::$settings; } $stored = get_option( self::OPTION, array() ); $stored = is_array( $stored ) ? $stored : array(); $defaults = self::defaults(); self::$settings = array_merge( $defaults, array_intersect_key( $stored, $defaults ) ); return self::$settings; } /** * Returns a single setting. * * @since 0.1 * * @param string $key Setting key. * @return mixed Setting value, or null when the key is unknown. */ public static function get( $key ) { $settings = self::all(); return array_key_exists( $key, $settings ) ? $settings[ $key ] : null; } /** * Clears the runtime cache. * * @since 0.1 * @return void */ public static function flush_cache() { self::$settings = null; self::$defaults = null; } /** * Sanitizes the submitted settings. * * Only the keys present in the request are touched, so saving one tab never * wipes the values of another. Unchecked checkboxes are still submitted as * `0` through a paired hidden input. * * @since 0.1 * * @param mixed $input Raw `$_POST` value for the option. * @return array */ public static function sanitize( $input ) { $current = self::all(); if ( ! is_array( $input ) ) { return $current; } $output = $current; foreach ( self::schema() as $key => $field ) { if ( ! array_key_exists( $key, $input ) ) { continue; } $output[ $key ] = self::sanitize_value( $input[ $key ], $field ); } // The cache is deliberately NOT primed here. This runs as the // sanitize_option callback, before anything is persisted, so if the // write is vetoed or fails the rest of the request would read values // that were never saved. Plugin::init() flushes the cache on // update_option_mmpg_settings instead, once the save is real. return $output; } /** * Sanitizes one value against its schema entry. * * @since 0.1 * * @param mixed $value Raw value. * @param array $field Schema entry. * @return mixed */ public static function sanitize_value( $value, array $field ) { $type = isset( $field['type'] ) ? $field['type'] : 'text'; $default = isset( $field['default'] ) ? $field['default'] : ''; switch ( $type ) { case 'bool': return Helpers::to_bool( $value ) ? 1 : 0; case 'int': $min = isset( $field['min'] ) ? $field['min'] : 0; $max = isset( $field['max'] ) ? $field['max'] : 9999; return is_numeric( $value ) ? Helpers::clamp_int( $value, $min, $max ) : $default; case 'color': $raw = is_string( $value ) ? trim( $value ) : ''; // Some colours may be left empty on purpose: a transparent card, // or a category colour that simply follows the accent colour. if ( '' === $raw && ! empty( $field['allow_empty'] ) ) { return ''; } $color = sanitize_hex_color( $raw ); return $color ? $color : $default; case 'choice': $choices = isset( $field['choices'] ) ? array_map( 'strval', (array) $field['choices'] ) : array(); $value = is_scalar( $value ) ? (string) $value : ''; // Compared as strings, so 700 and '700' are the same choice // whether they come from a form, a shortcode or a filter. return in_array( $value, $choices, true ) ? $value : $default; case 'post_type': $post_type = sanitize_key( $value ); return post_type_exists( $post_type ) && is_post_type_viewable( $post_type ) ? $post_type : $default; case 'image_size': $size = sanitize_key( $value ); return in_array( $size, self::image_sizes(), true ) ? $size : $default; case 'text': default: return sanitize_text_field( (string) $value ); } } /** * Returns the registered image size names, including `full`. * * @since 0.1 * @return string[] */ public static function image_sizes() { $sizes = get_intermediate_image_sizes(); $sizes = is_array( $sizes ) ? $sizes : array(); $sizes[] = 'full'; return array_values( array_unique( $sizes ) ); } }