Source: my-calendar-widgets.php

<?php
/**
 * Construct widgets. Incorporate widget classes & supporting widget functions.
 *
 * @category Calendar
 * @package  My Calendar
 * @author   Joe Dolson
 * @license  GPLv2 or later
 * @link     https://www.joedolson.com/my-calendar/
 */

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

include( dirname( __FILE__ ) . '/includes/widgets/class-my-calendar-simple-search.php' );
include( dirname( __FILE__ ) . '/includes/widgets/class-my-calendar-filters.php' );
include( dirname( __FILE__ ) . '/includes/widgets/class-my-calendar-today-widget.php' );
include( dirname( __FILE__ ) . '/includes/widgets/class-my-calendar-upcoming-widget.php' );
include( dirname( __FILE__ ) . '/includes/widgets/class-my-calendar-mini-widget.php' );

/**
 * Generate the widget output for upcoming events.
 *
 * @param array $args Event selection arguments.
 *
 * @return String HTML output list.
 */
function my_calendar_upcoming_events( $args ) {
	$language = isset( $args['language'] ) ? $args['language'] : '';
	$switched = '';
	if ( $language ) {
		$locale   = get_locale();
		$switched = mc_switch_language( $locale, $language );
	}
	$before         = ( isset( $args['before'] ) ) ? $args['before'] : 'default';
	$after          = ( isset( $args['after'] ) ) ? $args['after'] : 'default';
	$type           = ( isset( $args['type'] ) ) ? $args['type'] : 'default';
	$category       = ( isset( $args['category'] ) ) ? $args['category'] : 'default';
	$template       = ( isset( $args['template'] ) ) ? $args['template'] : 'default';
	$substitute     = ( isset( $args['fallback'] ) ) ? $args['fallback'] : '';
	$order          = ( isset( $args['order'] ) ) ? $args['order'] : 'asc';
	$skip           = ( isset( $args['skip'] ) ) ? $args['skip'] : 0;
	$show_today     = ( isset( $args['show_today'] ) ) ? $args['show_today'] : 'yes';
	$show_recurring = ( isset( $args['show_recurring'] ) ) ? $args['show_recurring'] : 'yes';
	$author         = ( isset( $args['author'] ) ) ? $args['author'] : 'default';
	$host           = ( isset( $args['host'] ) ) ? $args['host'] : 'default';
	$ltype          = ( isset( $args['ltype'] ) ) ? $args['ltype'] : '';
	$lvalue         = ( isset( $args['lvalue'] ) ) ? $args['lvalue'] : '';
	$from           = ( isset( $args['from'] ) ) ? $args['from'] : '';
	$to             = ( isset( $args['to'] ) ) ? $args['to'] : '';
	$site           = ( isset( $args['site'] ) ) ? $args['site'] : false;

	if ( $site ) {
		$site = ( 'global' === $site ) ? BLOG_ID_CURRENT_SITE : $site;
		switch_to_blog( $site );
	}

	$hash         = md5( implode( ',', $args ) );
	$output       = '';
	$defaults     = mc_widget_defaults();
	$display_type = ( 'default' === $type ) ? $defaults['upcoming']['type'] : $type;
	$display_type = ( '' === $display_type ) ? 'events' : $display_type;

	// Get number of units we should go into the future.
	$after = ( 'default' === $after ) ? $defaults['upcoming']['after'] : $after;
	$after = ( '' === $after ) ? 10 : $after;

	// Get number of units we should go into the past.
	$before   = ( 'default' === $before ) ? $defaults['upcoming']['before'] : $before;
	$before   = ( '' === $before ) ? 0 : $before;
	$category = ( 'default' === $category ) ? '' : $category;

	/**
	 * Pass a custom template to the upcoming events list. Template can either be a template key referencing a stored template or a template pattern using {} template tags.
	 *
	 * @hook mc_upcoming_events_template
	 *
	 * @param {string} $template Un-parsed template.
	 *
	 * @return {string} Template string.
	 */
	$template = apply_filters( 'mc_upcoming_events_template', $template );

	// allow reference by file to external template.
	if ( '' !== $template && mc_file_exists( $template ) ) {
		$template = file_get_contents( mc_get_file( $template ) );
	}

	$template = ( ! $template || 'default' === $template ) ? $defaults['upcoming']['template'] : $template;
	if ( mc_key_exists( $template ) ) {
		$template = mc_get_custom_template( $template );
	}

	$no_event_text  = ( '' === $substitute ) ? $defaults['upcoming']['text'] : $substitute;
	$lang           = ( $switched ) ? ' lang="' . esc_attr( $switched ) . '"' : '';
	$header         = "<ul id='upcoming-events-$hash' class='upcoming-events'$lang>";
	$footer         = '</ul>';
	$display_events = ( 'events' === $display_type || 'event' === $display_type ) ? true : false;
	if ( ! $display_events ) {
		$temp_array = array();
		if ( 'days' === $display_type ) {
			$from = mc_date( 'Y-m-d', strtotime( "-$before days" ), false );
			$to   = mc_date( 'Y-m-d', strtotime( "+$after days" ), false );
		}
		if ( 'month' === $display_type ) {
			$from = mc_date( 'Y-m-1' );
			$to   = mc_date( 'Y-m-t' );
		}
		if ( 'custom' === $display_type && '' !== $from && '' !== $to ) {
			$from = mc_date( 'Y-m-d', strtotime( $from ), false );
			$to   = ( 'today' === $to ) ? current_time( 'Y-m-d' ) : mc_date( 'Y-m-d', strtotime( $to ), false );
		}
		/* Yes, this is crude. But there are only 12 possibilities, after all. */
		if ( 'month+1' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+1 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+1 month' ), false );
		}
		if ( 'month+2' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+2 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+2 month' ), false );
		}
		if ( 'month+3' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+3 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+3 month' ), false );
		}
		if ( 'month+4' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+4 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+4 month' ), false );
		}
		if ( 'month+5' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+5 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+5 month' ), false );
		}
		if ( 'month+6' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+6 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+6 month' ), false );
		}
		if ( 'month+7' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+7 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+7 month' ), false );
		}
		if ( 'month+8' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+8 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+8 month' ), false );
		}
		if ( 'month+9' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+9 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+9 month' ), false );
		}
		if ( 'month+10' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+10 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+10 month' ), false );
		}
		if ( 'month+11' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+11 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+11 month' ), false );
		}
		if ( 'month+12' === $display_type ) {
			$from = mc_date( 'Y-m-1', strtotime( '+12 month' ), false );
			$to   = mc_date( 'Y-m-t', strtotime( '+12 month' ), false );
		}
		if ( 'year' === $display_type ) {
			$from = mc_date( 'Y-1-1' );
			$to   = mc_date( 'Y-12-31' );
		}
		/**
		 * Custom upcoming events date start value for upcoming events lists using date parameters.
		 *
		 * @hook mc_upcoming_date_from
		 *
		 * @param {string} $from Starting date for this list of upcoming events in Y-m-d format.
		 * @param {array}  $args Associative array holding the arguments used to generate this list of upcoming events.
		 *
		 * @return {string} List starting date.
		 */
		$from = apply_filters( 'mc_upcoming_date_from', $from, $args );
		/**
		 * Custom upcoming events date end value for upcoming events lists using date parameters.
		 *
		 * @hook mc_upcoming_date_to
		 *
		 * @param {string} $to Ending date for this list of upcoming events in Y-m-d format.
		 * @param {array}  $args Associative array holding the arguments used to generate this list of upcoming events.
		 *
		 * @return {string} List ending date.
		 */
		$to = apply_filters( 'mc_upcoming_date_to', $to, $args );

		$query = array(
			'from'     => $from,
			'to'       => $to,
			'category' => $category,
			'ltype'    => $ltype,
			'lvalue'   => $lvalue,
			'author'   => $author,
			'host'     => $host,
			'search'   => '',
			'source'   => 'upcoming',
			'site'     => $site,
		);
		/**
		 * Modify the arguments used to generate upcoming events.
		 *
		 * @hook mc_upcoming_attributes
		 *
		 * @param {array} $query All arguments used to generate this list.
		 * @param {array} $args Subset of parameters used to generate this list's ID hash.
		 *
		 * @return {array} Array of event listing arguments.
		 */
		$query       = apply_filters( 'mc_upcoming_attributes', $query, $args );
		$event_array = my_calendar_events( $query );

		if ( 0 !== count( $event_array ) ) {
			foreach ( $event_array as $key => $value ) {
				if ( is_array( $value ) ) {
					foreach ( $value as $k => $v ) {
						if ( mc_private_event( $v ) ) {
							// this event is private.
						} else {
							$temp_array[] = $v;
						}
					}
				}
			}
		}
		$i         = 0;
		$last_item = '';
		$skips     = array();
		$omit      = array();
		foreach ( reverse_array( $temp_array, true, $order ) as $event ) {
			$details = mc_create_tags( $event );
			/**
			 * Draw a custom template for upcoming events. Returning any non-empty string short circuits other template settings.
			 *
			 * @hook mc_draw_upcoming_event
			 *
			 * @param {string} $item Empty string before event template is drawn.
			 * @param {array}  $details Associative array of event template tags.
			 * @param {string} $template Template string passed from widget or shortcode.
			 * @param {array}  $args Associative array holding the arguments used to generate this list of upcoming events.
			 *
			 * @return {string} Event template details.
			 */
			$item = apply_filters( 'mc_draw_upcoming_event', '', $details, $template, $args );
			// if an event is a multidate group, only display first found.
			if ( in_array( $event->event_group_id, $omit, true ) ) {
				continue;
			}
			if ( '1' === $event->event_span ) {
				$omit[] = $event->event_group_id;
			}
			if ( '' === $item ) {
				$item = mc_draw_template( $details, $template, 'list', $event );
			}
			if ( $i < $skip && 0 !== $skip ) {
				$i ++;
			} else {
				$today   = current_time( 'Y-m-d H:i' );
				$date    = mc_date( 'Y-m-d H:i', strtotime( $details['dtstart'], false ) );
				$classes = mc_event_classes( $event, 'upcoming' );
				$prepend = "<li class='$classes'>";
				$append  = '</li>';
				/**
				 * Opening elements for upcoming events list items. Default `<li class="$classes">`.
				 *
				 * @hook mc_event_upcoming_before
				 *
				 * @param {string} $append Template HTML closing tag.
				 * @param {string} $classes Space-separated list of classes for the event.
				 * @param {string} $date Event date in Y-m-d H:i:s format.
				 *
				 * @return {string} HTML following each event in upcoming events lists.
				 */
				$prepend = apply_filters( 'mc_event_upcoming_before', $prepend, $classes, $date );
				/**
				 * Closing elements for upcoming events list items. Default `</li>`.
				 *
				 * @hook mc_event_upcoming_after
				 *
				 * @param {string} $append Template HTML closing tag.
				 * @param {string} $classes Space-separated list of classes for the event.
				 * @param {string} $date Event date in Y-m-d H:i:s format.
				 *
				 * @return {string} HTML following each event in upcoming events lists.
				 */
				$append = apply_filters( 'mc_event_upcoming_after', $append, $classes, $date );
				// Recurring events should only appear once.
				if ( ! in_array( $details['dateid'], $skips, true ) ) {
					$output .= ( $item === $last_item ) ? '' : $prepend . $item . $append;
				}
			}
			$skips[]   = $details['dateid']; // Prevent the same event from showing more than once.
			$last_item = $item;
		}
	} else {
		$query  = array(
			'category' => $category,
			'before'   => $before,
			'after'    => $after,
			'today'    => $show_today,
			'author'   => $author,
			'host'     => $host,
			'ltype'    => $ltype,
			'lvalue'   => $lvalue,
			'site'     => $site,
		);
		$events = mc_get_all_events( $query );

		$holidays      = mc_get_all_holidays( $before, $after, $show_today );
		$holiday_array = mc_set_date_array( $holidays );

		if ( is_array( $events ) && ! empty( $events ) ) {
			$event_array = mc_set_date_array( $events );
			if ( is_array( $holidays ) && count( $holidays ) > 0 ) {
				$event_array = mc_holiday_limit( $event_array, $holiday_array ); // if there are holidays, filter results.
			}
		}
		if ( ! empty( $event_array ) ) {
			$output .= mc_produce_upcoming_events( $event_array, $template, 'list', $order, $skip, $before, $after, $show_today, $show_recurring );
		} else {
			$output = '';
		}
	}
	if ( '' !== $output ) {
		/**
		 * Replace the list header for upcoming events lists. Default value `<ul id='upcoming-events-$hash' class='upcoming-events'$lang>`.
		 *
		 * @hook mc_upcoming_events_header
		 *
		 * @param {string} $header Existing upcoming events header HTML.
		 *
		 * @return {string} List header HTML.
		 */
		$header = apply_filters( 'mc_upcoming_events_header', $header );
		/**
		 * Replace the list footer for upcoming events lists. Default value `</ul>`.
		 *
		 * @hook mc_upcoming_events_footer
		 *
		 * @param {string} $header Existing upcoming events footer HTML.
		 *
		 * @return {string} List header HTML.
		 */

		$footer = apply_filters( 'mc_upcoming_events_footer', $footer );
		$output = $header . $output . $footer;
		$return = mc_run_shortcodes( $output );
	} else {
		$return = '<div class="no-events-fallback upcoming-events">' . stripcslashes( $no_event_text ) . '</div>';
	}

	if ( $site ) {
		restore_current_blog();
	}

	if ( $language ) {
		mc_switch_language( $language, $locale );
	}

	return $return;
}

/**
 * For a set of grouped events, get the total time spanned by the group of events.
 *
 * @param int $group_id Event Group ID.
 *
 * @return array beginning and ending dates
 */
function mc_span_time( $group_id ) {
	global $wpdb;
	$mcdb = $wpdb;
	if ( 'true' === get_option( 'mc_remote' ) && function_exists( 'mc_remote_db' ) ) {
		$mcdb = mc_remote_db();
	}
	$group_id = (int) $group_id;
	$dates    = $mcdb->get_results( $wpdb->prepare( 'SELECT event_begin, event_time, event_end, event_endtime FROM ' . my_calendar_table() . ' WHERE event_group_id = %d ORDER BY event_begin ASC', $group_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	$count    = count( $dates );
	$last     = $count - 1;
	$begin    = $dates[0]->event_begin . ' ' . $dates[0]->event_time;
	$end      = $dates[ $last ]->event_end . ' ' . $dates[ $last ]->event_endtime;

	return array( $begin, $end );
}

/**
 * Generates the list of upcoming events when counting by events rather than a date pattern
 *
 * @param array  $events (Array of events to analyze).
 * @param string $template Custom template to use for display.
 * @param string $type Usually 'list', but also RSS or export.
 * @param string $order 'asc' or 'desc'.
 * @param int    $skip Number of events to skip over.
 * @param int    $before How many past events to show.
 * @param int    $after How many future events to show.
 * @param string $show_today 'yes' (anything else is false); whether to include events happening today.
 * @param string $show_recurring 'yes', show all recurring events. Else, first only.
 * @param string $context Display context.
 *
 * @return string; HTML output of list
 */
function mc_produce_upcoming_events( $events, $template, $type = 'list', $order = 'asc', $skip = 0, $before = 0, $after = 3, $show_today = 'yes', $show_recurring = 'yes', $context = 'filters' ) {
	// $events has +5 before and +5 after if those values are non-zero.
	// $events equals array of events based on before/after queries. Nothing skipped, order is not set, holiday conflicts removed.
	$output      = array();
	$near_events = array();
	$temp_array  = array();
	$past        = 1;
	$future      = 1;
	$today       = current_time( 'Y-m-d' );
	uksort( $events, 'mc_timediff_cmp' ); // Sort all events by proximity to current date.
	$count = count( $events );
	$group = array();
	$spans = array();
	$occur = array();
	$extra = 0;
	$i     = 0;
	// Create near_events array.
	$recurring_events = array();
	$last_events      = array();
	$last_group       = array();
	if ( is_array( $events ) ) {
		foreach ( $events as $k => $event ) {
			if ( $i < $count ) {
				if ( is_array( $event ) ) {
					foreach ( $event as $e ) {
						if ( ! mc_private_event( $e ) ) {
							$beginning = $e->occur_begin;
							$end       = $e->occur_end;
							// Store span time in an array to avoid repeating database query.
							if ( '1' === $e->event_span && ( ! isset( $spans[ $e->occur_group_id ] ) ) ) {
								// This is a multi-day event: treat each event as if it spanned the entire range of the group.
								$span_time                   = mc_span_time( $e->occur_group_id );
								$beginning                   = $span_time[0];
								$end                         = $span_time[1];
								$spans[ $e->occur_group_id ] = $span_time;
							} elseif ( '1' === $e->event_span && ( isset( $spans[ $e->occur_group_id ] ) ) ) {
								$span_time = $spans[ $e->occur_group_id ];
								$beginning = $span_time[0];
								$end       = $span_time[1];
							}
							$current = current_time( 'Y-m-d H:i:00' );
							if ( $e ) {
								// If a multi-day event, show only once.
								if ( '0' !== $e->occur_group_id && '1' === $e->event_span && in_array( $e->occur_group_id, $group, true ) || in_array( $e->occur_id, $occur, true ) ) {
									$md = true;
								} else {
									$group[] = $e->occur_group_id;
									$occur[] = $e->occur_id;
									$md      = false;
								}
								// end multi-day reduction.
								if ( ! $md ) {
									if ( in_array( $e->occur_event_id, $recurring_events, true ) ) {
										$is_recurring = true;
									} else {
										$is_recurring = false;
										$instances    = mc_get_occurrences( $e->occur_event_id );
										if ( count( $instances ) > 1 ) {
											$recurring_events[] = $e->occur_event_id;
										}
									}
									if ( ( 'yes' !== $show_recurring ) && $is_recurring ) {
										continue;
									}

									if ( 'yes' === $show_today && my_calendar_date_equal( $beginning, $current ) ) {

										/**
										 * Should today's events be counted towards total number of upcoming events. Default `yes`. Any value other than 'no' will be interpreted as 'yes'.
										 *
										 * @hook mc_include_today_in_total
										 *
										 * @param {string} $in_total Return 'no' to exclude today's events from event count.
										 *
										 * @return {string} 'yes' or 'no'.
										 */
										$in_total = apply_filters( 'mc_include_today_in_total', 'yes' ); // count todays events in total.
										if ( 'no' !== $in_total ) {
											$near_events[] = $e;
											if ( $before > $after ) {
												$future ++;
											} else {
												$past ++;
											}
										} else {
											$near_events[] = $e;
										}
									} elseif ( ( $past <= $before && $future <= $after ) ) {
										$near_events[] = $e; // If neither limit is reached, split off ly.
									} elseif ( $past <= $before && ( my_calendar_date_comp( $beginning, $current ) ) ) {
										$near_events[] = $e; // Split off another past event.
									} elseif ( $future <= $after && ( ! my_calendar_date_comp( $end, $current ) ) ) {
										$near_events[] = $e; // Split off another future event.
									}

									if ( my_calendar_date_comp( $beginning, $current ) ) {
										$past ++;
									} elseif ( my_calendar_date_equal( $beginning, $current ) ) {
										if ( 'yes' === $show_today ) {
											$extra ++;
										}
									} elseif ( ! my_calendar_date_comp( $end, $current ) ) {
										$future ++;
									}

									$last_events[] = $e->occur_id;
									$last_group[]  = $e->occur_group_id;
									$last_date     = $beginning;
								}
								if ( $past > $before && $future > $after && 'yes' !== $show_today ) {
									break;
								}
							}
						}
					}
				}
			}
		}
	}
	$events = $near_events;
	usort( $events, 'mc_datetime_cmp' ); // Sort split events by date.

	if ( is_array( $events ) ) {
		foreach ( array_keys( $events ) as $key ) {
			$event        =& $events[ $key ];
			$temp_array[] = $event;
		}
		$i      = 0;
		$groups = array();
		$skips  = array();

		foreach ( reverse_array( $temp_array, true, $order ) as $event ) {
			$details = mc_create_tags( $event, $context );
			if ( ! in_array( $details['group'], $groups, true ) ) {
				// dtstart is already in current time zone.
				$date    = mc_date( 'Y-m-d H:i:s', strtotime( $details['dtstart'] ), false );
				$classes = mc_event_classes( $event, 'upcoming' );

				if ( my_calendar_date_equal( $date, $today ) ) {
					$classes .= ' today';
				}
				if ( '1' === $details['event_span'] ) {
					$classes .= ' multiday';
				}
				if ( 'list' === $type ) {
					$prepend = "\n<li class=\"$classes\">";
					$append  = "</li>\n";
				} else {
					$prepend = '';
					$append  = '';
				}
				/**
				 * Opening elements for today's events list items. Default `<li class="$classes">`.
				 *
				 * @hook mc_event_upcoming_before
				 *
				 * @param {string} $append Template HTML closing tag.
				 * @param {string} $classes Space-separated list of classes for the event.
				 * @param {string} $date Event date in Y-m-d H:i:s format.
				 *
				 * @return {string} HTML following each event in upcoming events lists.
				 */
				$prepend = apply_filters( 'mc_event_upcoming_before', $prepend, $classes, $date );
				/**
				 * Closing elements for today's events list items. Default `</li>`.
				 *
				 * @hook mc_event_upcoming_after
				 *
				 * @param {string} $append Template HTML closing tag.
				 * @param {string} $classes Space-separated list of classes for the event.
				 * @param {string} $date Event date in Y-m-d H:i:s format.
				 *
				 * @return {string} HTML following each event in upcoming events lists.
				 */
				$append = apply_filters( 'mc_event_upcoming_after', $append, $classes, $date );

				if ( $i < $skip && 0 !== $skip ) {
					$i ++;
				} else {
					if ( ! in_array( $details['dateid'], $skips, true ) ) {

						/**
						 * Draw a custom template for upcoming events. Returning any non-empty string short circuits other template settings.
						 *
						 * @hook mc_draw_upcoming_event
						 *
						 * @param {string} $item Empty string before event template is drawn.
						 * @param {array}  $details Associative array of event template tags.
						 * @param {string} $template Template string passed from widget or shortcode.
						 * @param {array}  $args Associative array holding the arguments used to generate this list of upcoming events.
						 *
						 * @return {string} Event template details.
						 */
						$item = apply_filters( 'mc_draw_upcoming_event', '', $details, $template, $type );
						if ( '' === $item ) {
							$item = mc_draw_template( $details, $template, $type, $event );
						}

						/**
						 * Filter upcoming events list item output.
						 *
						 * @hook mc_event_upcoming
						 *
						 * @param {string} $item List item HTML for upcoming event.
						 * @param {object} $event Event object.
						 *
						 * @return {array} Array of event listing arguments.
						 */
						$output[] = apply_filters( 'mc_event_upcoming', $prepend . $item . $append, $event );
						$skips[]  = $details['dateid'];
					}
				}
				if ( '1' === $details['event_span'] ) {
					$groups[] = $details['group'];
				}
			}
		}
	}
	// If more items than there should be (due to handling of current-day's events), pop off.
	$intended = $before + $after + $extra;
	$actual   = count( $output );
	if ( $actual > $intended ) {
		for ( $i = 0; $i < ( $actual - $intended ); $i ++ ) {
			array_pop( $output );
		}
	}
	$html = '';
	foreach ( $output as $out ) {
		$html .= $out;
	}

	return $html;
}

/**
 * Process the Today's Events widget.
 *
 * @param array $args Event & output construction parameters.
 *
 * @return string HTML.
 */
function my_calendar_todays_events( $args ) {
	$language = isset( $args['language'] ) ? $args['language'] : '';
	$switched = '';
	if ( $language ) {
		$locale   = get_locale();
		$switched = mc_switch_language( $locale, $language );
	}

	$category   = ( isset( $args['category'] ) ) ? $args['category'] : 'default';
	$template   = ( isset( $args['template'] ) ) ? $args['template'] : 'default';
	$substitute = ( isset( $args['fallback'] ) ) ? $args['fallback'] : '';
	$author     = ( isset( $args['author'] ) ) ? $args['author'] : 'all';
	$host       = ( isset( $args['host'] ) ) ? $args['host'] : 'all';
	$date       = ( isset( $args['date'] ) ) ? $args['date'] : false;
	$site       = ( isset( $args['site'] ) ) ? $args['site'] : false;

	if ( $site ) {
		$site = ( 'global' === $site ) ? BLOG_ID_CURRENT_SITE : $site;
		switch_to_blog( $site );
	}

	$params = array(
		'category'   => $category,
		'template'   => $template,
		'substitute' => $substitute,
		'author'     => $author,
		'host'       => $host,
		'date'       => $date,
	);
	$hash   = md5( implode( ',', $params ) );
	$output = '';

	// allow reference by file to external template.
	if ( '' !== $template && mc_file_exists( $template ) ) {
		$template = file_get_contents( mc_get_file( $template ) );
	}
	$defaults = mc_widget_defaults();
	$template = ( ! $template || 'default' === $template ) ? $defaults['today']['template'] : $template;

	if ( mc_key_exists( $template ) ) {
		$template = mc_get_custom_template( $template );
	}

	$category      = ( 'default' === $category ) ? $defaults['today']['category'] : $category;
	$no_event_text = ( '' === $substitute ) ? $defaults['today']['text'] : $substitute;
	if ( $date ) {
		$from = mc_date( 'Y-m-d', strtotime( $date ), false );
		$to   = mc_date( 'Y-m-d', strtotime( $date ), false );
	} else {
		$from = current_time( 'Y-m-d' );
		$to   = current_time( 'Y-m-d' );
	}

	$args = array(
		'from'     => $from,
		'to'       => $to,
		'category' => $category,
		'ltype'    => '',
		'lvalue'   => '',
		'author'   => $author,
		'host'     => $host,
		'search'   => '',
		'source'   => 'upcoming',
		'site'     => $site,
	);
	/**
	 * Modify the arguments used to generate today's events.
	 *
	 * @hook mc_today_attributes
	 *
	 * @param {array} $args All arguments used to generate this list.
	 * @param {array} $params Subset of parameters used to generate this list's ID hash.
	 *
	 * @return {array} Array of event listing arguments.
	 */
	$args   = apply_filters( 'mc_today_attributes', $args, $params );
	$events = my_calendar_events( $args );

	$today         = ( isset( $events[ $from ] ) ) ? $events[ $from ] : false;
	$lang          = ( $switched ) ? ' lang="' . esc_attr( $switched ) . '"' : '';
	$header        = "<ul id='todays-events-$hash' class='todays-events'$lang>";
	$footer        = '</ul>';
	$groups        = array();
	$todays_events = array();
	// quick loop through all events today to check for holidays.
	if ( is_array( $today ) ) {
		foreach ( $today as $e ) {
			if ( ! mc_private_event( $e ) && ! in_array( $e->event_group_id, $groups, true ) ) {
				$event_details = mc_create_tags( $e );
				$ts            = $e->ts_occur_begin;
				$classes       = mc_event_classes( $e, 'today' );

				/**
				 * Modify the HTML preceding each list item in a list of today's events.
				 *
				 * @hook mc_todays_events_before
				 *
				 * @param {string} $item HTML string before each event.
				 * @param {string} $classes Space separated list of classes for this event.
				 * @param {string} $category Category argument passed to this list.
				 *
				 * @return {string} HTML preceding each event in today's events lists.
				 */
				$prepend = apply_filters( 'mc_todays_events_before', "<li class='$classes'>", $classes, $category );
				/**
				 * Closing elements for today's events list items. Default `</li>`.
				 *
				 * @hook mc_todays_events_after
				 *
				 * @param {string} $item Template HTML closing tag.
				 *
				 * @return {string} HTML following each event in today's events lists.
				 */
				$append = apply_filters( 'mc_todays_events_after', '</li>' );

				/**
				 * Draw a custom template for today's events. Returning any non-empty string short circuits other template settings.
				 *
				 * @hook mc_draw_todays_event
				 *
				 * @param {string} $item Empty string before event template is drawn.
				 * @param {array}  $event_details Associative array of event template tags.
				 * @param {string} $template Template string passed from widget or shortcode.
				 * @param {array}  $args Associative array holding the arguments used to generate this list of events.
				 *
				 * @return {string} Event output details.
				 */
				$item = apply_filters( 'mc_draw_todays_event', '', $event_details, $template, $args );
				if ( '' === $item ) {
					$item = mc_draw_template( $event_details, $template, 'list', $e );
				}
				$todays_events[ $ts ][] = $prepend . $item . $append;
			}
		}
		/**
		 * Filter the array of events listed in today's events lists.
		 *
		 * @hook mc_event_today
		 *
		 * @param {array} $todays_events  A multidimensional array of event items with today's date as a key with an array of formatted HTML on event templates on the current date.
		 * @param {array} $events Array of events without private events removed. Values are event objects.
		 *
		 * @return {array} A multidimensional array of event items with today's date as a key with an array of formatted HTML on event templates on the current date.
		 */
		$todays_events = apply_filters( 'mc_event_today', $todays_events, $events );
		foreach ( $todays_events as $k => $t ) {
			foreach ( $t as $now ) {
				$output .= $now;
			}
		}
		if ( 0 !== count( $events ) ) {
			/**
			 * Replace the list header for today's events lists. Default value `<ul id='todays-events-$hash' class='todays-events'$lang>`.
			 *
			 * @hook mc_todays_events_header
			 *
			 * @param {string} $header Existing today's events header HTML.
			 *
			 * @return {string} List header HTML.
			 */
			$return  = apply_filters( 'mc_todays_events_header', $header );
			$return .= $output;
			/**
			 * Replace the list footer for today's events lists. Default value `</ul>`.
			 *
			 * @hook mc_todays_events_header
			 *
			 * @param {string} $header Existing today's events header HTML.
			 *
			 * @return {string} List header HTML.
			 */
			$return .= apply_filters( 'mc_todays_events_footer', $footer );
		} else {
			$return = '<div class="no-events-fallback todays-events">' . stripcslashes( $no_event_text ) . '</div>';
		}
	} else {
		$return = '<div class="no-events-fallback todays-events">' . stripcslashes( $no_event_text ) . '</div>';
	}

	if ( $site ) {
		restore_current_blog();
	}

	$output = mc_run_shortcodes( $return );

	if ( $language ) {
		mc_switch_language( $language, $locale );
	}

	return $output;
}