HEX
Server: Apache/2.4.65 (Debian)
System: Linux wordpress-7cb4c6b6f6-5w6qq 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/bricks-views/query-loop.php
<?php

namespace Jet_Engine\Bricks_Views;

use Bricks\Api;
use Bricks\Query;
use Bricks\Database;
use Jet_Engine\Bricks_Views\Helpers\Options_Converter;
use Jet_Engine\Query_Builder\Manager as Query_Manager;

class Query_Loop {

	public $initial_object = [];
	public $initial_popup_object = null;
	public $listing_stack = array();

	function __construct() {
		// Bricks loop with default object types
		add_action( 'bricks/query/before_loop', array( $this, 'initialize_object_before_render' ), 10 );
		add_action( 'bricks/query/after_loop', array( $this, 'restore_initial_object_after_render' ), 10 );
		add_filter( 'jet-engine/listings/data/the-post/is-main-query', array( $this, 'maybe_modify_is_main_query' ) );

		// Bricks loop with Query builder
		add_action( 'init', array( $this, 'add_control_to_elements' ), 40 );
		add_filter( 'bricks/setup/control_options', array( $this, 'setup_query_controls' ) );

		add_filter( 'bricks/query/run', array( $this, 'run_query' ), 10, 2 );
		add_filter( 'bricks/query/result_count', array( $this, 'set_count' ), 10, 2 );
		add_filter( 'bricks/query/result_max_num_pages', array( $this, 'set_max_num_pages' ), 10, 2 );
		add_filter( 'bricks/query/init_loop_index', array( $this, 'initialize_loop_index' ), 10, 3 );
		add_filter( 'bricks/query/loop_object', array( $this, 'set_loop_object' ), 10, 3 );

		// Initialize current listing for Bricks loop with default object types and Query builder.
		add_action( 'bricks/query/before_loop', array( $this, 'initialize_current_listing' ), 10 );
		add_action( 'bricks/query/after_loop', array( $this, 'restore_current_listing' ), 10 );

		// Ajax-powered popup inside a Bricks loop item
		add_action( 'bricks/frontend/before_render_data', array( $this, 'initialize_object_before_popup_render' ), 10, 2 );
		add_action( 'bricks/frontend/after_render_data', array( $this, 'restore_object_after_popup_render' ), 10, 2);

		add_filter( 'bricks/element/settings', array( $this, 'manage_stack_for_bricks_loop' ), 10, 2 );

		// JetEngine shortcode inside a Bricks loop item
		add_filter( 'jet-engine/listings/data/default-object', array( $this, 'maybe_set_bricks_loop_object' ) );
	}

	/**
	 * Initialize the object before rendering.
	 *
	 * @param object $query The query object containing details about the query.
	 */
	public function initialize_object_before_render( $query ) {

		if ( in_array( $query->object_type, [ 'user', 'term' ] ) ) {
			add_action( 'jet-engine/listing-element/before-render', array( $this, 'set_current_object' ) );
		}

		$this->initial_object[ $query->element_id ] = jet_engine()->listings->data->get_current_object();
	}

	/**
	 * Restore the initial object after rendering.
	 *
	 * @param object $query The query object containing details about the query.
	 */
	public function restore_initial_object_after_render( $query ) {

		if ( in_array( $query->object_type, [ 'user', 'term' ] ) ) {
			remove_action( 'jet-engine/listing-element/before-render', array( $this, 'set_current_object' ) );
		}

		if ( ! empty( $this->initial_object[ $query->element_id ] ) ) {
			jet_engine()->listings->data->set_current_object( $this->initial_object[ $query->element_id ] );
			unset( $this->initial_object[ $query->element_id ] );
		}
	}

	// Set current User or Term object to dynamic widgets in a Bricks loop
	public function set_current_object() {
		jet_engine()->listings->data->set_current_object( Query::get_loop_object() );
	}

	/**
	 * Modify the main query under certain conditions.
	 *
	 * @param bool   $is_main_query  Whether the query is the main query.
	 * @param object $post           The current post object.
	 * @param object $query          The current WP_Query object.
	 *
	 * @return bool  Modified value for $is_main_query.
	 */
	public function maybe_modify_is_main_query( $is_main_query ) {
		$content_type = Database::$active_templates['content_type'] ?? '';

		if ( $is_main_query && $content_type === 'archive' ) {
			return ! $is_main_query;
		}

		return $is_main_query;
	}

	public function add_control_to_elements() {
		// Only container, block and div element have query controls
		$elements = [ 'section', 'container', 'block', 'div' ];

		foreach ( $elements as $name ) {
			add_filter( "bricks/elements/{$name}/controls", [ $this, 'add_jet_engine_controls' ], 40 );
		}
	}

	public function add_jet_engine_controls( $controls ) {
		$options = \Jet_Engine\Query_Builder\Manager::instance()->get_queries_for_options();

		// jet_engine_query_builder_id will be my option key
		$jet_engine_control['jet_engine_query_builder_id'] = [
			'tab'         => 'content',
			'label'       => esc_html__( 'JetEngine Queries', 'jet-engine' ),
			'type'        => 'select',
			'options'     => Options_Converter::remove_empty_key_in_options( $options ),
			'placeholder' => esc_html__( 'Choose a query', 'jet-engine' ),
			'required'    => array(
				[ 'query.objectType', '=', 'jet_engine_query_builder' ],
				[ 'hasLoop', '!=', false ]
			),
			'rerender'    => true,
			'description' => esc_html__( 'Please create a query in JetEngine Query Builder First', 'jet-engine' ),
			'searchable'  => true,
			'multiple'    => false,
		];

		// Below 2 lines is just some php array functions to force my new control located after the query control
		$query_key_index = absint( array_search( 'query', array_keys( $controls ) ) );
		$new_controls    = array_slice( $controls, 0, $query_key_index + 1, true ) + $jet_engine_control + array_slice( $controls, $query_key_index + 1, null, true );

		return $new_controls;
	}

	public function setup_query_controls( $control_options ) {
		// Add a new query loop type
		$control_options['queryTypes']['jet_engine_query_builder'] = esc_html__( 'JetEngine Query Builder', 'jet-engine' );

		return $control_options;
	}

	public function run_query( $results, $query ) {
		if ( ! $this->is_jet_engine_query( $query ) ) {
			return $results;
		}

		$query->add_to_history();

		$je_query = $this->get_jet_engine_query( $query->settings );

		// Return empty results if query not found in JetEngine Query Builder
		if ( ! $je_query ) {
			return $results;
		}

		// Setup query args
		$je_query->setup_query();

		$paged = $query->query_vars['paged'] ?? 1;

		if ( $paged > 1 ) {
			$je_query->set_filtered_prop( '_page', $paged );
		}

		do_action( 'jet-engine/bricks-views/query-builder/on-query', $je_query, $query->element_id );

		// Get the results
		return $je_query->get_items();
	}

	public function set_count( $count, $query ) {
		if ( ! $this->is_jet_engine_query( $query ) ) {
			return $count;
		}

		$je_query = $this->get_jet_engine_query( $query->settings );

		// Return empty results if query not found in JetEngine Query Builder
		if ( ! $je_query ) {
			return $count;
		}

		return $je_query->get_items_total_count();
	}

	public function set_max_num_pages( $max_num_pages, $query ) {
		if ( ! $this->is_jet_engine_query( $query ) ) {
			return $max_num_pages;
		}

		$je_query = $this->get_jet_engine_query( $query->settings );

		// Return empty results if query not found in JetEngine Query Builder
		if ( ! $je_query ) {
			return $max_num_pages;
		}

		return $je_query->get_items_pages_count();
	}

	/**
	 * Calculates the initial loop index for Jet Engine queries.
	 *
	 * @param int $index The default loop index passed through the filter.
	 * @param string $object_type The type of the query object (e.g., 'posts', 'terms', 'users').
	 * @param object $query The query object containing the query variables and settings.
	 * @return int The calculated initial loop index.
	 */
	function initialize_loop_index( $index, $object_type, $query ) {
		if ( ! $this->is_jet_engine_query( $query ) ) {
			return $index;
		}

		$je_query = $this->get_jet_engine_query( $query->settings );

		if ( ! $je_query ) {
			return $index;
		}

		$query_args     = $je_query->get_query_args();
		$paged          = ! empty( $query_args['paged'] ) ? $query_args['paged'] : 1;
		$offset         = isset( $query_args['offset'] ) ? intval( $query_args['offset'] ) : 0;
		$posts_per_page = $je_query->get_items_per_page();

		switch ( $query_args['_query_type'] ) {
			// Post loop and User loop
			case 'posts':
			case 'users':
				$initial_index = $offset + ( $posts_per_page > 0 ? ( $paged - 1 ) * $posts_per_page : 0 );
				break;

			// Term loop and CCT
			case 'terms':
			case 'custom-content-type':
				$initial_index = $offset;
				break;
			default:
				$initial_index = $index;
				break;
		}

		return $initial_index;
	}

	public function set_loop_object( $loop_object, $loop_key, $query ) {
		if ( ! $this->is_jet_engine_query( $query ) ) {
			return $loop_object;
		}

		global $post;

		// I only tested on JetEngine Posts Query, Terms Query, Comments Query and WC Products Query
		// I didn't set WP_Term condition because it's not related to the $post global variable
		if ( is_a( $loop_object, 'WP_Post' ) ) {
			$post = $loop_object;
		} elseif ( is_a( $loop_object, 'WC_Product' ) ) {
			// $post should be a WP_Post object
			$post = get_post( $loop_object->get_id() );
		} elseif ( is_a( $loop_object, 'WP_Comment' ) ) {
			// A comment should refer to a post, so I set the $post global variable to the comment's post
			// You might want to change this to $loop_object->comment_ID
			$post = get_post( $loop_object->comment_post_ID );
		}

		setup_postdata( $post );

		$je_query = $this->get_jet_engine_query( $query->settings );

		// Return empty results if query not found in JetEngine Query Builder
		if ( ! $je_query ) {
			return $loop_object;
		}

		// Set current object for JetEngine
		jet_engine()->listings->data->set_current_object( $loop_object );

		// We still return the $loop_object so \Bricks\Query::get_loop_object() can use it
		return $loop_object;
	}

	/**
	 * Initialize the current listing based on the provided query.
	 *
	 * @param object $query The query object containing the query variables.
	 */
	public function initialize_current_listing( $query ) {
		// Check if the query is a Jet Engine Query Builder request
		if ( $this->is_jet_engine_query( $query ) ) {
			// Set the listing data for Jet Engine Query
			$listing_data = array(
				'listing_source' => 'query',
				'_query_id'      => $this->get_jet_engine_query_id( $query->settings ),
			);
		} else {
			// Get the source from the Bricks query loop
			$source = $this->get_bricks_query_object_type( $query );

			// If a source exists, populate the listing data with new values
			if ( $source ) {
				$query_vars  = $query->query_vars;
				$post_type   = ! empty( $query_vars['post_type'] ) ? $query_vars['post_type'][0] : 'post';
				$tax         = ! empty( $query_vars['taxonomy'] ) ? $query_vars['taxonomy'][0] : 'category';

				$listing_data = array(
					'listing_source'    => $source,
					'listing_post_type' => $post_type,
					'listing_tax'       => $tax,
				);
			}
		}

		// If no listing data was set, exit the function early
		if ( empty( $listing_data ) ) {
			return;
		}

		if ( empty( $this->listing_stack ) ) {
			$this->listing_stack['root'] = jet_engine()->listings->data->get_listing();
		}

		// Create a new document and set the current listing
		$doc = jet_engine()->listings->get_new_doc( $listing_data, 0 );
		jet_engine()->listings->data->set_listing( $doc );

		if ( ! array_key_exists( $query->element_id, $this->listing_stack ) ) {
			$this->listing_stack[ $query->element_id ] = $doc;
		}
	}

	/**
	 * Restore the current listing based on the provided query.
	 *
	 * @param object $query The query object containing the query variables.
	 */
	public function restore_current_listing( $query ) {
		// If this is neither a Jet Engine query nor a Bricks query object type, exit the function
		if ( ! $this->is_jet_engine_query( $query ) && ! $this->get_bricks_query_object_type( $query ) ) {
			return;
		}

		// If element_id does not exist in the stack, exit
		if ( ! isset( $this->listing_stack[ $query->element_id ] ) ) {
			return;
		}

		// Remove the element from the stack
		unset( $this->listing_stack[ $query->element_id ] );

		// Get previous element key
		$keys = array_keys( $this->listing_stack );
		$previous_key = end( $keys );

		// Restore the last element
		jet_engine()->listings->data->set_listing( $this->listing_stack[ $previous_key ] );
	}

	/**
	 * Retrieve the JetEngine query object based on the provided settings.
	 *
	 * @param array $settings The settings array containing the query builder ID.
	 * @return mixed Returns the JetEngine query object if found, or false if no valid query ID.
	 */
	public function get_jet_engine_query( $settings ) {
		$query_id = $this->get_jet_engine_query_id( $settings );

		// Return empty results if no query selected or Use Query is not checked
		if ( $query_id === 0 ) {
			return false;
		}

		// Get the query object from JetEngine based on the query id
		return Query_Manager::instance()->get_query_by_id( $query_id );
	}

	/**
	 * Retrieve the JetEngine query ID from the given settings array.
	 *
	 * @param array $settings The settings array containing the query builder ID.
	 * @return int The query ID as an integer, or 0 if not found or empty.
	 */
	public function get_jet_engine_query_id( $settings ) {
		$query_id = isset( $settings['jet_engine_query_builder_id'] ) ? absint( $settings['jet_engine_query_builder_id'] ) : 0;

		return apply_filters( 'jet-engine/bricks-views/query-builder/query-id', $query_id );
	}

	/**
	 * Check if the provided query object is a Jet Engine Query Builder query.
	 *
	 * @param object $query The query object to validate.
	 * @return bool Returns true if the query is valid, false otherwise.
	 */
	public function is_jet_engine_query( $query ) {
		if ( $query->object_type !== 'jet_engine_query_builder' || ! $query->settings['hasLoop'] ) {
			return false;
		}

		return true;
	}

	/**
	 * Retrieves the default bricks loop source based on the object type.
	 *
	 * @param object $query The query object containing the object_type property.
	 * @return string|false The corresponding source for the bricks loop, or false if not found.
	 */
	public function get_bricks_query_object_type( $query ) {
		$source_mapping = array(
			'post' => 'posts',
			'term' => 'terms',
			'user' => 'users'
		);

		return $source_mapping[ $query->object_type ] ?? false;
	}

	/**
	 * Sets the queried object for rendering JetEngine dynamic widgets in a popup.
	 *
	 * @param array  $elements Array of elements to be rendered.
	 * @param string $area     The area where the elements will be rendered.
	 *
	 * @return void
	 */
	public function initialize_object_before_popup_render( $elements, $area ) {
		if ( $area !== 'popup' || ! $this->is_ajax_popup_looping() ) {
			return;
		}

		/*$this->initial_popup_object = jet_engine()->listings->data->get_current_object();*/

		jet_engine()->listings->data->set_current_object( get_queried_object() );
	}

	/**
	 * Sets the initial object after popup rendering.
	 *
	 * @param array  $elements Array of elements to be rendered.
	 * @param string $area     The area where the elements will be rendered.
	 *
	 * @return void
	 */
	public function restore_object_after_popup_render( $elements, $area ) {
		if ( $area !== 'popup' || ! $this->is_ajax_popup_looping() ) {
			return;
		}

		// Set initial object for generating dynamic style in Listing grid
		/*if ( ! empty( $this->initial_popup_object ) ) {
			jet_engine()->listings->data->set_current_object( $this->initial_popup_object );
		}*/
	}

	/**
	 * Checks if the AJAX popup is currently in a looping state.
	 *
	 * @return bool Returns true if the popup is in a looping state, false otherwise.
	 */
	public function is_ajax_popup_looping() {
		if ( ! Api::is_current_endpoint( 'load_popup_content' ) ) {
			return false;
		}

		$request_data     = jet_engine()->bricks_views->get_request_data();
		$is_looping       = $request_data['isLooping'] ?? '';
		$popup_context_id = $request_data['popupContextId'] ?? '';

		if ( empty( $popup_context_id ) || empty( $is_looping ) ) {
			return false;
		}

		return true;
	}

	// Set popup object to dynamic widgets in a Popup
	public function set_popup_object() {
		jet_engine()->listings->data->set_current_object( get_queried_object() );
	}

	public function manage_stack_for_bricks_loop( $settings, $element ) {
		if ( ! isset( $settings['hasLoop'] ) ) {
			return $settings;
		}

		$object_type = $this->get_object_type( $settings );
		$query_id    = $this->get_jet_engine_query_id( $settings );

		if ( $object_type === 'jet_engine_query_builder' && $query_id !== 0 ) {
			add_filter( 'bricks/query/loop_object', array( $this, 'add_query_builder_object_to_stack' ) );
		} elseif( $object_type === 'post' ) {
			add_action( 'the_post', array( $this, 'add_bricks_loop_object_to_stack' ) );
		} else {
			return $settings;
		}

		add_filter( 'bricks/dynamic_data/render_content', array( $this, 'remove_object_from_stack' ), 10, 2 );

		return $settings;
	}

	/**
	 * Add object to the stack
	 *
	 * @param  [type] $object [description]
	 * @return [type]         [description]
	 */
	public function add_to_stack( $object ) {
		do_action( 'jet-engine/object-stack/increase', $object );
	}

	public function add_query_builder_object_to_stack( $object ) {
		$this->add_to_stack( $object );

		return $object;
	}

	public function add_bricks_loop_object_to_stack( $object ) {
		if ( Query::is_looping() ) {
			$this->add_to_stack( $object );
		}
	}

	/**
	 * Remove object from the stack
	 *
	 * @param  [type] $object [description]
	 * @return [type]         [description]
	 */
	public function remove_from_stack( $object ) {
		do_action( 'jet-engine/object-stack/decrease', $object );
	}

	public function remove_object_from_stack( $content, $object ) {
		$this->remove_from_stack( $object );

		return $content;
	}

	public function get_object_type( $settings ) {
		return ! empty( $settings['query']['objectType'] ) ? $settings['query']['objectType'] : 'post';
	}

	/**
	 * Sets the default object in the Bricks loop during an API request.
	 *
	 * @param mixed $object The current loop object.
	 * @return mixed The Bricks loop object or the original object.
	 */
	function maybe_set_bricks_loop_object( $object ) {
		// Check if no object is set, Bricks is looping, and it's an API request
		if ( ! $object && \Bricks\Query::is_looping() && $this->is_bricks_api_request() ) {
			$looping_query_id = \Bricks\Query::is_any_looping();
			$object           = \Bricks\Query::get_loop_object( $looping_query_id );
		}

		return $object;
	}

	/**
	 * Checks if the current request is a Bricks API request.
	 *
	 * @return bool True if the request is to Bricks API.
	 */
	function is_bricks_api_request() {

		// phpcs:disable
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url( $_SERVER['REQUEST_URI'] ) : '';
		// phpcs:enable

		if ( empty( $request_uri ) ) {
			return false;
		}

		$request_uri         = parse_url( $request_uri, PHP_URL_PATH );
		$bricks_request_path = 'wp-json/bricks/v1/render_element';

		// Check if the request URI contains the API path at the end
		return ! empty( $request_uri ) && str_ends_with( rtrim( $request_uri, '/' ), $bricks_request_path );
	}
}