HEX
Server: LiteSpeed
System: Linux server315.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: globfdxw (6114)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: //home/globfdxw/www/wp-content/plugins/kirki/includes/Ajax/Collaboration/Collaboration.php
<?php
/**
 * Collaboration controller
 *
 * @package kirki
 */

namespace Kirki\Ajax\Collaboration;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use Kirki\HelperFunctions;

/**
 * Collaboration class for running and getting the collaboration process.
 */
class Collaboration {

	/**
	 * This method will trigger from builder ajax actions.
	 * This method will save all type and action related data inside data column.
	 *
	 * @return void wp_send_json
	 */
	public static function save_actions() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$data = isset( $_POST['data'] ) ? $_POST['data'] : null;
		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
		$session_id = HelperFunctions::sanitize_text( isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '' );
		if ( empty( $data ) ) {
			wp_send_json_error( array( 'message' => 'No data received' ) );
		}
		// Decode the JSON data
		$data = json_decode( stripslashes( $data ), true );

		$results = array();

		// Handle both single action and batch of actions
		if ( isset( $data[0] ) && is_array( $data[0] ) ) {
			// Batch mode
			foreach ( $data as $item ) {
				$parent    = isset( $item['parent'] ) ? $item['parent'] : '';
				$parent_id = isset( $item['parent_id'] ) ? $item['parent_id'] : '';
				$action    = isset( $item['action'] ) ? $item['action'] : '';
				$status    = 1;

				$result = self::save_action_to_db( $parent, $parent_id, $action, $status, $session_id );
				if ( $result ) {
					$results[] = $result;
				}
			}
		} else {
			// Single action (backward compatible)
			$parent    = isset( $data['parent'] ) ? $data['parent'] : '';
			$parent_id = isset( $data['parent_id'] ) ? $data['parent_id'] : '';
			$action    = isset( $data['action'] ) ? $data['action'] : '';
			$status    = 1;

			$result = self::save_action_to_db( $parent, $parent_id, $action, $status, $session_id );
			if ( $result ) {
				$results[] = $result;
			}
		}

		wp_send_json_success( $results );
	}


	/**
	 * Save action to db function will save a event if more than one people connected.
	 *
	 * @param string $parent post | styleblock for now.
	 * @param string $parent_id post id or 0 if global data.
	 * @param array  $data total event data.
	 * @param int    $status event status.
	 *
	 * @return bool,array if success.
	 */
	public static function save_action_to_db( $parent, $parent_id, $data, $status = 1, $session_id = '', $cleanup = true ) {
		if ( count( self::get_all_connected_rows( $cleanup ) ) > 1 ) {
			$user_id = get_current_user_id();

			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$wpdb->insert(
				$wpdb->prefix . KIRKI_COLLABORATION_TABLE,
				array(
					'user_id'    => (int) $user_id,
					'session_id' => $session_id,
					'parent'     => $parent,
					'parent_id'  => (int) $parent_id,
					'data'       => wp_json_encode( $data ),
					'status'     => (int) $status,
				),
				array(
					'%d',
					'%s',
					'%s',
					'%d',
					'%s',
					'%d',
				)
			);
			return array(
				'id'         => $wpdb->insert_id,
				'session_id' => $session_id,
			);
		}

		return false;
	}

	/**
	 * Send action is The main eventsouce function.
	 * this method will start the eventsouce mechanism.
	 *
	 * @return void
	 */
	public static function send_actions() {
		self::save_connection_data();
		$sender = new Sender();
		$sender->start();
		self::clean_disconnected_rows();
		exit();
	}

	/**
	 * Save connection data
	 */
	private static function save_connection_data() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$session_id = HelperFunctions::sanitize_text( sanitize_text_field( wp_unslash( $_GET['session_id'] ) ) );
		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$post_id = HelperFunctions::sanitize_text( sanitize_text_field( wp_unslash( $_GET['post_id'] ) ) );
		$user_id = get_current_user_id();

		if ( ! self::get_connection( $session_id ) ) {
			self::add_connection( $user_id, $session_id, $post_id );
		}

	}

	/**
	 * Add connection if a user is give eventsource request.
	 *
	 * @param int    $user_id wp user id.
	 * @param string $session_id current user session id.
	 * @param int    $post_id current post id.
	 */
	private static function add_connection( $user_id, $session_id, $post_id ) {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->insert(
			$wpdb->prefix . KIRKI_COLLABORATION_TABLE . '_connected',
			array(
				'user_id'    => (int) $user_id,
				'post_id'    => (int) $post_id,
				'session_id' => $session_id,
			),
			array(
				'%d',
				'%d',
				'%s',
			)
		);
		if ( $wpdb->insert_id ) {
			// TODO: send connection id to all connected users for add
			$this_connection = self::get_connection( $session_id );

			$data = array(
				'type'    => 'COLLABORATION_ADD_CONNECTION',
				'payload' => array( 'data' => $this_connection ),
			);
			self::save_action_to_db( 'post', $post_id, $data, 1, $session_id );

			return $this_connection;
		}
		return false;
	}

	/**
	 * Get single connection.
	 *
	 * @param string $session_id current user session id.
	 *
	 * @return bool,array
	 */
	public static function get_connection( $session_id ) {
		global $wpdb;
		$query = $wpdb->prepare(
			'SELECT * FROM %1s WHERE session_id = %s',
			$wpdb->prefix . KIRKI_COLLABORATION_TABLE . '_connected',
			$session_id
		);
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		$this_connection = $wpdb->get_row( $query );
		if ( $this_connection ) {
			$date = gmdate( 'Y-m-d H:i:s' );
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->update(
				$wpdb->prefix . KIRKI_COLLABORATION_TABLE . '_connected',
				array(
					'updated_at' => $date,
				),
				array(
					'session_id' => $session_id,
				),
				array(
					'%s',
				),
				array(
					'%s',
				)
			);
			return self::format_connection_data( $this_connection );
		}
		return false;
	}

	/**
	 * Get all connected rows.
	 * first clean disconnected rows then get only recent connected rows.
	 *
	 * @param bool $cleanup if true will clean disconnected rows.
	 * @return array
	 */
	public static function get_all_connected_rows( $cleanup = true ) {
		if ( $cleanup ) {
			self::clean_disconnected_rows();
		}
		global $wpdb;
		$query2 = $wpdb->prepare(
			'SELECT * FROM %1s',
			$wpdb->prefix . KIRKI_COLLABORATION_TABLE . '_connected'
		);
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		$res = $wpdb->get_results( $query2 );
		return $res;
	}

	public static function get_connected_collaboration_users_list( $post_id ) {
		global $wpdb;
		$query = $wpdb->prepare(
			'SELECT * FROM %1s WHERE post_id = %d',
			$wpdb->prefix . KIRKI_COLLABORATION_TABLE . '_connected',
			$post_id
		);
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		$res = $wpdb->get_results( $query );

		foreach ( $res as $key => $connection ) {
			$res[ $key ] = self::format_connection_data( $connection );
		}
		return $res;
	}

	private static function format_connection_data( $connection ) {
		$connection->user_name = get_the_author_meta( 'display_name', $connection->user_id );
		return $connection;
	}

	/**
	 * Clean disconnected rows if inactive less then 50 seconds.
	 *
	 * @return void
	 */
	public static function clean_disconnected_rows() {
		global $wpdb;

		$fifty_seconds_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-20 seconds' ) );
		$table_name        = $wpdb->prefix . KIRKI_COLLABORATION_TABLE . '_connected';

		// Get all expired connections (session_id + post_id in one query)
		$query = $wpdb->prepare(
			"SELECT session_id, post_id FROM $table_name WHERE updated_at <= %s",
			$fifty_seconds_ago
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		$connections = $wpdb->get_results( $query );

		if ( empty( $connections ) ) {
			return;
		}

		// Step 1: Broadcast removal for each connection
		foreach ( $connections as $connection ) {
			$data = array(
				'type'    => 'COLLABORATION_REMOVE_CONNECTION',
				'payload' => array( 'session_id' => $connection->session_id ),
			);
			self::save_action_to_db( 'post', $connection->post_id, $data, 1, $connection->session_id, false );
		}

		// Step 2: Bulk delete all expired sessions in one query
		$session_ids  = wp_list_pluck( $connections, 'session_id' );
		$placeholders = implode( ',', array_fill( 0, count( $session_ids ), '%s' ) );

		$delete_sql = $wpdb->prepare(
			"DELETE FROM $table_name WHERE session_id IN ($placeholders)",
			$session_ids
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query( $delete_sql );
	}

	/**
	 * Delete connection if a user gives event close request.
	 *
	 * @param string $session_id current user session id.
	 * @param bool   $send       whether to broadcast removal.
	 */
	public static function delete_connection( $session_id ) {
		global $wpdb;

		$table_name = $wpdb->prefix . KIRKI_COLLABORATION_TABLE . '_connected';

		$query = $wpdb->prepare(
			"SELECT * FROM $table_name WHERE session_id = %s",
			$session_id
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		$this_connection = $wpdb->get_row( $query );

		if ( $this_connection ) {
			$post_id = $this_connection->post_id;
			$data    = array(
				'type'    => 'COLLABORATION_REMOVE_CONNECTION',
				'payload' => array( 'session_id' => $session_id ),
			);

			self::save_action_to_db( 'post', $post_id, $data, 1, $session_id );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->delete( $table_name, array( 'session_id' => $session_id ) );
	}

}