*/ public static function shortcode_defaults() { $settings = Settings::all(); $defaults = array( // Query. 'post_type' => $settings['post_type'], 'posts_per_page' => $settings['posts_per_page'], 'offset' => 0, 'category' => '', 'categories' => '', 'tag' => '', 'taxonomy' => '', 'terms' => '', 'author' => '', 'exclude' => '', 'order' => $settings['order'], 'orderby' => $settings['orderby'], 'ignore_sticky' => 'yes', 'pagination' => $settings['pagination'], // Layout. 'columns' => $settings['columns'], 'columns_tablet' => $settings['columns_tablet'], 'columns_mobile' => $settings['columns_mobile'], 'gap' => $settings['gap'], 'image_size' => $settings['image_size'], 'image_ratio' => $settings['image_ratio'], // Content. 'show_image' => $settings['show_image'], 'image_fallback' => $settings['image_fallback'], 'show_category' => $settings['show_category'], 'show_title' => $settings['show_title'], 'show_excerpt' => $settings['show_excerpt'], 'show_date' => $settings['show_date'], 'show_author' => $settings['show_author'], 'show_read_more' => $settings['show_read_more'], 'title_tag' => $settings['title_tag'], 'excerpt_length' => $settings['excerpt_length'], 'read_more_text' => $settings['read_more_text'], // Style modifiers. These switch classes or per grid custom // properties, so one grid can differ from the site wide look. 'card_layout' => $settings['card_layout'], 'hover_effect' => $settings['hover_effect'], 'text_align' => $settings['text_align'], 'image_hover' => $settings['image_hover'], 'term_style' => $settings['term_style'], 'button_style' => $settings['button_style'], 'button_size' => $settings['button_size'], 'pagination_align' => $settings['pagination_align'], 'title_lines' => $settings['title_lines'], 'excerpt_lines' => $settings['excerpt_lines'], // Markup. 'class' => '', 'id' => '', ); /** * Filters the shortcode attribute defaults. * * @since 0.1 * * @param array $defaults Attribute defaults. */ return apply_filters( 'mmpg_shortcode_defaults', $defaults ); } /** * Builds normalized args from shortcode attributes. * * @since 0.1 * * @param array $atts Raw shortcode attributes. * @return array */ public static function from_shortcode( $atts ) { $atts = shortcode_atts( self::shortcode_defaults(), is_array( $atts ) ? $atts : array(), Plugin::SHORTCODE ); $args = array( 'post_type' => Helpers::to_list( $atts['post_type'] ), 'offset' => absint( $atts['offset'] ), 'exclude' => Helpers::to_id_list( $atts['exclude'] ), 'author' => Helpers::to_id_list( $atts['author'] ), 'tax' => self::tax_clauses( $atts ), 'class' => Helpers::to_class_list( $atts['class'] ), 'id' => sanitize_html_class( (string) $atts['id'] ), ); foreach ( self::SCHEMA_KEYS as $key ) { $args[ $key ] = $atts[ $key ]; } foreach ( self::BOOL_KEYS as $key ) { if ( isset( $atts[ $key ] ) ) { $args[ $key ] = $atts[ $key ]; } } return self::normalize( $args ); } /** * Builds normalized args from an untrusted REST payload. * * @since 0.1 * * @param array $payload Decoded payload. * @return array */ public static function from_request( $payload ) { $payload = is_array( $payload ) ? $payload : array(); $args = self::normalize( $payload, false ); // The endpoint only ever serves additional pages for a load more grid. $args['pagination'] = 'load_more'; return $args; } /** * Validates and completes an arg array. * * @since 0.1 * * @param array $args Partial args in the normalized shape. * @param bool $trusted Whether the input came from a shortcode. * @return array */ public static function normalize( array $args, $trusted = true ) { $schema = Settings::schema(); $out = array(); // Values that share a name and rules with a setting. foreach ( self::SCHEMA_KEYS as $key ) { $field = isset( $schema[ $key ] ) ? $schema[ $key ] : array( 'type' => 'text' ); $value = array_key_exists( $key, $args ) ? $args[ $key ] : Settings::get( $key ); $out[ $key ] = Settings::sanitize_value( $value, $field ); } foreach ( self::BOOL_KEYS as $key ) { if ( array_key_exists( $key, $args ) ) { $out[ $key ] = Helpers::to_bool( $args[ $key ] ) ? 1 : 0; } elseif ( isset( $schema[ $key ] ) ) { $out[ $key ] = (int) Settings::get( $key ); } else { $out[ $key ] = 1; } } $out['posts_per_page'] = Helpers::clamp_int( $out['posts_per_page'], 1, self::MAX_POSTS ); $out['offset'] = isset( $args['offset'] ) ? absint( $args['offset'] ) : 0; $out['exclude'] = isset( $args['exclude'] ) ? Helpers::to_id_list( $args['exclude'] ) : array(); $out['author'] = isset( $args['author'] ) ? Helpers::to_id_list( $args['author'] ) : array(); $out['post_type'] = self::post_types( isset( $args['post_type'] ) ? $args['post_type'] : array() ); $out['tax'] = self::normalize_tax( isset( $args['tax'] ) ? $args['tax'] : array(), $trusted ); $out['class'] = isset( $args['class'] ) ? Helpers::to_class_list( $args['class'] ) : ''; $out['id'] = isset( $args['id'] ) ? sanitize_html_class( (string) $args['id'] ) : ''; return $out; } /** * Returns the payload that the load more button sends back to the REST route. * * @since 0.1 * * @param array $args Normalized args. * @return array */ public static function to_payload( array $args ) { unset( $args['class'], $args['id'], $args['pagination'] ); return $args; } /** * Keeps only post types that exist and are publicly viewable. * * @since 0.1 * * @param mixed $value Raw post type value. * @return string[] */ private static function post_types( $value ) { $types = array(); foreach ( Helpers::to_list( $value ) as $type ) { $type = sanitize_key( $type ); if ( post_type_exists( $type ) && is_post_type_viewable( $type ) ) { $types[] = $type; } } if ( empty( $types ) ) { $fallback = (string) Settings::get( 'post_type' ); $types[] = is_post_type_viewable( $fallback ) ? $fallback : 'post'; } return array_values( array_unique( $types ) ); } /** * Builds taxonomy clauses from the `category`, `tag` and `taxonomy` attributes. * * @since 0.1 * * @param array $atts Shortcode attributes. * @return array> */ private static function tax_clauses( array $atts ) { $clauses = array(); $categories = Helpers::to_list( $atts['category'] ); $categories = array_merge( $categories, Helpers::to_list( $atts['categories'] ) ); if ( ! empty( $categories ) ) { $clauses[] = array( 'taxonomy' => 'category', 'terms' => $categories, ); } $tags = Helpers::to_list( $atts['tag'] ); if ( ! empty( $tags ) ) { $clauses[] = array( 'taxonomy' => 'post_tag', 'terms' => $tags, ); } $taxonomy = sanitize_key( $atts['taxonomy'] ); $terms = Helpers::to_list( $atts['terms'] ); if ( '' !== $taxonomy && ! empty( $terms ) ) { $clauses[] = array( 'taxonomy' => $taxonomy, 'terms' => $terms, ); } return $clauses; } /** * Validates taxonomy clauses. * * Untrusted input is additionally restricted to public taxonomies. * * @since 0.1 * * @param mixed $clauses Raw clauses. * @param bool $trusted Whether the input came from a shortcode. * @return array> */ private static function normalize_tax( $clauses, $trusted ) { if ( ! is_array( $clauses ) ) { return array(); } $out = array(); foreach ( $clauses as $clause ) { if ( ! is_array( $clause ) || empty( $clause['taxonomy'] ) || empty( $clause['terms'] ) ) { continue; } $taxonomy = sanitize_key( $clause['taxonomy'] ); if ( ! taxonomy_exists( $taxonomy ) ) { continue; } if ( ! $trusted ) { $object = get_taxonomy( $taxonomy ); if ( ! $object || empty( $object->public ) ) { continue; } } $terms = array(); foreach ( Helpers::to_list( $clause['terms'] ) as $term ) { $terms[] = is_numeric( $term ) ? absint( $term ) : sanitize_title( $term ); } $terms = array_values( array_filter( $terms ) ); if ( empty( $terms ) ) { continue; } $out[] = array( 'taxonomy' => $taxonomy, 'terms' => $terms, ); } return $out; } }