HEX
Server: Apache/2.4.65 (Debian)
System: Linux wordpress-7cb4c6b6f6-qgbk2 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/relations/ajax-handlers.php
<?php
namespace Jet_Engine\Relations;

use Jet_Engine\Relations\Storage\Ordering;

/**
 * Relations manager
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Define Relations Manager class
 */
class Ajax_Handlers {

	public function __construct() {

		$endpoints = array(
			'get_type_items',
			'get_related_items',
			'save_relation_meta',
			'get_related_item_meta',
			'update_relation_items',
			'reorder_relation_items',
			'disconnect_relation_items',
			'create_item_of_type',
			'reindex'
		);

		foreach ( $endpoints as $endpoint ) {
			add_action( 'wp_ajax_jet_engine_relations_' . $endpoint, array( $this, $endpoint ) );
		}

	}

	/**
	 * Change Rel ID column type fro text to varchar(40)
	 *
	 * @param  [type] $db_instance [description]
	 * @return [type]              [description]
	 */
	public function fix_rel_id_column_type( $db_instance ) {

		$table_name  = $db_instance->table();
		$column_name = 'rel_id';

		// Query to check the column type
		$query = $db_instance->wpdb()->prepare(
			"SELECT DATA_TYPE
			FROM information_schema.COLUMNS
			WHERE TABLE_SCHEMA = %s
			AND TABLE_NAME = %s
			AND COLUMN_NAME = %s",
			DB_NAME,
			$table_name,
			$column_name
		);

		$column = $db_instance->wpdb()->get_row( $query );

		// Check if the column type is TEXT
		if ( $column && 'text' === $column->DATA_TYPE ) {
			// Alter the column to VARCHAR(40)
			$alter_query = "ALTER TABLE `$table_name` MODIFY `$column_name` VARCHAR(40)";
			$db_instance->wpdb()->query( $alter_query );
		}

	}

	/**
	 * Reindex relations
	 *
	 * @return [type] [description]
	 */
	public function reindex() {

		$this->check_nonce();

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'You not allowed to do this', 'jet-engine' ) );
		}

		$relations   = jet_engine()->relations->get_active_relations();
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$relation_id = ! empty( $_REQUEST['relation'] ) ? absint( $_REQUEST['relation'] ) : false;
		$checked_default = [];

		if ( ! empty( $relations ) ) {

			foreach ( $relations as $relation ) {

				if ( $relation_id && absint( $relation->get_id() ) !== $relation_id ) {
					continue;
				}

				$data = [
					'main' => [
						'db_intance' => $relation->db,
						'drop'       => [
							"DROP INDEX parent_id ON " . '%1$s' . ";",
							"DROP INDEX child_id ON " . '%1$s' . ";",
						],
						'index_base' => [
							"CREATE INDEX parent_id ON " . '%1$s' . " ( rel_id, parent_object_id );",
							"CREATE INDEX child_id ON " . '%1$s' . " ( rel_id, child_object_id );"
						],
						'index_custom' => [
							"CREATE INDEX parent_id ON " . '%1$s' . " ( parent_object_id );",
							"CREATE INDEX child_id ON " . '%1$s' . " ( child_object_id );",
						],
					],
					'meta' => [
						'db_intance'   => $relation->meta_db,
						'drop'         => [
							"DROP INDEX meta_id ON " . '%1$s' . ";"
						],
						'index_base'   => [
							"CREATE INDEX meta_id ON " . '%1$s' . " ( rel_id, parent_object_id, child_object_id );"
						],
						'index_custom' => [
							"CREATE INDEX  meta_id ON " . '%1$s' . " ( parent_object_id, child_object_id );",
						],
					],
				];

				foreach ( $data as $key => $table_data ) {

					if ( $table_data['db_intance'] ) {

						if ( ! $table_data['db_intance']->is_table_exists() ) {
							$table_data['db_intance']->create_table();
						}

						$index_sql = [];
						$drop_sql  = [];

						if ( $relation->get_args( 'db_table' ) ) {
							$source = 'index_custom';
							$this->fix_rel_id_column_type( $table_data['db_intance'] );
						} else {

							$source = 'index_base';

							if ( ! in_array( $key, $checked_default ) ) {
								$this->fix_rel_id_column_type( $table_data['db_intance'] );
								$checked_default[] = $key;
							}

						}

						foreach ( $table_data[ $source ] as $row ) {
							$index_sql[] = sprintf( $row, $table_data['db_intance']->table() );
						}

						$index_sql = apply_filters( 'jet-engine/relations/db/index-sql', $index_sql, $relation, $key );

						foreach ( $table_data['drop'] as $row ) {
							$drop_sql[] = sprintf( $row, $table_data['db_intance']->table() );
						}

						foreach ( $drop_sql as $query ) {
							$table_data['db_intance']->raw_query( $query );
						}

						foreach ( $index_sql as $query ) {
							$table_data['db_intance']->raw_query( $query );
						}
					}
				}
			}
		}

		wp_send_json_success();
	}

	/**
	 * Returns error message
	 *
	 * @return [type] [description]
	 */
	public function get_error_message() {

		$message = wp_cache_get( 'jet-engine-relations-error' );

		if ( $message ) {
			wp_cache_delete( 'jet-engine-relations-error' );
		}

		return $message;
	}

	/**
	 * Create new item of given type
	 *
	 * @return [type] [description]
	 */
	public function create_item_of_type() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$related_object_type = ! empty( $_REQUEST['relatedObjectType'] ) ? sanitize_text_field( $_REQUEST['relatedObjectType'] ) : false;
		$related_object_name = ! empty( $_REQUEST['relatedObjectName'] ) ? sanitize_text_field( $_REQUEST['relatedObjectName'] ) : false;
		$data                = ! empty( $_REQUEST['item'] ) ? $_REQUEST['item'] : array();

		if ( ! $related_object_type || ! $related_object_name ) {
			wp_send_json_error( 'Incomplete request' );
		}

		$item_id = jet_engine()->relations->types_helper->create_item(
			jet_engine()->relations->types_helper->type_name_by_parts( $related_object_type, $related_object_name ),
			$data
		);

		if ( $item_id ) {
			wp_send_json_success( array( 'itemID' => $item_id ) );
		} else {

			$message = $this->get_error_message();

			if ( ! $message ) {
				$message = __( 'Can`t create a new item', 'jet-engine' );
			}

			wp_send_json_error( $message );

		}

	}

	/**
	 * Get items callback
	 *
	 * @return [type] [description]
	 */
	public function get_type_items() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		$type     = ! empty( $_GET['objectType'] ) ? sanitize_text_field( $_GET['objectType'] ) : false;
		$object   = ! empty( $_GET['object'] ) ? sanitize_text_field( $_GET['object'] ) : false;
		$existing = ! empty( $_REQUEST['existing'] ) ? $_REQUEST['existing'] : array();

		$relation = $this->get_relation_from_request();

		if ( ! $relation ) {
			wp_send_json_error( __( 'Relation ID not found in the reques', 'jet-engine' ) );
		}

		wp_send_json_success( jet_engine()->relations->types_helper->get_type_items( $type, $object, $relation, $existing ) );

	}

	/**
	 * Retrieve relation object from request data
	 *
	 * @return [type] [description]
	 */
	public function get_relation_from_request() {

		$rel_id = isset( $_REQUEST['relID'] ) ? sanitize_text_field( $_REQUEST['relID'] ) : false;

		if ( ! $rel_id ) {
			wp_send_json_error( __( 'Relation not found in the request', 'jet-engine' ) );
		}

		$relation = jet_engine()->relations->get_active_relations( $rel_id );

		if ( ! $relation ) {
			wp_send_json_error( __( 'Relation not found', 'jet-engine' ) );
		}

		return $relation;
	}

	/**
	 * Ajax callback to retrieve related items list
	 *
	 * @return [type] [description]
	 */
	public function get_related_items() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$related_object_type = ! empty( $_REQUEST['objectType'] ) ? sanitize_text_field( $_REQUEST['objectType'] ) : false;
		$related_object_name = ! empty( $_REQUEST['object'] ) ? sanitize_text_field( $_REQUEST['object'] ) : false;
		$current_object      = ! empty( $_REQUEST['currentObjectID'] ) ? sanitize_text_field( $_REQUEST['currentObjectID'] ) : false;

		if ( ! $related_object_type || ! $current_object || ! $related_object_name ) {
			wp_send_json_error( __( 'Incomplete request', 'jet-engine' ) );
		}

		$relation = $this->get_relation_from_request();

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( isset( $_REQUEST['isParentProcessed'] ) ) {
			$is_parent_processed = filter_var( wp_unslash( $_REQUEST['isParentProcessed'] ), FILTER_VALIDATE_BOOLEAN );
		} else {
			$is_parent_processed = $relation->is_parent( $related_object_type, $related_object_name );
		}

		if ( ! $relation->db->is_table_exists() ) {
			$relation->db->create_table();
		}

		wp_send_json_success( $this->get_related_list( $current_object, $relation, $is_parent_processed ) );

	}

	/**
	 * Process disconnect item request
	 *
	 * @return [type] [description]
	 */
	public function disconnect_relation_items() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		$data = $this->get_data_from_request();

		$relation            = $data['relation'];
		$parent_object_id    = $data['parent_object_id'];
		$child_object_id     = $data['child_object_id'];
		$is_parent_processed = $data['is_parent_processed'];
		$related_object_type = $data['related_object_type'];
		$related_object_name = $data['related_object_name'];
		$related_object      = $data['related_object'];
		$current_object      = $data['current_object'];
		$is_trash            = ! empty( $_POST['isTrash'] ) ? filter_var( $_POST['isTrash'], FILTER_VALIDATE_BOOLEAN ) : false;

		$relation->delete_rows( $parent_object_id, $child_object_id );

		$type = jet_engine()->relations->types_helper->type_name_by_parts( $related_object_type, $related_object_name );

		if ( $is_trash && jet_engine()->relations->types_helper->current_user_can( 'delete', $type, $related_object, $related_object_name ) ) {
			jet_engine()->relations->types_helper->delete_item( $type, $related_object );
		}

		wp_send_json_success( array(
			'related_list' => $this->get_related_list( $current_object, $relation, $is_parent_processed )
		) );

	}

	/**
	 * save_relation_meta callback
	 *
	 * @return [type] [description]
	 */
	public function save_relation_meta() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		$data = $this->get_data_from_request();

		$relation            = $data['relation'];
		$parent_object_id    = $data['parent_object_id'];
		$child_object_id     = $data['child_object_id'];
		$is_parent_processed = $data['is_parent_processed'];

		$meta = isset( $_POST['meta'] ) ? $_POST['meta'] : array();

		$relation->update_all_meta( $meta, $parent_object_id, $child_object_id );

		wp_send_json_success();

	}

	public function get_related_item_meta() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		$data = $this->get_data_from_request();

		$relation            = $data['relation'];
		$parent_object_id    = $data['parent_object_id'];
		$child_object_id     = $data['child_object_id'];
		$is_parent_processed = $data['is_parent_processed'];

		if ( ! $relation->meta_db->is_table_exists() ) {
			$relation->meta_db->create_table();
		}

		$meta = isset( $_POST['meta'] ) ? $_POST['meta'] : array();

		wp_send_json_success( $relation->format_meta( $relation->get_all_meta( $parent_object_id, $child_object_id ) ) );
	}

	/**
	 * Returns typical data required for most of callbacks
	 *
	 * @return [type] [description]
	 */
	public function get_data_from_request() {

		$related_object      = ! empty( $_REQUEST['relatedObjectID'] ) ? sanitize_text_field( $_REQUEST['relatedObjectID'] ) : false;
		$related_object_type = ! empty( $_REQUEST['relatedObjectType'] ) ? sanitize_text_field( $_REQUEST['relatedObjectType'] ) : false;
		$related_object_name = ! empty( $_REQUEST['relatedObjectName'] ) ? sanitize_text_field( $_REQUEST['relatedObjectName'] ) : false;
		$current_object      = ! empty( $_REQUEST['currentObjectID'] ) ? sanitize_text_field( $_REQUEST['currentObjectID'] ) : false;

		if ( ! $related_object || ! $related_object_type || ! $current_object || ! $related_object_name ) {
			wp_send_json_error( __( 'Incomplete request', 'jet-engine' ) );
		}

		$relation = $this->get_relation_from_request();

		if ( isset( $_REQUEST['isParentProcessed'] ) ) {
			$is_parent_processed = filter_var( $_REQUEST['isParentProcessed'], FILTER_VALIDATE_BOOLEAN );
		} else {
			$is_parent_processed = $relation->is_parent( $related_object_type, $related_object_name );
		}

		if ( $is_parent_processed ) {
			$parent_object_id = $related_object;
			$child_object_id  = $current_object;
			$current_type     = $relation->get_args( 'child_object' );
		} else {
			$parent_object_id = $current_object;
			$child_object_id  = $related_object;
			$current_type     = $relation->get_args( 'parent_object' );
		}

		$current_object_type = jet_engine()->relations->types_helper->type_parts_by_name( $current_type );

		if ( ! jet_engine()->relations->types_helper->current_user_can( 'edit', $current_type, $current_object, $current_object_type[1] ) ) {
			wp_send_json_error( __( 'You are not allowed to do this', 'jet-engine' ) );
		}

		return array(
			'relation'            => $relation,
			'is_parent_processed' => $is_parent_processed,
			'parent_object_id'    => $parent_object_id,
			'child_object_id'     => $child_object_id,
			'related_object'      => $related_object,
			'related_object_type' => $related_object_type,
			'related_object_name' => $related_object_name,
			'current_object'      => $current_object,
		);
	}

	/**
	 * Reorder items callback
	 *
	 * @return [type] [description]
	 */
	public function reorder_relation_items() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		$data = $this->get_data_from_request();

		$relation            = $data['relation'];
		$parent_object_id    = $data['parent_object_id'];
		$child_object_id     = $data['child_object_id'];
		$is_parent_processed = $data['is_parent_processed'];
		$current_object      = $data['current_object'];
		$items               = ! empty( $_REQUEST['itemsOrder'] ) ? $_REQUEST['itemsOrder'] : false;

		if ( empty( $items ) ) {
			wp_send_json_success(
				$this->get_related_list( $current_object, $relation, $is_parent_processed )
			);
		}

		Ordering::instance()->reorder_relation_items( $relation, $items );

		wp_send_json_success(
			$this->get_related_list( $current_object, $relation, $is_parent_processed )
		);
	}

	/**
	 * Update items callback
	 *
	 * @return [type] [description]
	 */
	public function update_relation_items() {

		// verify nonce and abort if not
		$this->check_nonce();

		// verify user access
		$this->check_user_access();

		$data = $this->get_data_from_request();

		$relation            = $data['relation'];
		$parent_object_id    = $data['parent_object_id'];
		$child_object_id     = $data['child_object_id'];
		$is_parent_processed = $data['is_parent_processed'];
		$current_object      = $data['current_object'];

		// if we currently processing parent - this means update comes from child object page
		if ( $is_parent_processed ) {
			$relation->set_update_context( 'parent' );
		} else {
			$relation->set_update_context( 'child' );
		}

		$_id = $relation->update( $parent_object_id, $child_object_id );

		wp_send_json_success( array(
			'related_item' => $_id,
			'related_list' => $this->get_related_list( $current_object, $relation, $is_parent_processed )
		) );

	}

	/**
	 * Returns correctly fromatted related items list for editor
	 *
	 * @param  [type]  $current_object      [description]
	 * @param  [type]  $relation            [description]
	 * @param  boolean $is_parent_processed [description]
	 * @return [type]                       [description]
	 */
	public function get_related_list( $current_object, $relation, $is_parent_processed = false ) {

		$related_list = $is_parent_processed ? $relation->get_parents( $current_object ) : $relation->get_children( $current_object );

		return array_map( function( $item ) use ( $is_parent_processed, $relation ) {

			if ( $is_parent_processed ) {
				$item_id    = $item['parent_object_id'];
				$current_id = $item['child_object_id'];
				$type       = $relation->get_args( 'parent_object' );
				$allow_del  = $relation->get_args( 'child_allow_delete' );
			} else {
				$item_id    = $item['child_object_id'];
				$current_id = $item['parent_object_id'];
				$type       = $relation->get_args( 'child_object' );
				$allow_del  = $relation->get_args( 'parent_allow_delete' );
			}

			$columns   = $relation->get_table_columns_for_object( $type );
			$type_data = jet_engine()->relations->types_helper->type_parts_by_name( $type );
			unset( $columns['actions'] );

			if ( isset( $columns['meta'] ) ) {
				unset( $columns['meta'] );
			}

			$item['columns'] = $this->get_columns_contents( $columns, $type, $item_id, $relation, $current_id );
			$item['actions'] = array(
				'view'       => jet_engine()->relations->types_helper->get_type_item_view_url( $type, $item_id, $relation ),
				'disconnect' => true,
			);

			if ( jet_engine()->relations->types_helper->current_user_can( 'edit', $type, $item_id, $type_data[1] ) ) {
				$item['actions']['edit'] = jet_engine()->relations->types_helper->get_type_item_edit_url( $type, $item_id, $relation );
			}

			if ( $allow_del && jet_engine()->relations->types_helper->current_user_can( 'delete', $type, $item_id, $type_data[1] ) ) {
				$item['actions']['trash'] = true;
			}

			$item['related_id'] = $item_id;
			$item['current_id'] = $current_id;

			return $item;

		}, $related_list );
	}

	/**
	 * Returns array wiith content of table columns for given item
	 *
	 * @param  array $columns     Array of table columns
	 * @param  string $type       Relation type
	 * @param  string $item_id    Related item ID
	 * @param  Relation $relation Relation instance
	 * @param  string $current_id Current object ID
	 *
	 * @return array              Columns content
	 */
	public function get_columns_contents( $columns, $type, $item_id, $relation, $current_id ) {

		$result = array();

		foreach ( $columns as $key => $label ) {

			switch ( $key ) {
				case 'title':
					$result[] = jet_engine()->relations->types_helper->get_type_item_title( $type, $item_id, $relation );
					break;

				default:

					$column = $relation->get_object_column( $type, $key );

					if ( $column && ! empty( $column['callback'] ) && is_callable( $column['callback'] ) ) {
						$result[] = call_user_func( $column['callback'], $item_id, $current_id, $relation, $type );
					} else {
						$result[] = apply_filters( 'jet-engine/relations/column-content/' . $key, '', $item_id, $type, $relation );
					}

					break;
			}
		}

		return $result;

	}

	/**
	 * Verify request nonce
	 *
	 * @return void
	 */
	public function check_nonce() {

		$nonce = ! empty( $_REQUEST['_nonce'] ) ? $_REQUEST['_nonce'] : false;

		if ( ! wp_verify_nonce( $nonce, 'jet-engine-relations-control' ) ) {
			wp_send_json_error( __( 'The link is expired, please reload the page and try again', 'jet-engine' ) );
		}

	}

	/**
	 * Check user access
	 *
	 * @return void
	 */
	public function check_user_access() {

		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error( __( 'Access denied', 'jet-engine' ) );
		}

	}
}