<?php /** * Ajax Dropdown class. */ /** * Handles common AJAX operations. * * @since 4.6 */ class Tribe__Ajax__Dropdown { // phpcs:ignore-next-line PEAR.NamingConventions.ValidClassName.Invalid /** * Hooks the AJAX for Select2 Dropdowns * * @since 4.6 * * @return void */ public function hook() { add_action( 'wp_ajax_tribe_dropdown', [ $this, 'route' ] ); add_action( 'wp_ajax_nopriv_tribe_dropdown', [ $this, 'route' ] ); } /** * Search for Terms using Select2 * * @since 4.6 * * @param string|array<string|mixed> $search Search string from Select2. * @param int $page When we deal with pagination. * @param array<string|mixed> $args Which arguments we got from the Template. * @param string $source What source it is. * * @return array<string|mixed> */ public function search_terms( $search, $page, $args, $source ) { $data = []; if ( empty( $args['taxonomy'] ) ) { $this->error( esc_attr__( 'Cannot look for Terms without a taxonomy', 'tribe-common' ) ); } // We always want all the fields so we overwrite it. $args['fields'] = $args['fields'] ?? 'all'; $args['hide_empty'] = $args['hide_empty'] ?? false; if ( ! empty( $search ) ) { if ( ! is_array( $search ) ) { // For older pieces that still use Select2 format. $args['search'] = $search; } else { // Newer SelectWoo uses a new search format. $args['search'] = $search['term']; } } // On versions older than 4.5 taxonomy goes as a Param. if ( version_compare( $GLOBALS['wp_version'], '4.5', '<' ) ) { $terms = get_terms( $args['taxonomy'], $args ); } else { $terms = get_terms( $args ); } $results = []; // Respect the parent/child_of argument if set. $parent = ! empty( $args['child_of'] ) ? (int) $args['child_of'] : 0; $parent = ! empty( $args['parent'] ) ? (int) $args['parent'] : $parent; if ( empty( $args['search'] ) ) { $this->sort_terms_hierarchically( $terms, $results, $parent ); $results = $this->convert_children_to_array( $results ); } else { foreach ( $terms as $term ) { // Prep for Select2. $term->id = $term->term_id; $term->text = $term->name; $term->breadcrumbs = []; if ( 0 !== (int) $term->parent ) { $ancestors = get_ancestors( $term->id, $term->taxonomy ); $ancestors = array_reverse( $ancestors ); foreach ( $ancestors as $ancestor ) { $ancestor = get_term( $ancestor ); $term->breadcrumbs[] = $ancestor->name; } } $results[] = $term; } } foreach ( $results as $result ) { $result->text = wp_specialchars_decode( wp_kses( $result->text, [] ) ); } $data['results'] = $results; $data['taxonomies'] = get_taxonomies(); return $data; } /** * Search for Posts using Select2 * * @since 4.12.17 * * @param string|array<string,mixed> $search Search string from Select2. * @param int $page Page we want when we're dealing with pagination. * @param array<string,mixed> $args Arguments to pass to the query. * @param string|int $selected Selected item ID. * * @return array<string|mixed> An Array of results. */ public function search_posts( $search, $page = 1, $args = [], $selected = null ) { if ( ! empty( $search ) ) { if ( is_array( $search ) ) { // Newer SelectWoo uses a new search format. $args['s'] = $search['term']; // post? } else { // For older pieces that still use Select2 format. $args['s'] = $search; } } $args['post_status'] = $args['post_status'] ?? 'publish'; $args['paged'] = $page; $args['update_post_meta_cache'] = false; $args['update_post_term_cache'] = false; $results = new WP_Query( $args ); $has_pagination = $results->post_count < $results->found_posts; return $this->format_posts_for_dropdown( $results->posts, $selected, $has_pagination ); } /** * Formats a given array of posts to be displayed into the Dropdown.js module with SelectWoo. * * @since 4.12.17 * * @param array<WP_Post> $posts Array of WP_Post objects. * @param null|int $selected Selected item ID. * @param boolean $pagination Whether or not we have pagination. * * @return array */ public function format_posts_for_dropdown( array $posts, $selected = null, $pagination = false ) { $data = [ 'posts' => [], 'pagination' => $pagination, ]; // Skip when we don't have posts. if ( empty( $posts ) ) { return $data; } foreach ( $posts as $post ) { if ( ! $post instanceof \WP_Post ) { $post = get_post( $post ); } // Skip non WP Post Objects. if ( ! $post instanceof \WP_Post ) { continue; } // Prep for Select2. $data['posts'][] = [ 'id' => $post->ID, 'text' => ! empty( $post->post_title_formatted ) ? $post->post_title_formatted : $post->post_title, 'selected' => ! empty( $selected ) && (int) $post->ID === (int) $selected, ]; } return $data; } /** * Sorts all the Terms for Select2 hierarchically. * * @since 4.6 * * @param array<int|object> $terms Array of Terms from `get_terms`. * @param array<string|mixed> $into Variable where we will store the. * @param integer $parent Used for the recursion. */ public function sort_terms_hierarchically( &$terms, &$into, $parent = 0 ) { foreach ( $terms as $i => $term ) { if ( $term->parent === $parent ) { // Prep for Select2. $term->id = $term->term_id; $term->text = $term->name; $into[ $term->term_id ] = $term; unset( $terms[ $i ] ); } } foreach ( $into as $term ) { $term->children = []; $this->sort_terms_hierarchically( $terms, $term->children, $term->term_id ); } } /** * Makes sure we have arrays for the JS data for Select2 * * @since 4.6 * * @param object|array<string|mixed> $results The Select2 results. * * @return array<string|mixed> */ public function convert_children_to_array( $results ) { if ( isset( $results->children ) ) { $results->children = $this->convert_children_to_array( $results->children ); if ( empty( $results->children ) ) { unset( $results->children ); } } else { foreach ( $results as $key => $item ) { $item = $this->convert_children_to_array( $item ); } } if ( empty( $results ) ) { return []; } return array_values( (array) $results ); } /** * Parses the Params coming from Select2 Search box. * * @since 4.6 * @since 5.1.17 Added an allow list of params to restrict the shape of the database queries. * * @param array<string|mixed> $params Params to overwrite the defaults. * * @return object */ public function parse_params( $params ) { $allowed_params = [ 'search' => $params['search'] ?? null, 'page' => $params['page'] ?? 0, 'source' => $params['source'] ?? null, 'args' => [ 'post_status' => 'publish' ], ]; if ( isset( $params['args']['taxonomy'] ) ) { $allowed_params['args']['taxonomy'] = $params['args']['taxonomy']; } if ( isset( $params['args']['post_type'] ) ) { $allowed_params['args']['post_type'] = $params['args']['post_type']; } // Return Object just for the sake of making it simpler to read. return (object) $allowed_params; } /** * The default Method that will route all the AJAX calls from our Dropdown AJAX requests * It is like a Catch All on `wp_ajax_tribe_dropdown` and `wp_ajax_nopriv_tribe_dropdown` * * @since 4.6 * @since 5.1.17 Adding more sanitization to the request params. * * @return void */ public function route() { // Push all POST params into a Default set of data. $args = $this->parse_params( empty( $_POST ) ? [] : $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing check_ajax_referer( 'tribe_dropdown', 'nonce' ); if ( empty( $args->source ) ) { $this->error( esc_attr__( 'Missing data source for this dropdown', 'tribe-common' ) ); } // Define a Filter to allow external calls to our Select2 Dropdowns. $filter = sanitize_key( 'tribe_dropdown_' . $args->source ); if ( has_filter( $filter ) ) { $data = apply_filters( $filter, [], $args->search, $args->page, $args->args, $args->source ); } elseif ( in_array( $args->source, [ 'search_terms', 'search_posts' ] ) ) { // Switched from array to object above. Specify what we're sending. $data = call_user_func_array( [ $this, $args->source ], [ $args->search, $args->page, $args->args, $args->source, ] ); } // If we've got a empty dataset we return an error. if ( empty( $data ) ) { $this->error( esc_attr__( 'Empty data set for this dropdown', 'tribe-common' ) ); } else { $this->success( $data ); } } /** * Prints a success message and ensures that we don't hit bugs on Select2 * * @since 4.6 * * @param array $data The data to send back to Select2. * * @return void */ private function success( $data ) { // We need a Results item for Select2 Work. if ( ! isset( $data['results'] ) ) { $data['results'] = []; } wp_send_json_success( $data ); } /** * Prints an error message and ensures that we don't hit bugs on Select2 * * @since 4.6 * * @param string $message The error message. * * @return void */ private function error( $message ) { $data = [ 'message' => $message, 'results' => [], ]; wp_send_json_error( $data ); } /** * Avoid throwing fatals or notices on sources that are invalid * * @since 4.6 * * @param string $name The name of the method. * @param mixed $arguments The arguments passed to the method. */ public function __call( $name, $arguments ) { /* Translators: %1$s is the name of the source, %2$s is the class name */ $message = __( 'The "%1$s" source is invalid and cannot be reached on "%2$s" instance.', 'tribe-common' ); return $this->error( sprintf( $message, $name, __CLASS__ ) ); } }