HEX
Server: Apache/2.4.65 (Debian)
System: Linux wordpress-7cb4c6b6f6-nmkdc 5.15.0-131-generic #141-Ubuntu SMP Fri Jan 10 21:18:28 UTC 2025 x86_64
User: www-data (33)
PHP: 8.3.27
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/jet-engine/includes/components/query-builder/queries/repeater.php
<?php
namespace Jet_Engine\Query_Builder\Queries;

use Jet_Engine\Query_Builder\Manager;

class Repeater_Query extends Base_Query {

	public $_instance_fields = array();
	private $parent_object   = null;
	private $object_id       = null;

	use Traits\Meta_Query_Trait;

	public function maybe_setup_repeater_preview() {

		if ( ! \Jet_Engine_Listings_Preview::$is_preview ) {
			return;
		}

		$post_id = ! empty( $this->preview['post_id'] ) ? $this->preview['post_id'] : false;

		if ( ! $post_id ) {
			return;
		}

		$this->setup_current_object( get_post( $post_id ) );

		if ( ! empty( $this->preview['query_string'] ) ) {

			parse_str( $this->preview['query_string'], $query_array );

			if ( ! empty( $query_array ) ) {
				foreach ( $query_array as $key => $value ) {
					$_GET[ $key ]     = $value;
					$_REQUEST[ $key ] = $value;
				}
			}

		}

		// Update query
		$this->setup_query();

	}

	/**
	 * Ensure `Jet_Engine_Queried_Repeater_Item` class is included
	 */
	public function include_queried_repeater_item() {
		if ( ! class_exists( '\Jet_Engine_Queried_Repeater_Item' ) ) {
			require_once jet_engine()->plugin_path( 'includes/classes/repeater-item.php' );
		}
	}

	/**
	 * Returns queried items array
	 *
	 * @return array
	 */
	public function get_items() {
		// Before get items, ensure `Jet_Engine_Queried_Repeater_Item` class is included
		$this->include_queried_repeater_item();
		return parent::get_items();
	}

	/**
	 * Returns queries items
	 *
	 * @return [type] [description]
	 */
	public function _get_items( $count = false ) {

		$this->maybe_setup_repeater_preview();

		$items     = array();
		$args      = $this->final_query;
		$object_id = ! empty( $args['object_id'] ) ? $args['object_id'] : false;

		$this->setup_current_object( $object_id );

		$current_object_id = jet_engine()->listings->data->get_current_object_id();

		switch ( $args['source'] ) {
			case 'jet_engine':

				$field = ! empty( $args['jet_engine_field'] ) ? $args['jet_engine_field'] : '';
				$field_data = explode( '::', $field );

				if ( ! empty( $field_data[1] ) ) {
					$items = jet_engine()->listings->data->get_meta( $field_data[1] );
				}

				break;

			case 'jet_engine_option':

				$field = ! empty( $args['jet_engine_option_field'] ) ? $args['jet_engine_option_field'] : '';

				if ( ! empty( $field ) ) {
					$items = jet_engine()->listings->data->get_option( $field );
				}

				$current_object_id = 0;

				break;

			case 'custom':

				$field = ! empty( $args['custom_field'] ) ? $args['custom_field'] : '';

				if ( $field ) {

					$items = jet_engine()->listings->data->get_meta( $field );

					if ( ! empty( $items ) && ! is_array( $items ) ) {
						$items = json_decode( $items );
					}

					$items = wp_unslash( $items );
				}

				$current_object_id = jet_engine()->listings->data->get_current_object_id();

				break;

			default:

				$items = apply_filters(
					'jet-engine/query-builder/types/repeater-query/items/' . $args['source'],
					$items, $args, $this
				);

				break;
		}

		$this->reset_current_object( $object_id );

		if ( empty( $items ) || ! is_array( $items ) ) {
			$items = array();
		}

		$this->include_queried_repeater_item();

		if ( ! empty( $args['meta_query'] ) ) {
			$args['meta_query'] = $this->prepare_meta_query_args( $args );
		}

		$items = array_values( $items );
		$items = array_filter( array_map( function( $item, $index ) use ( $args, $current_object_id ) {

			$item = (object) $item;

			if ( ! $this->is_item_met_requested_args( $item, $args ) ) {
				return false;
			}

			return new \Jet_Engine_Queried_Repeater_Item( $item, $index, $current_object_id, $this->id );

		}, $items, array_keys( $items ) ) );

		$this->apply_sorting( $items, $args );

		// Pagination
		$limit  = $this->get_items_per_page();
		$offset = ! empty( $this->final_query['offset'] ) ? absint( $this->final_query['offset'] ) : 0;

		if ( ! $count ) {
			$page   = $this->get_current_items_page();
			$offset = $offset + ( $page - 1 ) * $limit;
		}

		$limit = ( ! empty( $limit ) && ! $count ) ? $limit : null;

		$items = array_slice( $items, $offset, $limit );

		if ( empty( $items ) ) {
			$items = array();
		}

		return array_values( $items );
	}

	/**
	 * Apply sorting to items array
	 *
	 * This function sorts the given items array based on specified criteria.
	 *
	 * @param  array &$items The array of items to be sorted.
	 * @return void
	 */
	public function apply_sorting( &$items ) {

		$orderby = ! empty( $this->final_query['orderby'] ) ? $this->final_query['orderby'] : false;

		if ( ! $orderby ) {
			return;
		}

		usort( $items, function ( $a, $b ) use ( $orderby ) {

			foreach ( $orderby as $order ) {

				$_by = ! empty( $order['orderby'] ) ? $order['orderby'] : 'index';
				$field = ! empty( $order['field_name'] ) ? $order['field_name'] : 'index';
				$type  = ! empty( $order['order_type'] ) ? strtolower( $order['order_type'] ) : 'numeric';
				$direction = ! empty( $order['order'] ) ? strtolower( $order['order'] ) : 'asc';

				// Retrieve the field values
				if ( 'index' === $_by ) {
					$value_a = $a->get_index();
					$value_b = $b->get_index();
					$type    = 'numeric'; // Force numeric type for index sorting.
				} else {
					$value_a = $a->$field;
					$value_b = $b->$field;
				}

				switch ( $type ) {
					case 'numeric':
						$value_a = floatval( $value_a );
						$value_b = floatval( $value_b );
						break;

					case 'alphabetical':
						$value_a = strtolower( $value_a );
						$value_b = strtolower( $value_b );
						break;

					case 'dates':
						$value_a = strtotime( $value_a );
						$value_b = strtotime( $value_b );
						break;
				}

				// If the values are different, decide order based on the 'order' property
				if ( $value_a < $value_b ) {
					return ( $direction === 'asc' ) ? -1 : 1;
				} elseif ( $value_a > $value_b ) {
					return ( $direction === 'asc' ) ? 1 : -1;
				}
				// If equal, move to the next order condition
			}

			// All compared fields are equal
			return 0;
		});
	}

	/**
	 * Check if item meets requested meta query args
	 *
	 * @param  object $item Repeater item.
	 * @param  array $args Query arguments to check the item against.
	 * @return array
	 */
	public function is_item_met_requested_args( $item, $args ) {

		$result = true;

		if ( empty( $args['meta_query'] ) ) {
			return $result;
		}

		$meta_query = $args['meta_query'];
		$relation   = ! empty( $meta_query['relation'] ) ? strtolower( $meta_query['relation'] ) : 'and';

		unset( $meta_query['relation'] );

		if ( empty( $meta_query ) ) {
			return $result;
		}

		$result = 'or' !== $relation;

		foreach ( $meta_query as $clause ) {

			if ( ! empty( $clause['relation'] ) ) {

				$matched = $this->is_item_met_requested_args( $item,
					array(
						'meta_query' => $clause,
					)
				);

			} else {

				$key = ! empty( $clause['key'] ) ? $clause['key'] : '';

				if ( ! $key ) {
					continue;
				}

				$compare = ! empty( $clause['compare'] ) ? $clause['compare'] : '=';
				$value   = ! \Jet_Engine_Tools::is_empty( $clause, 'value' ) ? $clause['value'] : '';

				$matched = false;

				switch ( $compare ) {
					case '=':
						if ( isset( $item->$key ) ) {
							$matched = $item->$key == $value;
						}
						break;

					case '!=' :
						if ( isset( $item->$key ) ) {
							$matched = $item->$key != $value;
						} else {
							$matched = true;
						}
						break;

					case '>':
						if ( isset( $item->$key ) ) {
							$matched = $item->$key > $value;
						}
						break;

					case '>=':
						if ( isset( $item->$key ) ) {
							$matched = $item->$key >= $value;
						}
						break;

					case '<':
						if ( isset( $item->$key ) ) {
							$matched = $item->$key < $value;
						}
						break;

					case '<=':
						if ( isset( $item->$key ) ) {
							$matched = $item->$key <= $value;
						}
						break;

					case 'LIKE':
						if ( isset( $item->$key ) ) {

							if ( is_array( $item->$key ) ) { // for Checkbox, Select2 ... fields
								$matched = in_array( $value, $item->$key );
							} else {
								$matched = false !== strpos( $item->$key, $value );
							}
						}
						break;

					case 'NOT LIKE':
						if ( isset( $item->$key ) ) {

							if ( is_array( $item->$key ) ) { // for Checkbox, Select2 ... fields
								$matched = ! in_array( $value, $item->$key );
							} else {
								$matched = false === strpos( $item->$key, $value );
							}

						} else {
							$matched = true;
						}
						break;

					case 'IN':
						if ( isset( $item->$key ) ) {
							$value   = $this->value_to_array( $value );
							$matched = in_array( $item->$key, $value );
						}
						break;

					case 'NOT IN':
						if ( isset( $item->$key ) ) {
							$value   = $this->value_to_array( $value );
							$matched = ! in_array( $item->$key, $value );
						} else {
							$matched = true;
						}
						break;

					case 'BETWEEN':
						if ( isset( $item->$key ) ) {
							$value = $this->value_to_array( $value );
							if ( isset( $value[1] ) ) {
								$matched = ( $value[0] <= $item->$key && $item->$key <= $value[1] );
							}
						}
						break;

					case 'NOT BETWEEN':
						if ( isset( $item->$key ) ) {
							$value = $this->value_to_array( $value );
							if ( isset( $value[1] ) ) {
								$matched = ( $value[0] > $item->$key || $item->$key > $value[1] );
							}
						} else {
							$matched = true;
						}

						break;

					case 'EXISTS':
						$matched = ! empty( $item->$key );
						break;

					case 'NOT EXISTS':
						$matched = empty( $item->$key );
						break;

					case 'REGEXP':

						if ( isset( $item->$key ) ) {
							$subject = $item->$key;

							if ( is_array( $item->$key ) ) {
								// Serialize item value if filtered by checkbox
								if ( false !== strpos( $value, ';s:4:"true"' ) ) {
									$subject = maybe_serialize( $item->$key );
								} else {
									$subject = json_encode( $item->$key );
								}
							}

							$value = trim( $value, '/' );
							preg_match( '/' . $value . '/', $subject, $matched );
							$matched = ! empty( $matched );
						}

						break;

					case 'NOT REGEXP':

						if ( isset( $item->$key ) ) {

							if ( is_array( $item->$key ) ) {
								$item->$key = json_encode( $item->$key );
							}

							$value = trim( $value, '/' );
							preg_match( '/' . $value . '/', $item->$key, $matched );
							$matched = empty( $matched );
						} else {
							$matched = true;
						}

						break;

				}

			}

			if ( 'or' === $relation && $matched ) {
				$result = true;
			} elseif ( 'or' !== $relation && ! $matched ) {
				$result = false;
			}

		}

		return $result;
	}

	public function value_to_array( $value ) {

		if ( ! is_array( $value ) ) {
			$value = explode( ',', $value );
			$value = array_map( 'trim', $value );
		}

		return $value;
	}

	public function setup_current_object( $object ) {

		if ( ! $object ) {
			// Added to clear a query cache if a repeater listing is inside another listing.
			$this->apply_macros( '%current_id%' );
			return;
		}

		if ( ! is_object( $object ) ) {
			$this->object_id = absint( $object );
			add_filter( 'jet-engine/listing/current-object-id', array( $this, 'replace_object_id' ) );

			if ( ! empty( $this->final_query['source'] )
				 && 'jet_engine' === $this->final_query['source']
				 && ! empty( $this->final_query['jet_engine_field'] )
			) {
				$field_data = explode( '::', $this->final_query['jet_engine_field'] );

				if ( ! empty( $field_data[0] ) ) {

					if ( taxonomy_exists( $field_data[0] ) ) {
						$object = get_term( $object );
					} else {
						$object = get_post( $object );
					}

				}

				if ( ! empty( $field_data['2'] ) && $field_data['2'] === 'user' ) {
					$user = get_user_by( 'ID', $this->object_id );
					
					if ( $user->ID ?? false ) {
						$object = $user;
					}
				}
			}

			if ( ! is_object( $object )  ) {
				$object = get_post( $object );
			}

			$object = apply_filters( 'jet-engine/query-builder/repeater-query/object-by-id', $object, $this );
		}

		/**
		 * If we get an array - let's assume it's what we need and just convert it to object.
		 * https://github.com/Crocoblock/issues-tracker/issues/11593
		 */
		if ( ! empty( $object ) && is_array( $object ) ) {
			$object = (object) $object;
		}

		if ( is_object( $object ) ) {
			$this->parent_object = jet_engine()->listings->data->get_current_object();
			jet_engine()->listings->data->set_current_object( $object );
		}

	}

	public function reset_current_object( $object ) {

		if ( ! $object ) {
			return;
		}

		if ( $this->object_id ) {
			$this->object_id = null;
			remove_filter( 'jet-engine/listing/current-object-id', array( $this, 'replace_object_id' ) );
		}

		if ( $this->parent_object ) {
			jet_engine()->listings->data->set_current_object( $this->parent_object );
			$this->parent_object = null;
		}

	}

	public function replace_object_id( $object_id ) {

		if ( $this->object_id ) {
			return $this->object_id;
		}

		return $object_id;

	}

	public function get_object_id() {
		return $this->object_id;
	}

	public function get_current_items_page() {

		if ( empty( $this->final_query['page'] ) ) {
			return 1;
		}

		return absint( $this->final_query['page'] );
	}

	/**
	 * Returns total found items count
	 *
	 * @return [type] [description]
	 */
	public function get_items_total_count() {

		$cached = $this->get_cached_data( 'count' );

		if ( false !== $cached ) {
			return $cached;
		}

		$this->setup_query();

		$items  = $this->_get_items( true );
		$result = count( $items );

		$this->update_query_cache( $result, 'count' );

		return $result;

	}

	/**
	 * Returns count of the items visible per single listing grid loop/page
	 * @return [type] [description]
	 */
	public function get_items_per_page() {

		$this->setup_query();
		$limit = 0;

		if ( ! empty( $this->final_query['per_page'] ) ) {
			$limit = absint( $this->final_query['per_page'] );
		}

		return $limit;
	}

	/**
	 * Returns queried items count per page
	 *
	 * @return [type] [description]
	 */
	public function get_items_page_count() {

		$result   = $this->get_items_total_count();
		$per_page = $this->get_items_per_page();

		if ( $per_page < $result ) {

			$page  = $this->get_current_items_page();
			$pages = $this->get_items_pages_count();

			if ( $page < $pages ) {
				$result = $per_page;
			} elseif ( $page == $pages ) {
				$offset = ( $page - 1 ) * $per_page;
				$result = $result - $offset;
			}
		}

		return $result;
	}

	/**
	 * Returns queried items pages count
	 *
	 * @return [type] [description]
	 */
	public function get_items_pages_count() {

		$per_page = $this->get_items_per_page();
		$total    = $this->get_items_total_count();

		if ( ! $per_page || ! $total ) {
			return 1;
		}

		return ceil( $total / $per_page );
	}

	/**
	 * Get fields list are available for the current instance of this query
	 *
	 * @return [type] [description]
	 */
	public function get_instance_fields() {

		if ( ! empty( $this->_instance_fields ) ) {
			return $this->_instance_fields;
		}

		$result = array();
		$args = $this->query;

		if ( empty( $args['source'] ) ) {
			return $result;
		}

		switch ( $args['source'] ) {

			case 'jet_engine':

				$field = ! empty( $args['jet_engine_field'] ) ? $args['jet_engine_field'] : '';
				$field_data = explode( '::', $field );

				if ( ! empty( $field_data[1] ) ) {
					$fields = jet_engine()->meta_boxes->get_meta_fields_for_object( $field_data[0] );
					$result = $this->get_options_from_fields_data( $field_data[1], $fields );
				}

				break;

			case 'jet_engine_option':
				$field = ! empty( $args['jet_engine_option_field'] ) ? $args['jet_engine_option_field'] : '';
				$field_data = explode( '::', $field );

				if ( ! empty( $field_data[1] ) ) {
					$page = isset( jet_engine()->options_pages->registered_pages[ $field_data[0] ] ) ? jet_engine()->options_pages->registered_pages[ $field_data[0] ] : false;

					if ( $page ) {
						$result = $this->get_options_from_fields_data( $field_data[1], $page->meta_box );
					}

				}

				break;

			case 'custom':
				$fields_list = ! empty( $args['fields_list'] ) ? $args['fields_list'] : '';
				$fields_list = explode( ',', str_replace( ', ', ',', $fields_list ) );
				$result      = array_combine( $fields_list, $fields_list );
				break;

			default:
				$result = apply_filters(
					'jet-engine/query-builder/types/repeater-query/fields/' . $args['source'],
					$result, $args, $this
				);
				break;
		}

		$this->_instance_fields = $result;

		return $result;

	}

	public function get_options_from_fields_data( $search_field, $all_fields ) {

		$field_settings = $this->find_field( $search_field, $all_fields );
		$result         = array();

		$fields = ! empty( $field_settings['repeater-fields'] ) ? $field_settings['repeater-fields'] : false;
		$fields = ! $fields && ! empty( $field_settings['fields'] ) ? $field_settings['fields'] : $fields;

		if ( ! $fields ) {
			return $result;
		}

		foreach ( $fields as $field ) {

			$label = ! empty( $field['title'] ) ? $field['title'] : false;
			$label = ! $label && ! empty( $field['label'] ) ? $field['label'] : $field['name'];
			$result[ $field['name'] ] = $label;
		}

		return $result;

	}

	public function find_field( $name, $fields ) {

		foreach ( $fields as $field ) {
			if ( isset( $field['name'] ) && $name === $field['name'] ) {
				return $field;
			}
		}

		return array();
	}

	/**
	 * Sets up the filtered properties of the repeater.
	 *
	 * @return void
	 */
	public function set_filtered_prop( $prop = '', $value = null ) {

		switch ( $prop ) {

			case '_page':
				$this->final_query['page'] = $value;
				break;

			case '_items_per_page':
				$this->final_query['per_page'] = $value;
				break;

			case 'meta_query':
				$this->replace_meta_query_row( $value );
				break;

			case 'orderby':
			case 'order':
			case 'meta_key':
					$this->set_filtered_order( $prop, $value );
					break;

			default:
				$this->merge_default_props( $prop, $value );
				break;
		}
	}

	/**
	 * Set filtered order in the similar way like for the posts
	 *
	 * @param  [type] $value [description]
	 * @return [type]        [description]
	 */
	public function set_filtered_order( $key, $value ) {

		if (
			empty( $this->final_query['orderby'] )
			|| ! isset( $this->final_query['orderby']['custom'] )
		) {
			$this->final_query['orderby'] = array( 'custom' => array( 'orderby' => 'field' ) );
		}

		if ( 'orderby' === $key ) {
			$key   = 'order_type';
			$value = ( 'meta_value' === $value ) ? 'alphabetical' : 'numeric';
		}

		if ( 'meta_key' === $key ) {
			$key = 'field_name';
		}

		$this->final_query['orderby']['custom'][ $key ] = $value;
	}
}