File: /var/www/html/triad-infosec/wp-content/plugins/events-calendar-pro/src/Tribe/Repositories/Event.php
<?php
/**
* An extension of The Events Calendar base repository to support PRO functions.
*
* @since 4.7
*/
/**
* Class Tribe__Events__Pro__Repositories__Event
*
* @since 4.7
*/
class Tribe__Events__Pro__Repositories__Event extends Tribe__Events__Repositories__Event {
/**
* A map relating the custom fields labels to their slug.
*
* @var array
*/
protected $custom_fields_map;
/**
* The data payload that should be used to create a recurring event.
*
* This is created during the save operations and unset afterwards.
*
* @var array
*/
protected $create_recurrence_payload = array();
/**
* A map relating the IDs of the posts the repository is updating
* with their recurrence payloads if any.
*
* @var array
*/
protected $update_recurrence_payloads = array();
/**
* The full post array of the event that is being created or updated.
*
* @var array
*/
protected $postarr;
/**
* A map relating post IDs to the post array used to update them.
*
* This is set while filtering the post arrays for update operations.
*
* @var
*/
protected $update_postarrs;
/**
* A flag property to keep track of whether the repository is currently in the context of a query
* to set up to collapse recurring event instances, or not.
*
* @since 5.2.0
*
* @var bool
*/
protected $collapsing_recurring_event_instances = false;
/**
* Tribe__Events__Pro__Repositories__Event constructor.
*
* @since 4.7
*/
public function __construct() {
parent::__construct();
$this->add_schema_entry( 'custom_field', array( $this, 'filter_by_custom_field' ) );
$this->add_schema_entry( 'custom_field_between', array( $this, 'filter_by_custom_field_between' ) );
$this->add_schema_entry( 'custom_field_less_than', array( $this, 'filter_by_custom_field_less_than' ) );
$this->add_schema_entry( 'custom_field_greater_than', array( $this, 'filter_by_custom_field_greater_than' ) );
$this->add_schema_entry( 'geoloc_lat', array( $this, 'filter_by_geoloc_lat' ) );
$this->add_schema_entry( 'geoloc_lng', array( $this, 'filter_by_geoloc_lng' ) );
$this->add_schema_entry( 'geoloc', array( $this, 'filter_by_geoloc' ) );
$this->add_schema_entry( 'has_geoloc', array( $this, 'filter_by_has_geoloc' ) );
$this->add_schema_entry( 'near', array( $this, 'filter_by_near' ) );
$this->add_schema_entry( 'series', array( $this, 'filter_by_in_series' ) );
$this->add_schema_entry( 'in_series', array( $this, 'filter_by_in_series' ) );
$this->add_schema_entry( 'related_to', array( $this, 'filter_by_related_to' ) );
}
/**
* Filters events to include events that have a specified custom field value.
*
* @since 4.7
*
* @param string $custom_field The custom field name or label.
* @param string $value A LIKE-compatible string or a regular expression will
* be used for LIKE or REGEXP comparisons. Use a regular
* expression to get exact matches. The limitations of SQL
* REGEXP syntax apply (e.g not modifiers).
*/
public function filter_by_custom_field( $custom_field, $value ) {
$custom_field_key = $this->get_field_slug( $custom_field );
if ( tribe_is_regex( $value ) ) {
$this->by( 'meta_regexp', $custom_field_key, tribe_unfenced_regex( $value ) );
return;
}
$this->by( 'meta_like', $custom_field_key, $value );
}
/**
* Returns the `name` of a custom field given its label.
*
* Here "custom fields" are those managed by the PRO plugin.
*
* @since 4.7
*
* @param string $label The custom field label.
*
* @return string The custom field name or the input string if not found.
*/
protected function get_field_slug( $label ) {
if ( null === $this->custom_fields_map ) {
$custom_fields = tribe_get_option( 'custom-fields', array() );
$this->custom_fields_map = array_combine(
array_map( 'strtolower', wp_list_pluck( $custom_fields, 'label' ) ),
wp_list_pluck( $custom_fields, 'name' )
);
}
return Tribe__Utils__Array::get( $this->custom_fields_map, strtolower( $label ), $label );
}
/**
* Filters events to include events that have a specified custom field between than the specified values.
*
* Fetch is inclusive.
*
* @since 4.7
*
* @param string $custom_field The custom field name or label.
* @param mixed $low The lower limit of the interval.
* @param mixed $high The upper limit of the interval.
*/
public function filter_by_custom_field_between( $custom_field, $low, $high ) {
$this->by( 'meta_between', $this->get_field_slug( $custom_field ), array( $low, $high ) );
}
/**
* Filters events to include events that have a specified custom field less than the specified value.
*
* Fetch is not inclusive.
*
* @since 4.7
*
* @param string $custom_field The custom field name or label.
* @param mixed $value The value to compare to.
*/
public function filter_by_custom_field_less_than( $custom_field, $value ) {
$this->by( 'meta_less_than', $this->get_field_slug( $custom_field ), $value );
}
/**
* Filters events to include events that have a specified custom field greater than the specified value.
*
* Fetch is not inclusive.
*
* @since 4.7
*
* @param string $custom_field The custom field name or label.
* @param mixed $value The value to compare to.
*/
public function filter_by_custom_field_greater_than( $custom_field, $value ) {
$this->by( 'meta_greater_than', $this->get_field_slug( $custom_field ), $value );
}
/**
* Will generate the JOIN clause for filtering by series.
*
* @since 6.0.5
*
* @return string The JOIN clause for in series meta filtering.
*/
public static function get_in_series_join_sql(): string {
global $wpdb;
return "JOIN {$wpdb->postmeta} in_series_meta ON {$wpdb->posts}.ID = in_series_meta.post_id ";
}
/**
* Will generate the WHERE clause for the in series param.
*
* @since 6.0.5
*
* @param mixed $in_series The series to filter by.
*
* @return string The WHERE clause to filter by this series.
*/
public static function get_in_series_where_sql( $in_series ): string {
global $wpdb;
if ( is_numeric( $in_series ) || $in_series instanceof WP_Post ) {
$parent_post_id = $in_series instanceof WP_Post ? $in_series->ID : absint( $in_series );
$children_clause = $wpdb->prepare( "{$wpdb->posts}.post_parent = %d", $parent_post_id );
$parent_clause = $wpdb->prepare( "{$wpdb->posts}.ID = %d", $parent_post_id );
} else {
$children_clause = "{$wpdb->posts}.post_parent != 0";
$parent_clause = "{$wpdb->posts}.post_parent = 0";
}
return "{$children_clause}
OR (
{$parent_clause}
AND in_series_meta.meta_key = '_EventRecurrence'
AND in_series_meta.meta_value IS NOT NULL
)";
}
/**
* Filters events to include only those that match the provided series state.
*
* @since 4.7
*
* @param bool|int $in_series A boolean to indicate whether to filter events that are part of a series (`true`)
* or not (`false`); a parent post ID to filter by events in a specific series.
*
* @return array|null Null if getting events in series, an array of query arguments that should be
* added to the query otherwise.
*/
public function filter_by_in_series( $in_series ) {
/**
* Will give an opportunity to provide custom filtering for series params.
*
* @since 6.0.5
*
* @param bool $did_handle_filter Flag whether to continue using filter parsing.
* @param Tribe__Repository__Query_Filters $filter_query This instance of the filter object.
* @param bool|numeric|WP_Post $in_series The series param.
*/
$did_handle_filter = apply_filters(
'tec_events_pro_tribe_repository_event_series_filter_override',
false,
$this->filter_query,
$in_series
);
// We handled this filter elsewhere, we are done.
if ( $did_handle_filter === true ) {
return null;
}
// Continue as usual.
if ( (bool) $in_series ) {
$this->filter_query->join( self::get_in_series_join_sql() );
$this->filter_query->where( self::get_in_series_where_sql( $in_series ) );
return null;
}
return array(
'post_parent' => 0,
'meta_query' => array(
'no-series-meta' => array(
'key' => '_EventRecurrence',
'value' => '#',
'compare' => 'NOT EXISTS',
),
),
);
}
/**
* Filters events to include only those that match the provided geolocation state.
*
* @since 4.7
*
* @param bool $has_geoloc Whether to fetch events related to Venues that have geolocation
* information available or not.
*/
public function filter_by_has_geoloc( $has_geoloc = true ) {
global $wpdb;
if ( (bool) $has_geoloc ) {
/*
* If the request is to filter by events that have geoloc then keep any event that has a
* Venue with complete (lat AND long) geoloc information.
*/
$this->filter_query->join( "JOIN {$wpdb->postmeta} has_geoloc_event_venue
ON has_geoloc_event_venue.post_id = {$wpdb->posts}.ID AND has_geoloc_event_venue.meta_key = '_EventVenueID'" );
$this->filter_query->join( $wpdb->prepare( "JOIN {$wpdb->postmeta} has_geoloc_venue_lat
ON ( has_geoloc_venue_lat.post_id = has_geoloc_event_venue.meta_value AND has_geoloc_venue_lat.meta_key = %s )",
Tribe__Events__Pro__Geo_Loc::LAT
) );
$this->filter_query->join( $wpdb->prepare( "JOIN {$wpdb->postmeta} has_geoloc_venue_lng
ON ( has_geoloc_venue_lng.post_id = has_geoloc_event_venue.meta_value AND has_geoloc_venue_lng.meta_key = %s )",
Tribe__Events__Pro__Geo_Loc::LNG
) );
$this->filter_query->where( 'has_geoloc_venue_lat.meta_value IS NOT NULL' );
$this->filter_query->where( 'has_geoloc_venue_lng.meta_value IS NOT NULL' );
return;
}
/*
* If the request is to filter by events that have no geoloc then keep any event that does not
* have a Venue or that's related to a Venue that does not have complete geoloc information.
*/
$this->filter_query->join( "LEFT JOIN {$wpdb->postmeta} has_geoloc_event_venue
ON ( has_geoloc_event_venue.post_id = {$wpdb->posts}.ID AND has_geoloc_event_venue.meta_key = '_EventVenueID')" );
$this->filter_query->join( $wpdb->prepare( "LEFT JOIN {$wpdb->postmeta} has_geoloc_venue_lat
ON has_geoloc_venue_lat.post_id = has_geoloc_event_venue.meta_value AND has_geoloc_venue_lat.meta_key = %s",
Tribe__Events__Pro__Geo_Loc::LAT
) );
$this->filter_query->join( $wpdb->prepare( "LEFT JOIN {$wpdb->postmeta} has_geoloc_venue_lng
ON has_geoloc_venue_lng.post_id = has_geoloc_event_venue.meta_value AND has_geoloc_venue_lng.meta_key = %s",
Tribe__Events__Pro__Geo_Loc::LNG
) );
$this->filter_query->where( 'has_geoloc_event_venue.meta_id IS NULL
OR ( has_geoloc_venue_lat.meta_id IS NULL or has_geoloc_venue_lng.meta_id IS NULL)' );
}
/**
* Filters events to include only those that are geographically close to the provided address
* within a certain distance.
*
* This filter will be ignored if the address cannot be resolved to a set of latitude
* and longitude coordinates.
*
* @since 4.7
*
* @param string $address The address string.
* @param int $distance The distance in units from the resolved address; defaults to 10.
*/
public function filter_by_near( $address, $distance = 10 ) {
$resolved = Tribe__Events__Pro__Geo_Loc::instance()->geocode_address( $address );
$bad_values = array(
'',
null,
);
if (
false === $resolved
|| ! isset( $resolved['lat'], $resolved['lng'] )
|| in_array( $resolved['lat'], $bad_values, true )
|| in_array( $resolved['lng'], $bad_values, true )
) {
// Ignore this filter if we could not resolve to a set of coordinates.
return;
}
$this->filter_by_geoloc( $resolved['lat'], $resolved['lng'], $distance );
}
/**
* Filters events to include only those that match the provided geoloc latitude and longitude,
* optionally providing a distance from geoloc.
*
* The unit type used will be the same as defined in the calendar settings.
*
* @since 4.7
*
* @param float $lat The center latitude.
* @param float $lng The center longitude.
* @param int $distance Number of units from the center; defaults to 10 units.
*/
public function filter_by_geoloc( $lat, $lng, $distance = 10 ) {
$this->filter_by_geoloc_lat( $lat, $distance );
$this->filter_by_geoloc_lng( $lng, $distance );
}
/**
* Filters events to include only those that match the provided geoloc latitude, optionally providing a distance
* from geoloc.
*
* The unit type used will be the same as defined in the calendar settings.
*
* @since 4.7
*
* @param float|int $lat The latitude to use as center.
* @param int $distance The radius to search around the latitude.
*/
public function filter_by_geoloc_lat( $lat, $distance = 10 ) {
global $wpdb;
$this->filter_query->join( "
JOIN {$wpdb->postmeta} event_venues_lat
ON ( {$wpdb->posts}.ID = event_venues_lat.post_id AND event_venues_lat.meta_key = '_EventVenueID' )
" );
$this->filter_query->join(
$wpdb->prepare( "
JOIN {$wpdb->postmeta} venues_meta_lat
ON ( venues_meta_lat.post_id = event_venues_lat.meta_value AND venues_meta_lat.meta_key = %s )
", Tribe__Events__Pro__Geo_Loc::LAT )
);
$this->filter_query->where(
$wpdb->prepare(
'venues_meta_lat.meta_value BETWEEN %d AND %d',
$lat - $distance,
$lat + $distance
)
);
}
/**
* Filters events to include only those that match the provided geoloc longitude optionally providing a distance
* from geoloc.
*
* The unit type used will be the same as defined in the calendar settings.
*
* @since 4.7
*
* @param float|int $lng The longitude to use as center.
* @param int $distance The radius to search around the latitude.
*/
public function filter_by_geoloc_lng( $lng, $distance = 10 ) {
global $wpdb;
$this->filter_query->join( "
JOIN {$wpdb->postmeta} event_venues_long
ON ( {$wpdb->posts}.ID = event_venues_long.post_id AND event_venues_long.meta_key = '_EventVenueID' )
" );
$this->filter_query->join(
$wpdb->prepare( "
JOIN {$wpdb->postmeta} venues_meta_long
ON ( venues_meta_long.post_id = event_venues_long.meta_value AND venues_meta_long.meta_key = %s )
", Tribe__Events__Pro__Geo_Loc::LNG )
);
$this->filter_query->where(
$wpdb->prepare(
'venues_meta_long.meta_value BETWEEN %d AND %d',
$lng - $distance,
$lng + $distance
)
);
}
/**
* Filters events to include only those that are related to a specific post.
*
* @since 4.7
*
* @param int|WP_Post $post Post ID or object.
*
* @return array|null An array of arguments that should be added to the WP_Query object or null if empty post ID.
*/
public function filter_by_related_to( $post ) {
$post_id = Tribe__Events__Main::postIdHelper( $post );
if ( ! $post_id ) {
return null;
}
$taxonomies = array(
'post_tag',
Tribe__Events__Main::TAXONOMY,
);
/**
* Filter the taxonomies used for related posts queries.
*
* @param array $taxonomies Taxonomies that are used to look up related posts.
*
* @since 4.7
*/
$taxonomies = apply_filters( 'tribe_related_posts_taxonomies', $taxonomies );
$args = array(
'post__not_in' => array(
$post_id,
),
'tax_query' => array(
'relation' => 'OR',
),
);
foreach ( $taxonomies as $taxonomy ) {
$term_ids = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) );
$term_ids = array_values( array_filter( $term_ids, 'is_numeric' ) );
if ( $term_ids && ! is_wp_error( $term_ids ) ) {
$args['tax_query'][ 'by-' . $taxonomy ] = array(
'taxonomy' => $taxonomy,
'field' => 'id',
'terms' => $term_ids,
);
}
}
$original_args = $args;
/**
* Filter the arguments used for related posts queries. Added for backwards compatibility.
*
* @param array $args Query arguments for related post lookups.
*
* @since 3.2
*/
$args = apply_filters( 'tribe_related_posts_args', $args );
// Check for significant change or no tax_query, if none found then no query needs to happen.
if ( $original_args === $args && 1 === count( $args['tax_query'] ) ) {
return null;
}
return $args;
}
/**
* Overrides the base method to store and update some values related to recurrences.
*
* @since 4.7
*
* @param array $postarr The post array that should be used to create the event.
*
* @return mixed The original method return value
*/
public function filter_postarr_for_create( array $postarr ) {
// Let TEC do its filtering first.
$filtered = parent::filter_postarr_for_create( $postarr );
if ( ! is_array( $filtered ) ) {
// It might not be an array and just be false due to some bad data detected by TEC.
return $filtered;
}
// Then, if a `recurrence` entry is present, save it to use it after the event has been created.
if ( isset( $filtered['meta_input']['recurrence'] ) ) {
// If the `recurrence` entry is a callback, resolve it now.
if ( is_callable( $filtered['meta_input']['recurrence'] ) ) {
$callback = $filtered['meta_input']['recurrence'];
$filtered['meta_input']['recurrence'] = $callback( $filtered );
}
/*
* Independently of what method is handling the recurrence creation we store the whole
* post array meta input as "recurrence payload" for the purpose of back-compatibility and context.
* For the same purpose we save the full post array too.
*/
$this->create_recurrence_payload = $filtered['meta_input'];
$this->postarr = $filtered;
unset( $filtered['meta_input']['recurrence'] );
}
return $filtered;
}
/**
* Overrides the base create method to additionally create recurring events after the main event
* is saved.
*
* @since 4.7
*
* @return false|WP_Post The original return value from the base repository `create`
* method.
*/
public function create() {
$event = parent::create();
// We cannot be 100% this will always be a post object, so let's just try to get it.
$event_post = get_post( $event );
if ( empty( $event ) || ! $event_post instanceof WP_Post ) {
// No sense in going on.
$this->create_recurrence_payload = false;
return $event;
}
try {
set_error_handler( array( $this, 'cast_error_to_exception' ) );
/*
* Many methods "down the road" might expect prefixed or un-prefixed meta keys, e.g. `_EventStartDate`
* and `EventStartDate` so we "duplicate" them now in the payload; this covers back-compatibility too.
* Recurrence handling methods should handle the case where the recurrence data is empty.
*/
if ( empty( $this->create_recurrence_payload ) ) {
// If the recurrence payload information is still empty then fill it up w/ the event meta.
$event_meta = Tribe__Utils__Array::flatten(
Tribe__Utils__Array::filter_prefixed( get_post_meta( $event_post->ID ), '_Event' )
);
$recurrence_payload = Tribe__Utils__Array::add_unprefixed_keys_to( $event_meta );
} else {
$recurrence_payload = Tribe__Utils__Array::add_unprefixed_keys_to( $this->create_recurrence_payload );
}
$callback = $this->get_recurrence_creation_callback( $event_post->ID, $recurrence_payload, $this->updates );
/*
* Since the burden of logging and handling falls on the callback we're not collecting this value.
* Filtering callbacks might return empty or falsy values for other reasons than a failure; an
* exception is the correct way to signal an error.
*/
$callback( $event_post->ID, $recurrence_payload );
} catch ( Exception $e ) {
// Something happened, let's log and move on.
tribe( 'logger' )->log(
'There was an error updating the recurrence rules and/or exclusions for event ' . $event_post->ID . ': ' . $e->getMessage(),
Tribe__Log::ERROR,
__CLASS__
);
restore_error_handler();
return $event;
}
restore_error_handler();
return $event;
}
/**
* Overrides the base method to store and capture the recurrence information.
*
* @since 4.7
*
* @param array $postarr The array of updates for the post.
* @param int $post_id The ID of the post that is being updated.
*
* @return mixed The base method return value.
*/
public function filter_postarr_for_update( array $postarr, $post_id ) {
// Let TEC do its filtering first.
$filtered = parent::filter_postarr_for_update( $postarr, $post_id );
if ( ! is_array( $filtered ) ) {
// It might not be an array and just be false due to some bad data detected by TEC.
return $filtered;
}
// Then, if a `recurrence` entry is present, save it to use it after the event has been created.
if ( isset( $filtered['meta_input']['recurrence'] ) ) {
/*
* In the context of updates the client code should be able to make an event non-recurring; to
* support this we will transform "falsy" values into an empty array.
* The empty array is chosen to indicate the will to update an event to non-recurring for
* back-compatibility reasons.
*/
if ( ! tribe_is_truthy( $filtered['meta_input']['recurrence'] ) ) {
$filtered['meta_input']['recurrence'] = array();
}
/*
* Independently of what method is handling the recurrence creation/update we store the whole
* post array meta input as "recurrence payload" for the purpose of back-compatibility and context.
* For the same purpose we save the full post array too.
*/
$this->update_recurrence_payloads[ (int) $post_id ] = $filtered['meta_input'];
$this->update_postarrs[ (int) $post_id ] = $filtered;
unset( $filtered['meta_input']['recurrence'] );
}
return $filtered;
}
/**
* Overrides the base method to save, along with the events, their additional recurring event instances.
*
* This method will not try to "predict" the load like the base `save` method does.
* While the save method knows for certain how many events it will update the current recurrence implementation
* does not allow to have that forecast.
*
* @since 4.7
*
* @param bool $return_promise Whether to return a promise object or just the ids
* of the updated posts; if `true` then a promise will
* be returned whether the update is happening in background
* or not.
*
* @return array|Tribe__Promise A list of the post IDs that have been (synchronous) or will
* be (asynchronous) updated if `$return_promise` is set to `false`;
* the Promise object if `$return_promise` is set to `true`.
*/
public function save( $return_promise = false ) {
$base_return = parent::save( $return_promise );
if ( empty( $this->update_recurrence_payloads ) ) {
return $base_return;
}
// Let's make sure we're iterating on arrays with an equal number of elements and same order.
ksort( $this->update_recurrence_payloads );
ksort( $this->update_postarrs );
// Each payload should have a post array and vice versa.
$valid_payloads = array_intersect_key( $this->update_recurrence_payloads, $this->update_postarrs );
$valid_postarrs = array_intersect_key( $this->update_postarrs, $valid_payloads );
$iterator = new MultipleIterator();
$iterator->attachIterator( new ArrayIterator( array_keys( $valid_payloads ) ), 'post_id' );
$iterator->attachIterator( new ArrayIterator( $valid_payloads ), 'recurrence_payload' );
$iterator->attachIterator( new ArrayIterator( $valid_postarrs ), 'postarr' );
set_error_handler( array( $this, 'cast_error_to_exception' ) );
foreach ( $iterator as $item ) {
list( $post_id, $recurrence_payload, $postarr ) = $item;
$event_post = get_post( $post_id );
if ( empty( $event_post ) || ! $event_post instanceof WP_Post ) {
// Might have been deleted in the meanwhile, bail.
continue;
}
try {
/*
* During update callbacks might expect more information about the event.
* For the purpose of completeness and back-compatibility let's fetch more information from the event
* meta and apply the post array meta input on top of it to use the very last version.
*/
$event_meta = Tribe__Utils__Array::flatten(
Tribe__Utils__Array::filter_prefixed( get_post_meta( $post_id ), '_Event' )
);
if ( isset( $postarr['meta_input'] ) ) {
$event_meta = array_merge( $event_meta, $postarr['meta_input'] );
}
/*
* Many methods "down the road" might expect prefixed or un-prefixed meta keys, e.g. `_EventStartDate`
* and `EventStartDate` so we "duplicate" them now in the payload; this covers back-compatibility too.
*/
$recurrence_payload = Tribe__Utils__Array::add_unprefixed_keys_to( array_merge( $event_meta, $recurrence_payload ) );
$callback = $this->get_recurrence_update_callback( $event_post->ID, $recurrence_payload, $postarr );
/*
* Since the burden of logging and handling falls on the callback we're not collecting this value.
* Filtering callbacks might return empty or falsy values for other reasons than a failure; an
* exception is the correct way to signal an error.
*/
$callback( $event_post->ID, $recurrence_payload );
} catch ( Exception $e ) {
// Something happened, let's log and move on.
tribe( 'logger' )->log(
'There was an error updating the recurrence rules and/or exclusions for event ' . $event_post->ID . ': ' . $e->getMessage(),
Tribe__Log::ERROR,
__CLASS__
);
restore_error_handler();
}
}
restore_error_handler();
return $base_return;
}
/**
* Filters and returns the recurrence creation callback the repository should use to create
* recurrences.
*
* @since 4.7
*
* @param int $event_id The post ID of the event that is currently being saved.
* @param mixed $recurrence_payload The recurrence data payload; there is no guarantee on the format
* of it as different implementations might use different formats.
* @param array $postarr The full post array that's been used to update or create the current event.
*
* @return callable The recurrence creation callback.
*/
protected function get_recurrence_creation_callback( $event_id, $recurrence_payload, array $postarr = null ) {
/**
* Filters the callback the Event repository should use to create recurrences.
*
* The callback will be passed exactly the same arguments this filter passes.
*
* @since 4.7
*
* @param callable $callback The callback that should be used to create posts; defaults
* to `Tribe__Events__Pro__Recurrence__Meta::updateRecurrenceMeta`.
* The burden of logging failures and reasons rests on the callback.
* @param int $event_id The post ID of the event that is currently being saved.
* @param mixed $recurrence_payload The recurrence data payload; there is no guarantee on the format
* of it as different implementations might use different formats.
* @param array $postarr The full post array that's been used to update or create the current event.
*
*/
$callback = apply_filters(
'tribe_repository_event_recurrence_create_callback',
array( 'Tribe__Events__Pro__Recurrence__Meta', 'updateRecurrenceMeta' ),
$event_id,
$recurrence_payload,
$postarr
);
return $callback;
}
/**
* Filters and returns the recurrence update callback the repository should use to update
* recurrences.
*
* @since 4.7
*
* @param int $event_id The post ID of the event that is currently being updated.
* @param mixed $recurrence_payload The recurrence data payload; there is no guarantee on the format
* of it as different implementations might use different formats.
* @param array $postarr The full post array that's been used to update or create the current event.
*
* @return callable The recurrence creation callback.
*/
protected function get_recurrence_update_callback( $event_id, $recurrence_payload, array $postarr ) {
/**
* Filters the callback the Event repository should use to update recurrences.
*
* The callback will be passed exactly the same arguments this filter passes.
*
* @since 4.7
*
* @param callable $callback The callback that should be used to create posts; defaults
* to `Tribe__Events__Pro__Recurrence__Meta::updateRecurrenceMeta`.
* The burden of logging failures and reasons rests on the callback.
* @param int $event_id The post ID of the event that is currently being updated.
* @param mixed $recurrence_payload The recurrence data payload; there is no guarantee on the format
* of it as different implementations might use different formats.
* @param array $postarr The full post array that's been used to update the current event.
*
*/
$callback = apply_filters(
'tribe_repository_event_recurrence_update_callback',
array( 'Tribe__Events__Pro__Recurrence__Meta', 'updateRecurrenceMeta' ),
$event_id,
$recurrence_payload,
$postarr
);
return $callback;
}
/**
* Overrides the base repository method to add, if required, query arguments to the query that will
* collapse recurring event instances.
*
* @since 5.2.0
*
* @return WP_Query The built query object, modified by means of its query vars, if required.
*/
protected function build_query_internally() {
// Handle the case where we're fetching by post name and want all event instances.
$this->maybe_expand_post_name();
if ( ! $this->should_collapse_recurring_event_instances() ) {
// Nothing to do here!
return parent::build_query_internally();
}
// Avoid infinite loops.
if ( ! $this->collapsing_recurring_event_instances ) {
$this->collapse_recurring_event_instances();
}
return parent::build_query_internally();
}
/**
* Returns a filtered list of display contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
*
* @since 4.7
*
* @return array A filtered list of display contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
*/
public function get_display_contexts_requiring_collapse() {
/**
* Filters the list of display contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
*
* @since 4.7
*
* @param array $contexts The list of display contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
* @param Tribe__Repository__Interface This repository object.
*/
$contexts = apply_filters(
"tribe_repository_{$this->filter_name}_display_contexts_requiring_collapse",
[ \Tribe\Events\Views\V2\Views\List_View::get_view_slug() ],
$this
);
return $contexts;
}
/**
* Returns a filtered list of render contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
*
* @since 4.7
*
* @return array A filtered list of render contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
*/
public function get_render_contexts_requiring_collapse() {
/**
* Filters the list of render contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
*
* @since 4.7
*
* @param array $contexts The list of render contexts that require recurring event
* instance collapsing if the "Show only the first instance of each recurring event"
* setting is truthy.
* @param Tribe__Repository__Interface This repository object.
*/
$contexts = apply_filters(
"tribe_repository_{$this->filter_name}_render_contexts_requiring_collapse",
array( 'widget' ),
$this
);
return $contexts;
}
/**
* Whether recurring event instances should be collapsed or not in the context of the ORM
* queries at all.
*
* This check is made on the option, the current render and display contexts.
*
* @since 4.7
*
* @return bool Whether recurring event instances should be collapsed or not in the context
* of ORM queries.
*/
protected function should_collapse_recurring_event_instances() {
$should_collapse = tribe_is_truthy( tribe_get_option( 'hideSubsequentRecurrencesDefault', false ) );
// Take the render and display context into account.
$should_collapse &= in_array( $this->render_context, $this->get_render_contexts_requiring_collapse(), true )
|| in_array( $this->display_context, $this->get_display_contexts_requiring_collapse(), true );
// Take into account an explicitly set collapse flag.
if ( isset( $this->query_args['hide_subsequent_recurrences'] ) ) {
$should_collapse = tribe_is_truthy( $this->query_args['hide_subsequent_recurrences'] );
}
/**
* Filters whether recurring event instances should be collapsed in ORM queries or not.
*
* The check is made in respect of the setting, the render and display contexts.
*
* @since 4.7
*
* @param bool $should_collapse Whether recurring event instances should be collapsed in ORM queries or not.
* @param string $render_context The current query render context.
* @param string $display_context The current query display context.
* @param Tribe__Repository__Interface This repository instance.
*/
return apply_filters(
"tribe_repository_{$this->filter_name}_collapse_recurring_event_instances",
$should_collapse,
$this->render_context,
$this->display_context,
$this
);
}
/**
* Handle the case where the current query display context requires name expansion and we're
* fetching by name.
*
* @since 4.7
*/
protected function maybe_expand_post_name() {
$qa = $this->query_args;
$requires_name_expansion = in_array( $this->display_context, $this->get_display_contexts_requiring_name_expansion(), true );
$querying_by_name = isset( $qa['name'] ) || isset( $qa['pagename'] ) || isset( $qa['post_name__in'] );
if ( ! ( $requires_name_expansion && $querying_by_name ) ) {
return;
}
// Get the name from any possible source in cascading order.
$names = (array) Tribe__Utils__Array::get( $qa,
'name', Tribe__Utils__Array::get( $qa,
'pagename', Tribe__Utils__Array::get( $qa,
'post_name__in', array() ) ) );
// Unset it as we're now taking care of this.
unset( $this->query_args['name'], $this->query_args['pagename'], $this->query_args['post_name__in'] );
if ( ! empty( $names ) ) {
// Posts with no parent should match exactly, posts with parent should match the regexp.
global $wpdb;
$p = $wpdb->posts;
$pattern = sprintf( '^(%s)', implode( '|', $names ) );
$name_interval = $this->prepare_interval( $names, '%s' );
$this->filter_query->where( "{$p}.post_name IN {$name_interval}
OR ({$p}.post_name REGEXP '{$pattern}' AND {$p}.post_parent != 0)" );
}
}
/**
* Returns the filtered list of display contexts that will require the `post_name` to match not only
* the parent event one but the instances too.
*
* @since 4.7
*
* @return array The filtered list of display contexts that will require the `post_name` to match not only
* the parent event one but the instances too.
*/
public function get_display_contexts_requiring_name_expansion() {
/**
* Filters the list of display contexts that require the `post_name` to be "expanded",
* really used in a regular expression, in ORM queries.
*
* @since 4.7
*
* @param array $contexts The the list of display contexts that require the `post_name` to be "expanded",
* really used in a regular expression, in ORM queries.
* @param Tribe__Repository__Interface This repository object.
*/
$contexts = apply_filters(
"tribe_repository_{$this->filter_name}_display_contexts_requiring_name_expansion",
array( 'all' ),
$this
);
return $contexts;
}
/**
* Cleans the `post__in` query argument to make sure its content is not filled with values previously
* set by the class methods.
*
* We use the `post__in` query argument when collapsing recurring event instances to show
* only the first upcoming instance. In this method we "clean" the `post__in` clause
*
* @since TB
*
* @param WP_Query $secondary_query The query object to update if required.
*/
protected function clean_post__in( WP_Query $secondary_query ) {
if ( ! isset( $secondary_query->query_vars['tribe_post__in'] ) ) {
return;
}
$tribe__post_in = $secondary_query->query_vars['tribe_post__in'];
$post__in = $secondary_query->get( 'post__in', false );
if ( false !== $post__in ) {
$post__in = array_diff(
array_map( 'intval', (array) $post__in ),
array_map( 'intval', (array) $tribe__post_in )
);
if ( count( $post__in ) ) {
$secondary_query->query_vars['post__in'] = $post__in;
} else {
unset( $secondary_query->query_vars['post__in'] );
}
}
unset( $secondary_query->query_vars['tribe__post_in'] );
}
/**
* Alters the current query arguments to add the ones that will allow to collapse recurring event instances.
*
* @since 5.2.0
*/
protected function collapse_recurring_event_instances() {
global $wpdb;
if ( ! $this->has_date_filters() ) {
/*
* Let's not add costly queries if not really needed.
* If no date filters are being applied we just want the first event of a series.
*/
$this->filter_query->where( "{$wpdb->posts}.post_parent = 0" );
return;
}
// Flag the current operation state and avoid infinite loops.
$this->collapsing_recurring_event_instances = true;
/*
* To make the cut an event must fit the date(s) criteria and either:
* - have no children and no parent (a single event)
* - have children and have no children fitting the criteria (a series first event)
* - have a parent not fitting the criteria (a series instance)
*
* Here we clone the query, run it, and get all the events fitting the date criteria.
*/
$secondary_query = clone parent::get_query();
// Lighten the query fetching IDs only.
$secondary_query->set( 'fields', 'ids' );
// Fetch ALL matching event IDs to override what limits the pagination would apply.
$secondary_query->set( 'posts_per_page', - 1 );
// Prevent paging so we avoid invalid SQL: LIMIT 0, -1 errors out.
$secondary_query->set( 'nopaging', true );
// Order events, whatever the criteria applying to the main query, by start date.
$secondary_query->set( 'orderby', 'meta_value' );
$secondary_query->set( 'meta_key', '_EventStartDateUTC' );
/*
* Since we use the `post__in` query argument for our logic
* let's remove what `post__in` IDs we might have added to make sure we fetch the correct results.
*/
$this->clean_post__in( $secondary_query );
/*
* We need the SQL to get the `post_parent` and `meta_value` fields included in the results.
* There is no way to do that other than filtering the WordPress query generated in the context
* of a `get_posts` request.
*/
$filter = new Tribe__Repository__Query_Filters();
$filter->set_query( $secondary_query );
$filter->fields( 'post_parent' );
$filter->fields( $wpdb->postmeta . '.meta_value' );
$request = $filter->get_request();
$all_ids = $wpdb->get_results( $request, ARRAY_N );
if ( empty( $all_ids ) ) {
return;
}
/*
* Let's put events in races:
* 1. group them by post parent, or own if parent, ID
* 2. order them by start date in order
*/
$order = $secondary_query->get( 'order', 'ASC' );
$winners = array_reduce(
$all_ids,
static function ( array $acc, array $result ) use ( $order ) {
list( $post_id, $post_parent, $start_date ) = $result;
$post_id = (int) $post_id;
$post_parent = (int) $post_parent;
$post_parent = 0 === $post_parent ? $post_id : $post_parent;
$current = isset( $acc[ $post_parent ]['start_date'] ) ? $acc[ $post_parent ]['start_date'] : false;
if ( ! $current ) {
$acc[ $post_parent ]['start_date'] = $start_date;
} else {
$acc[ $post_parent ]['start_date'] = 'DESC' === $order ? max( $start_date, $current ) : min( $start_date, $current );
}
if ( $acc[ $post_parent ]['start_date'] !== $current ) {
$acc[ $post_parent ]['ID'] = $post_id;
}
return $acc;
},
[]
);
// Let's add a query argument to keep track of what post IDs we've added in the `post__in` clause.
$this->query_args['tribe_post__in'] = array_column( $winners, 'ID' );
$this->where( 'post__in', array_column( $winners, 'ID' ) );
$this->collapsing_recurring_event_instances = false;
}
}