get_defaults(), $atts, 'aiopod_podcasts' ); $attributes = $this->resolve_attributes( $attributes ); // Enqueue style dependencies wp_enqueue_style( AIOPOD_PLUGIN_SLUG . '-public' ); wp_enqueue_script( AIOPOD_PLUGIN_SLUG . '-public' ); // Process output $content = $this->get_content( $attributes ); return $content; } /** * Load podcasts using Ajax. * * @since 1.0.0 */ public function ajax_callback_load_podcasts() { check_ajax_referer( 'aiopod_ajax_nonce', 'security' ); $attributes = $this->sanitize_ajax_attributes(); $content = $this->get_content( $attributes ); wp_send_json_success( array( 'html' => $content ) ); } /** * Return the allowed $_POST keys and their sanitization types for AJAX requests. * * Derived from aiopod_get_shortcode_fields() so premium-added fields are * automatically included. Internal/pagination state fields are appended below. * * @since 1.0.0 * @return array Map of attribute key to sanitization type string. */ private function get_allowed_attributes() { $type_map = array( 'checkbox' => 'bool', 'number' => 'int', 'select' => 'key', 'text' => 'text', 'podcasts' => 'int', 'categories' => 'array_int', 'participants' => 'array_int', 'tags' => 'array_int', ); $fields = aiopod_get_shortcode_fields(); $allowed = array(); foreach ( $fields['podcasts']['sections'] as $section ) { foreach ( $section['fields'] as $field ) { if ( ! isset( $type_map[ $field['type'] ] ) ) { continue; } $allowed[ $field['name'] ] = $type_map[ $field['type'] ]; } } // Internal / pagination state — not defined in aiopod_get_shortcode_fields() $allowed['source'] = 'key'; $allowed['title'] = 'text'; $allowed['count'] = 'int'; $allowed['paged'] = 'int'; $allowed['max_num_pages'] = 'int'; $allowed['include'] = 'text'; $allowed['exclude'] = 'text'; $allowed['hide_if_empty'] = 'int'; return $allowed; } /** * Sanitize AJAX $_POST attributes against the allowed list. * * @since 1.0.0 * @return array Sanitized attributes. */ private function sanitize_ajax_attributes() { $attributes = array(); foreach ( $this->get_allowed_attributes() as $key => $type ) { if ( ! isset( $_POST[ $key ] ) ) { continue; } switch ( $type ) { case 'int': $attributes[ $key ] = (int) wp_unslash( $_POST[ $key ] ); break; case 'bool': $raw = wp_unslash( $_POST[ $key ] ); if ( 'false' === $raw ) { $attributes[ $key ] = 0; } elseif ( 'true' === $raw ) { $attributes[ $key ] = 1; } else { $attributes[ $key ] = (int) $raw; } break; case 'key': $attributes[ $key ] = sanitize_key( wp_unslash( $_POST[ $key ] ) ); break; case 'text': $attributes[ $key ] = sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); break; case 'array_int': $attributes[ $key ] = array_map( 'intval', wp_unslash( (array) $_POST[ $key ] ) ); break; } } return $attributes; } /** * Get the html output. * * @since 1.0.0 * @param array $attributes An associative array of attributes. * @return string $content HTML output. */ public function get_content( $attributes ) { // Define the query $args = array( 'post_type' => 'aiopod_podcasts', 'posts_per_page' => ! empty( $attributes['limit'] ) ? (int) $attributes['limit'] : 100, 'post_status' => array( 'publish' ) ); if ( 'none' !== $attributes['pagination'] ) { // Pagination $args['paged'] = (int) $attributes['paged']; } if ( isset( $attributes['include'] ) ) { // Include podcast IDs $include = is_array( $attributes['include'] ) ? array_map( 'intval', $attributes['include'] ) : array_map( 'intval', explode( ',', $attributes['include'] ) ); $include = array_filter( $include ); if ( ! empty( $include ) ) { $args['post__in'] = $include; } } if ( isset( $attributes['exclude'] ) ) { // Exclude podcast IDs $exclude = is_array( $attributes['exclude'] ) ? array_map( 'intval', $attributes['exclude'] ) : array_map( 'intval', explode( ',', $attributes['exclude'] ) ); $exclude = array_filter( $exclude ); if ( ! empty( $exclude ) ) { $args['post__not_in'] = $exclude; } } // Taxonomy Parameters $tax_queries = array(); if ( ! empty( $attributes['categories'] ) ) { // Categories $categories = is_array( $attributes['categories'] ) ? array_map( 'intval', $attributes['categories'] ) : array_map( 'intval', explode( ',', $attributes['categories'] ) ); $categories = array_filter( $categories ); if ( ! empty( $categories ) ) { $tax_queries[] = array( 'taxonomy' => 'aiopod_categories', 'field' => 'term_id', 'terms' => $categories, 'include_children' => false ); } } if ( ! empty( $attributes['participants'] ) ) { // Participants $participants = array_map( 'intval', ( is_array( $attributes['participants'] ) ? $attributes['participants'] : explode( ',', $attributes['participants'] ) ) ); $participants = array_filter( $participants ); if ( ! empty( $participants ) ) { $tax_queries[] = array( 'taxonomy' => 'aiopod_participants', 'field' => 'term_id', 'terms' => $participants, 'include_children' => false ); } } $count_tax_queries = count( $tax_queries ); if ( $count_tax_queries ) { $args['tax_query'] = ( $count_tax_queries > 1 ) ? array_merge( array( 'relation' => 'AND' ), $tax_queries ) : $tax_queries; } // Order & Orderby Parameters $orderby = sanitize_text_field( $attributes['orderby'] ); $order = sanitize_text_field( $attributes['order'] ); switch ( $orderby ) { case 'popularity': $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'total_downloads_plays'; $args['order'] = $order; break; case 'downloads': $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'total_downloads'; $args['order'] = $order; break; case 'plays': $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'total_plays'; $args['order'] = $order; break; case 'rand': $seed = aiopod_get_orderby_rand_seed(); $args['orderby'] = 'RAND(' . $seed . ')'; break; default: $args['orderby'] = $orderby; $args['order'] = $order; } $args = apply_filters( 'aiopod_query_args', $args, $attributes ); $aiopod_query = new WP_Query( $args ); // Process output if ( $aiopod_query->have_posts() ) { if ( 'none' === $attributes['pagination'] ) { $attributes['count'] = $aiopod_query->post_count; } else { $attributes['count'] = $aiopod_query->found_posts; } if ( 'none' !== $attributes['pagination'] ) { $attributes['max_num_pages'] = $aiopod_query->max_num_pages; } } ob_start(); include apply_filters( 'aiopod_load_template', AIOPOD_PLUGIN_DIR . 'public/templates/podcasts/podcasts-grid.php', $attributes ); $content = ob_get_clean(); return $content; } /** * Resolves attributes into a valid, final configuration. * * This method guarantees that the returned attributes array is safe, * consistent, and ready for query building and rendering. * * @since 1.0.0 * @param array $attributes Raw attributes merged from defaults and user input. * @return array Fully resolved attributes array. */ private function resolve_attributes( $attributes ) { // Pagination if ( empty( $attributes['pagination'] ) ) { $attributes['pagination'] = $this->defaults['pagination']; } // Return resolved array return $attributes; } /** * Get the default shortcode attribute values. * * @since 1.0.0 * @return array An associative array of attributes. */ public function get_defaults() { if ( empty( $this->defaults ) ) { $podcasts_settings = aiopod_get_option( 'aiopod_podcasts_settings' ); $pagination_settings = aiopod_get_option( 'aiopod_pagination_settings' ); $fields = aiopod_get_shortcode_fields(); foreach ( $fields['podcasts']['sections'] as $section ) { foreach ( $section['fields'] as $field ) { $this->defaults[ $field['name'] ] = $field['value']; } } $this->defaults['source'] = 'podcasts'; $this->defaults['title'] = ''; $this->defaults['count'] = 0; $this->defaults['paged'] = aiopod_get_page_number(); $this->defaults['max_num_pages'] = 1; $this->defaults['include'] = ''; $this->defaults['exclude'] = ''; $this->defaults['pagination'] = ! empty( $podcasts_settings['pagination'] ) ? $podcasts_settings['pagination'] : $pagination_settings['layout']; $this->defaults['hide_if_empty'] = 0; } return $this->defaults; } }