Source: includes/event-utilities.php

<?php
/**
 * Event utilities. Event functions not directly related to display or management.
 *
 * @category Utilities
 * @package  My Calendar
 * @author   Joe Dolson
 * @license  GPLv2 or later
 * @link     https://www.joedolson.com/my-calendar/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Check an event for any occurrence overlap problems. Used in admin only.
 *
 * @param integer $event_id Event ID.
 *
 * @return string with edit link to go to event.
 */
function mc_error_check( $event_id ) {
	$data      = mc_form_data( $event_id );
	$test      = ( $data ) ? mc_test_occurrence_overlap( $data, true ) : '';
	$args      = array(
		'mode'     => 'edit',
		'event_id' => $event_id,
	);
	$edit_link = ' <a href="' . esc_url( add_query_arg( $args, admin_url( 'admin.php?page=my-calendar' ) ) ) . '">' . __( 'Edit Event', 'my-calendar' ) . '</a>';
	$test      = ( '' !== $test ) ? str_replace( '</p></div>', "$edit_link</p></div>", $test ) : $test;

	return $test;
}

/**
 * Test whether an event has an invalid overlap.
 *
 * @param object  $data Event object.
 * @param boolean $should_return Return or echo.
 *
 * @return string|void Warning text about problem with event.
 */
function mc_test_occurrence_overlap( $data, $should_return = false ) {
	$warning = '';
	// If this event is single, skip query.
	$single_recur = ( 'S' === $data->event_recur || 'S1' === $data->event_recur ) ? true : false;
	// If event starts and ends on same day, skip query.
	$start_end = ( $data->event_begin === $data->event_end ) ? true : false;
	// Only run test when an event is set up to recur & starts/ends on different days.
	if ( ! $single_recur && ! $start_end ) {
		$check = mc_increment_event( $data->event_id, array(), true );
		if ( my_calendar_date_xcomp( $check['occur_begin'], $data->event_end . ' ' . $data->event_endtime ) ) {
			$warning = "<div class='error'><span class='problem-icon dashicons dashicons-performance' aria-hidden='true'></span> <p><strong>" . __( 'Event hidden from public view.', 'my-calendar' ) . '</strong> ' . __( 'This event ends after the next occurrence begins. Events must end <strong>before</strong> the next occurrence begins.', 'my-calendar' ) . '</p><p>';
			$enddate = date_i18n( mc_date_format(), strtotime( $data->event_end ) );
			$endtime = mc_date( mc_time_format(), strtotime( $data->event_endtime ), false );
			$begin   = date_i18n( mc_date_format(), strtotime( $check['occur_begin'] ) ) . ' ' . mc_date( mc_time_format(), strtotime( $check['occur_begin'] ), false );
			// Translators: End date, end time, beginning of next event.
			$warning .= sprintf( __( 'Event end date: <strong>%1$s %2$s</strong>. Next occurrence starts: <strong>%3$s</strong>', 'my-calendar' ), $enddate, $endtime, $begin ) . '</p></div>';
			update_post_meta( $data->event_post, '_occurrence_overlap', 'false' );
		} else {
			delete_post_meta( $data->event_post, '_occurrence_overlap' );
		}
	} else {
		// If event has been changed to same date, still delete meta.
		delete_post_meta( $data->event_post, '_occurrence_overlap' );
	}
	if ( $should_return ) {
		return $warning;
	} else {
		echo wp_kses_post( $warning );
	}

	return $warning;
}

/**
 * Find event that conflicts with newly scheduled events based on time and location.
 *
 * @param string $begin date of event.
 * @param string $time time of event.
 * @param string $end date of event.
 * @param string $endtime time of event.
 * @param int    $loc_id location of event.
 *
 * @return mixed results array or false
 */
function mcs_check_conflicts( $begin, $time, $end, $endtime, $loc_id ) {
	global $wpdb;
	$select_location = ( $loc_id ) ? "event_location = '" . absint( $loc_id ) . "' AND" : '';
	$begin_time      = $begin . ' ' . $time;
	$end_time        = $end . ' ' . $endtime;
	// Need two queries; one to find outer events, one to find inner events.
	$event_query = 'SELECT occur_id
					FROM ' . my_calendar_event_table() . '
					JOIN ' . my_calendar_table() . "
					ON (event_id=occur_event_id)
					WHERE $select_location " . '
					( occur_begin BETWEEN cast( \'%1$s\' AS DATETIME ) AND cast( \'%2$s\' AS DATETIME )
					OR occur_end BETWEEN cast( \'%3$s\' AS DATETIME ) AND cast( \'%4$s\' AS DATETIME ) )';

	$results = $wpdb->get_results( $wpdb->prepare( $event_query, $begin_time, $end_time, $begin_time, $end_time ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

	if ( empty( $results ) ) {
		// Finds events that conflict if they either start or end during another event.
		$event_query2 = 'SELECT occur_id
						FROM ' . my_calendar_event_table() . '
						JOIN ' . my_calendar_table() . "
						ON (event_id=occur_event_id)
						WHERE $select_location " . '
						( cast( \'%1$s\' AS DATETIME ) BETWEEN occur_begin AND occur_end
						OR cast( \'%2$s\' AS DATETIME ) BETWEEN occur_begin AND occur_end )';

		$results = $wpdb->get_results( $wpdb->prepare( $event_query2, $begin_time, $end_time ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	return ( ! empty( $results ) ) ? $results : false;
}

/**
 * Get all states, labels, types. Valid types are 'hidden', 'public', and 'private'. Hidden statuses
 * are not output to public APIs or shown on calendars at all. Private statuses are shown to logged-in users.
 *
 * @return array
 */
function mc_event_states() {
	$states = array(
		'0' => array(
			'type'  => 'hidden',
			'label' => __( 'Draft', 'my-calendar' ),
		),
		'1' => array(
			'type'  => 'public',
			'label' => __( 'Published', 'my-calendar' ),
		),
		'2' => array(
			'type'  => 'hidden',
			'label' => __( 'Trash', 'my-calendar' ),
		),
		'3' => array(
			'type'  => 'public',
			'label' => __( 'Cancelled', 'my-calendar' ),
		),
		'4' => array(
			'type'  => 'private',
			'label' => __( 'Private', 'my-calendar' ),
		),
	);

	/**
	 * Filter available event status types.
	 *
	 * @hook mc_event_states
	 *
	 * @param {array} Array of states where key is the integer value of the
	 *                state and the value is an array with type and label.
	 *
	 * @return {array}
	 */
	$states = apply_filters( 'mc_event_states', $states );

	return $states;
}

/**
 * Get an array of states by type.
 *
 * @param  string $type 'public', 'private', or, 'hidden'.
 *
 * @return array
 */
function mc_event_states_by_type( $type ) {
	$statuses = mc_event_states();
	$values   = array();
	foreach ( $statuses as $key => $value ) {
		if ( $type === $value['type'] ) {
			$values[] = $key;
		}
	}

	/**
	 * Filter the display type for event states.
	 *
	 * @hook mc_event_states_by_type
	 *
	 * @param {array}  $values Array of integers representing the event states that match the passed type.
	 * @param {string} $type Publication type requested.
	 */
	return apply_filters( 'mc_event_states_by_type', $values, $type );
}

/**
 * Get the publication type of a status.
 *
 * @param int $state An integer status value.
 *
 * @return string 'public', 'private', or 'hidden'.
 */
function mc_event_states_type( $state ) {
	$states = mc_event_states();
	$return = $states[ $state ]['type'];

	/**
	 * Filter the display conditions of an event status. Events can either be public; private; or hidden.
	 * Public events are visible to all; private events are visible to logged-in users; and hidden events are not visible.
	 *
	 * @hook mc_event_states_type
	 *
	 * @param {string} $return Type for the current status.
	 * @param {int}    $states An integer representation of a status.
	 *
	 * @return {string}
	 */
	return apply_filters( 'mc_event_states_type', $return, $states );
}


/**
 * Get the label for an event state.
 *
 * @param int $state An integer state value.
 *
 * @return string Label for this state.
 */
function mc_event_states_label( $state ) {
	$states = mc_event_states();
	$return = $states[ $state ]['label'];

	/**
	 * Filter the label for an event state.
	 *
	 * @hook mc_event_states_label
	 *
	 * @param {string} $return Type for the current status.
	 * @param {int}    $states An integer representation of a status.
	 *
	 * @return {string}
	 */
	return apply_filters( 'mc_event_states_label', $return, $states );
}