+ __( 'Can I have more than one calendar?', 'tribe-common' ),
+ 'answer' => __( 'No, but you can use event categories or tags to display certain events like having...', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arh',
+ ],
+ [
+ 'question' => __( 'What do I get with Events Calendar Pro?', 'tribe-common' ),
+ 'answer' => __( 'Events Calendar Pro runs alongside The Events Calendar and enhances...' ),
+ 'link' => 'https://evnt.is/1arj',
+ ],
+ [
+ 'question' => __( 'How do I sell tickets to events?', 'tribe-common' ),
+ 'answer' => __( 'Use our free Event Tickets plugin to get started with tickets and RSVPs.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1ark',
+ ],
+ [
+ 'question' => __( 'Where can I find a list of available shortcodes?', 'tribe-common' ),
+ 'answer' => __( 'Our plugins include many shortcodes that do everything from embedding the calendar...', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arl',
+ ],
+ ] );
+
+ return $faqs;
+ }
+
+ /**
+ * Defines calendar extensions and displays them in the UI.
+ *
+ * @since 4.14.2
+ *
+ * @return array of extensions which are displayed on the calendar and community tab of the in-app help page.
+ */
+ public function get_calendar_extensions() {
+ $extensions = apply_filters( 'tec_help_calendar_extensions', [
+ [
+ 'title' => __( 'Calendar widget areas', 'tribe-common' ),
+ 'description' => __( 'This extension creates a useful variety of WordPress widget areas (a.k.a. sidebars).', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arc',
+ 'product-slug' => 'the-events-calendar',
+ ],
+ [
+ 'title' => __( 'Event block patterns', 'tribe-common' ),
+ 'description' => __( 'This extension adds a set of block patterns for events to the WordPress block editor.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1ard',
+ 'product-slug' => 'the-events-calendar',
+ ],
+ [
+ 'title' => __( 'Alternative photo view', 'tribe-common' ),
+ 'description' => __( 'This extension replaces photo view with a tiled grid of cards featuring event images.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1are',
+ 'product-slug' => 'events-calendar-pro',
+ ],
+ [
+ 'title' => __( 'The Events Calendar Tweaks', 'tribe-common' ),
+ 'description' => __( 'This extension is a collection of tweaks and snippets for The Events Calendar.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arg',
+ 'product-slug' => 'the-events-calendar',
+ ],
+ ] );
+
+ return $extensions;
+ }
+
+ /**
+ * Defines calendar products.
+ *
+ * @since 4.14.2
+ *
+ * @return array of products which are displayed on the calendar tab of the in-app help page.
+ */
+ public function get_calendar_products() {
+ $calendar_products = apply_filters( 'tec_help_calendar_products', [
+ 'events-calendar-pro',
+ 'tribe-filterbar',
+ 'event-aggregator',
+ 'event-automator',
+ 'events-virtual',
+ ] );
+
+ return $calendar_products;
+ }
+
+ /**
+ * Defines ticketing frequently asked questions and displays them in the UI.
+ *
+ * @since 4.14.2
+ *
+ * @return array of FAQs which are displayed on the ticketing tab of the in-app help page.
+ */
+ public function get_ticketing_faqs() {
+ $faqs = apply_filters( 'tec_help_ticketing_faqs', [
+ [
+ 'question' => __( 'How Do I create events with Tickets or RSVP’s?', 'tribe-common' ),
+ 'answer' => __( 'We’ve put together a video tutorial showing how to create events with Tickets using our plugins. Click on the link in the link in the title to learn more.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1art',
+ ],
+ [
+ 'question' => __( 'How Do I Set Up E-Commerce Plugins for Selling Tickets?', 'tribe-common' ),
+ 'answer' => __( 'You can sell tickets using our built-in e-commerce option, or upgrade to Event Tickets Plus to use ecommerce plugins such as WooCommerce.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arq',
+ ],
+ [
+ 'question' => __( 'Can I have a seating chart associated with my tickets?', 'tribe-common' ),
+ 'answer' => __( 'Yes! You can easily accomplish this task using the stock options and multiple ticket types available with Event Tickets.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arr',
+ ],
+ [
+ 'question' => __( 'How do I process refunds for tickets?', 'tribe-common' ),
+ 'answer' => __( 'When it comes to paid tickets, these orders can be refunded through the e-commerce platform in use.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1ars',
+ ],
+ ] );
+
+ return $faqs;
+ }
+
+ /**
+ * Defines ticketing extensions and displays them in the UI.
+ *
+ * @since 4.14.2
+ *
+ * @return array of extensions which are displayed on the ticketing tab of the in-app help page.
+ */
+ public function get_ticketing_extensions() {
+ $extensions = apply_filters( 'tec_help_ticketing_extensions', [
+ [
+ 'title' => __( 'Ticket Email Settings', 'tribe-common' ),
+ 'description' => __( 'Adds a new settings panel in Events > Settings that gives more control over the ticket and rsvp emails that are sent to attendees after registration.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arx',
+ 'product-slug' => 'event-tickets',
+ ],
+ [
+ 'title' => __( 'Per Event Check In API', 'tribe-common' ),
+ 'description' => __( 'This extension shows a meta box with an API key on each Event with Ticket/RSVP.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arw',
+ 'product-slug' => 'event-tickets',
+ ],
+ [
+ 'title' => __( 'Add Event & Attendee Info to WooCommerce Order Details', 'tribe-common' ),
+ 'description' => __( 'Displays the information collected by “attendee meta fields” in the WooCommerce order screens as well.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arv',
+ 'product-slug' => 'event-tickets',
+ ],
+ [
+ 'title' => __( 'Organizer Notification Email', 'tribe-common' ),
+ 'description' => __( 'This extension will send an email to event organizers whenever a user registers for their event.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1aru',
+ 'product-slug' => 'event-tickets',
+ ],
+ ] );
+
+ return $extensions;
+ }
+
+ /**
+ * Defines ticketing products.
+ *
+ * @since 4.14.2
+ *
+ * @return array of products which are displayed on the ticketing tab of the in-app help page.
+ */
+ public function get_ticketing_products() {
+ $ticketing_products = apply_filters( 'tec_help_ticketing_products', [
+ 'event-tickets',
+ 'event-tickets-plus',
+ 'tribe-eventbrite',
+ 'promoter',
+ ] );
+
+ return $ticketing_products;
+ }
+
+ /**
+ * Defines community extensions and displays them in the UI.
+ *
+ * @since 4.14.2
+ *
+ * @return array of extensions which are displayed on the community tab of the in-app help page.
+ */
+ public function get_community_extensions() {
+ $extensions = apply_filters( 'tec_help_ticketing_extensions', [
+ [
+ 'title' => __( 'Add Cost Currency Symbol', 'tribe-common' ),
+ 'description' => __( 'This extension allows you to set default currency symbols for your users to choose from instead of having a plain text field.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arn',
+ 'product-slug' => 'community-events',
+ ],
+ [
+ 'title' => __( 'Add Google Maps Display and Link Options', 'tribe-common' ),
+ 'description' => __( 'This extension adds the “Show Google Maps” and “Show Google Maps Link” checkboxes when creating a new Venue.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arm',
+ 'product-slug' => 'community-events',
+ ],
+ [
+ 'title' => __( 'Hide Others’ Organizers and Venues', 'tribe-common' ),
+ 'description' => __( 'This extension allows you to hide the Organizers and Venues that a visitor has not created from the Community Events submission form.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1aro',
+ 'product-slug' => 'community-events',
+ ],
+ [
+ 'title' => __( 'Display Custom HTML', 'tribe-common' ),
+ 'description' => __( 'This extension allows you to add custom HTML content to the top of the Community Events submission form.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1arp',
+ 'product-slug' => 'community-events',
+ ],
+ ] );
+
+ return $extensions;
+ }
+
+ /**
+ * Defines community products.
+ *
+ * @since 4.14.2
+ *
+ * @return array of products which are displayed on the community tab of the in-app help page.
+ */
+ public function get_community_products() {
+ $community_products = apply_filters( 'tec_help_ticketing_products', [
+ 'events-community',
+ 'events-community-tickets',
+ ] );
+
+ return $community_products;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Helpers.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Helpers.php
new file mode 100644
index 000000000..7f1d832b2
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Helpers.php
@@ -0,0 +1,193 @@
+doing_ajax() ) {
+ return false;
+ }
+
+ // Avoid Notices by checking the object type of WP_Screen
+ if ( ! $this->is_wp_screen() ) {
+ return false;
+ }
+
+ $defaults = apply_filters( 'tribe_is_post_type_screen_post_types', Tribe__Main::get_post_types() );
+
+ // Match any Post Type from Tribe
+ if ( is_null( $post_type ) && in_array( $current_screen->post_type, $defaults ) ) {
+ return true;
+ }
+
+ // Match any of the post_types set
+ if ( ! is_scalar( $post_type ) && in_array( $current_screen->post_type, (array) $post_type ) ) {
+ return true;
+ }
+
+ // Match a specific Post Type
+ if ( $current_screen->post_type === $post_type ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Matcher for administration pages that are from Tribe the easier way
+ *
+ * @param string|array|null $id What will be checked to see if we return true or false
+ *
+ * @return boolean
+ */
+ public function is_screen( $id = null ) {
+ global $current_screen;
+
+ // Not in the admin we don't even care
+ if ( ! is_admin() ) {
+ return false;
+ }
+
+ // Doing AJAX? bail.
+ if ( tribe( 'context' )->doing_ajax() ) {
+ return false;
+ }
+
+ // Avoid Notices by checking the object type of WP_Screen.
+ if ( ! $this->is_wp_screen() ) {
+ return false;
+ }
+
+ // Match any screen from Tribe.
+ if ( is_null( $id ) && false !== strpos( $current_screen->id, 'tribe' ) ) {
+ return true;
+ }
+
+ // Match any screen from TEC.
+ if ( is_null( $id ) && false !== strpos( $current_screen->id, 'tec' ) ) {
+ return true;
+ }
+
+ // Match any of the pages set
+ if ( ! is_scalar( $id ) && in_array( $current_screen->id, (array) $id ) ) {
+ return true;
+ }
+
+ // Match a specific page.
+ if ( $current_screen->id === $id ) {
+ return true;
+ }
+
+ // Match any post type page in the supported post types.
+ $defaults = apply_filters( 'tribe_is_post_type_screen_post_types', Tribe__Main::get_post_types() );
+ if ( in_array( $current_screen->post_type, $defaults ) ) {
+ return true;
+ }
+ return false;
+
+ }
+
+ /**
+ * Matcher for administration pages action
+ *
+ * @param string|array|null $action What will be checked to see if we return true or false
+ *
+ * @return boolean
+ */
+ public function is_action( $action = null ) {
+ global $current_screen;
+
+ // Not in the admin we don't even care
+ if ( ! is_admin() ) {
+ return false;
+ }
+
+ // Doing AJAX? bail.
+ if ( tribe( 'context' )->doing_ajax() ) {
+ return false;
+ }
+
+ // Avoid Notices by checking the object type of WP_Screen
+ if ( ! $this->is_wp_screen() ) {
+ return false;
+ }
+
+ // Match any of the pages set
+ if ( ! is_scalar( $action ) && in_array( $current_screen->action, (array) $action ) ) {
+ return true;
+ }
+
+ // Match a specific page
+ if ( $current_screen->action === $action ) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Live_Date_Preview.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Live_Date_Preview.php
new file mode 100644
index 000000000..9b53e99bc
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Live_Date_Preview.php
@@ -0,0 +1,68 @@
+ Settings > Display admin screen.
+ */
+class Tribe__Admin__Live_Date_Preview {
+ protected $target_fields = [
+ 'dateWithYearFormat',
+ 'dateWithoutYearFormat',
+ 'monthAndYearFormat',
+ 'weekDayFormat',
+ ];
+
+ /**
+ * Static Singleton Holder
+ *
+ * @var self
+ */
+ protected static $instance;
+
+ /**
+ * Static Singleton Factory Method
+ *
+ * @return self
+ */
+ public static function instance() {
+ return self::$instance ? self::$instance : self::$instance = new self;
+ }
+
+ /**
+ * Adds live date previews to the display settings tab (nothing is setup unless
+ * the user is actually on that tab).
+ */
+ public function __construct() {
+ add_action( 'tribe_settings_after_do_tabs', [ $this, 'listen' ] );
+ }
+
+ /**
+ * If the user looking at the Display settings tab, adds live date preview facilities.
+ */
+ public function listen() {
+ // We are only interested in the "Display" tab
+ if ( 'display' !== Tribe__Settings::instance()->currentTab ) {
+ return;
+ }
+
+ /**
+ * Add or remove fields which should have live date/time preview facilities.
+ *
+ * @var array $target_fields
+ */
+ $this->target_fields = (array) apply_filters( 'tribe_settings_date_preview_fields', $this->target_fields );
+
+ add_filter( 'tribe_field_div_end', [ $this, 'setup_date_previews' ], 10, 2 );
+
+ // We are still before `admin_enqueue_scripts` making it safe to use `tribe_asset`
+ tribe_asset( Tribe__Main::instance(), 'tribe-date-live-refresh', 'admin-date-preview.js', [ 'jquery' ], 'admin_enqueue_scripts' );
+ }
+
+ public function setup_date_previews( $html, $field ) {
+ // Not one of the fields we're interested in? Return without modification
+ if ( ! in_array( $field->id, $this->target_fields ) ) {
+ return $html;
+ }
+
+ $preview = esc_html( date_i18n( $field->value ) );
+ return " $preview $html";
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Date_Based.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Date_Based.php
new file mode 100644
index 000000000..79780d00c
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Date_Based.php
@@ -0,0 +1,378 @@
+tec_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Events__Main' );
+ $this->et_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Tickets__Main' );
+
+ $now = Dates::build_date_object( 'now', 'UTC' );
+ $notice_start = $this->get_start_time();
+ $notice_end = $this->get_end_time();
+ $extension_date = $this->get_extension_time();
+
+ // If we have an extension date defined.
+ if ( ! empty( $this->get_extension_time() ) ) {
+ // If the sale has started and
+ if (
+ $notice_start <= $now
+ && $notice_end < $now
+ && $now < $extension_date
+ ) {
+ add_filter( "tribe_{$this->slug}_notice_end_date", function() {
+ return $this->get_extension_time();
+ });
+ }
+ }
+
+ $this->hook();
+ }
+
+ /**
+ * Register the various Marketing notices.
+ *
+ * @since 4.14.2
+ */
+ public function hook() {
+ $this->hook_notice();
+ }
+
+ /**
+ * Register the notice.
+ *
+ * @since 4.14.2
+ */
+ public function hook_notice() {
+ tribe_notice(
+ $this->slug,
+ [ $this, "display_notice" ],
+ [
+ 'type' => 'tribe-banner',
+ 'dismiss' => 1,
+ 'priority' => -1,
+ 'wrap' => false,
+ ],
+ [ $this, "should_display" ]
+ );
+ }
+
+ /**
+ * HTML for the notice.
+ *
+ * @since 4.14.2
+ *
+ * @return string The HTML string to be displayed.
+ */
+ abstract function display_notice();
+
+ /**
+ * Function to get and filter the screens the notice is displayed on.
+ *
+ * @since 4.15.4
+ *
+ * @return array List of allowed screens.
+ */
+ public function get_screens() {
+ $screens = $this->screens;
+
+ /**
+ * Allows filtering of the screens for all date-based notices.
+ *
+ * @since 4.15.4
+ *
+ * @param array $screens The current list of allowed screens.
+ * @param string $slug The slug for the current notice.
+ *
+ * @return array $screens The modified list of allowed screens.
+ */
+ $screens = apply_filters(
+ 'tec_date_based_notice_get_screens',
+ $screens,
+ $this->slug
+ );
+
+ /**
+ * Allows filtering of the screens for a specific date-based notice.
+ *
+ * @since 4.15.4
+ *
+ * @param array $screens The current list of allowed screens.
+ *
+ * @return array $screens The modified list of allowed screens.
+ */
+ $screens = apply_filters(
+ "tec_date_based_notice_get_screens_{$this->slug}",
+ $screens
+ );
+
+ return $screens;
+ }
+
+ /**
+ * Whether the notice should display.
+ *
+ * @since 4.14.2
+ *
+ * @return boolean $should_display Whether the notice should display or not.
+ */
+ public function should_display() {
+ // If upsells have been manually hidden, respect that.
+ if ( tec_should_hide_upsell() ) {
+ return false;
+ }
+
+ $current_screen = get_current_screen();
+
+ $screens = $this->get_screens();
+
+ // If not a valid screen, don't display.
+ if ( empty( $current_screen->id ) || ! in_array( $current_screen->id, $screens, true ) ) {
+ return false;
+ }
+
+ $now = Dates::build_date_object( 'now', 'UTC' );
+ $notice_start = $this->get_start_time();
+ $notice_end = $this->get_end_time();
+
+ $should_display = $notice_start <= $now && $now < $notice_end;
+
+
+ /**
+ * Allow filtering of whether the notice should display.
+ *
+ * @since 4.14.2
+ *
+ * @param boolean $should_display Whether the notice should display.
+ * @param Tribe__Admin__Notice_Date_Based $notice The notice object.
+ */
+ return apply_filters( "tribe_{$this->slug}_notice_should_display", $should_display, $this );
+ }
+
+ /**
+ * Unix time for notice start.
+ *
+ * @since 4.14.2
+ *
+ * @return \Tribe\Utils\Date_I18n - Date Object
+ */
+ public function get_start_time() {
+ $date = Dates::build_date_object( $this->start_date, 'UTC' );
+ if ( $this->start_time !== null ) {
+ $date = $date->setTime( $this->start_time, 0 );
+ }
+
+ /**
+ * Allow filtering of the start date DateTime object,
+ * to allow for things like "the day before" ( $date->modify( '-1 day' ) ) and such.
+ *
+ * @since 4.14.2
+ *
+ * @param \DateTime $date Date object for the notice start.
+ */
+ $date = apply_filters( "tribe_{$this->slug}_notice_start_date", $date, $this );
+
+ return $date;
+ }
+
+ /**
+ * Unix time for notice end.
+ *
+ * @since 4.14.2
+ *
+ * @return \Tribe\Utils\Date_I18n - Date Object
+ */
+ public function get_end_time() {
+ $date = Dates::build_date_object( $this->end_date, 'UTC' );
+ if ( $this->end_time !== null ) {
+ $date = $date->setTime( $this->end_time, 0 );
+ }
+
+ /**
+ * Allow filtering of the end date DateTime object,
+ * to allow for things like "the day after" ( $date->modify( '+1 day' ) ) and such.
+ *
+ * @since 4.14.2
+ *
+ * @param \DateTime $date Date object for the notice end.
+ */
+ $date = apply_filters( "tribe_{$this->slug}_notice_end_date", $date, $this );
+
+ return $date;
+ }
+
+
+
+ /**
+ * Unix time for notice extension end.
+ *
+ * @since 4.15.4
+ *
+ * @return int $end_time The date & time the notice should stop displaying, as a Unix timestamp.
+ */
+ public function get_extension_time() {
+ if ( $this->extension_date === null ) {
+ return null;
+ }
+
+ $date = Dates::build_date_object( $this->extension_date, 'UTC' );
+
+ if ( $this->extension_time !== null ) {
+ $date = $date->setTime( $this->extension_time, 0 );
+ }
+
+ /**
+ * Allow filtering of the extension date DateTime object,
+ * to allow for things like "the day after" ( $date->modify( '+1 day' ) ) and such.
+ *
+ * @since 4.14.2
+ *
+ * @param \DateTime $date Date object for the notice end.
+ */
+ $date = apply_filters( "tribe_{$this->slug}_notice_extension_date", $date, $this );
+
+ return $date;
+ }
+
+ /**
+ * Gets the template instance used to setup the rendering of the page.
+ *
+ * @since 4.14.7
+ *
+ * @return \Tribe__Template
+ */
+ public function get_template() {
+ if ( empty( $this->template ) ) {
+ $this->template = new \Tribe__Template();
+ $this->template->set_template_origin( \Tribe__Main::instance() );
+ $this->template->set_template_folder( 'src/admin-views' );
+ $this->template->set_template_context_extract( true );
+ $this->template->set_template_folder_lookup( false );
+ }
+
+ return $this->template;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing.php
new file mode 100644
index 000000000..add054cb8
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing.php
@@ -0,0 +1,190 @@
+tec_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Events__Main' );
+ $this->et_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Tickets__Main' );
+ }
+
+ /**
+ * Register the various Marketing notices.
+ *
+ * @since 4.7.23
+ */
+ public function hook() {
+ $this->black_friday_hook_notice();
+ }
+
+ /**
+ * Register the Black Friday notice.
+ *
+ * @since 4.12.14
+ */
+ public function black_friday_hook_notice() {
+
+ tribe_notice(
+ 'black-friday',
+ [ $this, 'black_friday_display_notice' ],
+ [
+ 'type' => 'tribe-banner',
+ 'dismiss' => 1,
+ 'priority' => -1,
+ 'wrap' => false,
+ ],
+ [ $this, 'black_friday_should_display' ]
+ );
+ }
+
+ /**
+ * Unix time for Monday of Thanksgiving week @ 11am UTC. (11am UTC is 6am EST).
+ *
+ * @since 4.12.14
+ *
+ * @return int
+ */
+ public function get_black_friday_start_time() {
+ $date = Dates::build_date_object( 'fourth Thursday of November ' . date( 'Y' ), 'UTC' );
+ $date = $date->modify( '-3 days' );
+ $date = $date->setTime( 11, 0 );
+
+ $start_time = $date->format( 'U' );
+
+ /**
+ * Allow filtering of the Black Friday sale start date, mainly for testing purposes.
+ *
+ * @since 4.12.14
+ *
+ * @param int $bf_start_date Unix time for the Monday of Thanksgiving week @ 6am UTC.
+ */
+ return apply_filters( 'tribe_black_friday_start_time', $start_time );
+ }
+
+ /**
+ * Unix time for Dec 1 @ 5am UTC. (5am UTC is 12am EST).
+ *
+ * @since 4.12.14
+ *
+ * @return int
+ */
+ public function get_black_friday_end_time() {
+ $date = Dates::build_date_object( 'December 1st', 'UTC' );
+ $date = $date->setTime( 5, 0 );
+
+ $end_time = $date->format( 'U' );
+
+ /**
+ * Allow filtering of the Black Friday sale end date, mainly for testing purposes.
+ *
+ * @since 4.12.14
+ *
+ * @param int $bf_end_date Unix time for Dec 1 @ 6am UTC.
+ */
+ return apply_filters( 'tribe_black_friday_end_time', $end_time );
+ }
+ /**
+ * Whether the Black Friday notice should display.
+ *
+ * Unix times for Monday of Thanksgiving week @ 6am UTC and Dec 1 2020 @ 6am UTC.
+ * 6am UTC is midnight for TheEventsCalendar.com, which uses the America/Los_Angeles time zone.
+ *
+ * @since 4.12.14
+ *
+ * @return boolean
+ */
+ public function black_friday_should_display() {
+ // If upsells have been manually hidden, respect that.
+ if ( tec_should_hide_upsell() ) {
+ return false;
+ }
+
+ $now = Dates::build_date_object( 'now', 'UTC' )->format( 'U' );
+ $bf_sale_start = $this->get_black_friday_start_time();
+ $bf_sale_end = $this->get_black_friday_end_time();
+
+ $current_screen = get_current_screen();
+
+ $screens = [
+ 'tribe_events_page_tribe-app-shop', // App shop.
+ 'events_page_tribe-app-shop', // App shop.
+ 'tribe_events_page_tribe-common', // Settings & Welcome.
+ 'tribe_events_page_tec-events-settings', // New Settings & Welcome.
+ 'events_page_tribe-common', // Settings & Welcome.
+ 'toplevel_page_tribe-common', // Settings & Welcome.
+ ];
+
+ // If not a valid screen, don't display.
+ if ( empty( $current_screen->id ) || ! in_array( $current_screen->id, $screens, true ) ) {
+ return false;
+ }
+
+ return $bf_sale_start <= $now && $now < $bf_sale_end;
+ }
+
+ /**
+ * HTML for the Black Friday notice.
+ *
+ * @since 4.12.14
+ *
+ * @return string
+ */
+ public function black_friday_display_notice() {
+ Tribe__Assets::instance()->enqueue( [ 'tribe-common-admin' ] );
+
+ $current_screen = get_current_screen();
+
+ $icon_url = Tribe__Main::instance()->plugin_url . 'src/resources/images/icons/sale-burst.svg';
+ $cta_url = 'https://evnt.is/bf' . date( 'Y' );
+ $screens = [
+ 'tribe_events_page_tribe-common',
+ 'tribe_events_page_tec-events-settings',
+ 'events_page_tribe-common',
+ 'toplevel_page_tribe-common',
+ ];
+
+ // If we are on the settings page or a welcome page, change the Black Friday URL.
+ if (
+ ! empty( $current_screen->id )
+ && in_array( $current_screen->id, $screens )
+ ) {
+ if ( isset( $_GET['welcome-message-the-events-calendar'] ) || isset( $_GET['welcome-message-event-tickets' ] ) ) {
+ $cta_url .= 'welcome';
+ } else {
+ $cta_url .= 'settings';
+ }
+ }
+
+ ob_start();
+
+ include Tribe__Main::instance()->plugin_path . 'src/admin-views/notices/tribe-bf-general.php';
+
+ return ob_get_clean();
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Black_Friday.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Black_Friday.php
new file mode 100644
index 000000000..ddcb17f0d
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Black_Friday.php
@@ -0,0 +1,72 @@
+enqueue( [ 'tribe-common-admin' ] );
+
+ // Set up template variables.
+ $template_args = [
+ 'icon_url' => \Tribe__Main::instance()->plugin_url . 'src/resources/images/icons/sale-burst.svg',
+ 'cta_url' => 'https://evnt.is/1aqi',
+ 'end_date' => $this->get_end_time()->format_i18n( 'F jS' ),
+ ];
+
+ // Get the Black Friday notice content.
+ $content = $this->get_template()->template( 'notices/tribe-bf-general', $template_args, false );
+
+ return $content;
+ }
+
+ /**
+ * Unix time for notice start.
+ *
+ * @since 4.14.2
+ *
+ * @return \Tribe\Utils\Date_I18n - Date Object
+ */
+ public function get_start_time() {
+ $date = parent::get_start_time();
+ $date = $date->modify( '-3 days' );
+
+ return $date;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/End_Of_Year_Sale.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/End_Of_Year_Sale.php
new file mode 100644
index 000000000..30de2a3c3
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/End_Of_Year_Sale.php
@@ -0,0 +1,66 @@
+ \Tribe__Main::instance()->plugin_url . 'src/resources/images/marketing/eoy-sale-2021.svg',
+ 'cta_url' => 'https://evnt.is/1a-x',
+ ];
+
+ // Get the sale notice content.
+ $content = $this->get_template()->template( 'notices/end-of-year-sale', $template_args, false );
+
+ return $content;
+ }
+
+ /**
+ * Unix time for notice end.
+ *
+ * @since 4.14.9
+ *
+ * @return \Tribe\Utils\Date_I18n - Date Object
+ */
+ public function get_end_time() {
+ $date = parent::get_end_time();
+ $date = $date->setTime( 23, 59 );
+
+ return $date;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Stellar_Sale.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Stellar_Sale.php
new file mode 100644
index 000000000..232e72d60
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Stellar_Sale.php
@@ -0,0 +1,82 @@
+enqueue( [ 'tribe-common-admin' ] );
+
+ // Used in the template.
+ $cta_url = 'https://evnt.is/1bcv';
+ $stellar_url = 'https://evnt.is/1bcu';
+ $end_date = $this->get_end_time();
+ $template_args = [
+ 'cta_url' => 'https://evnt.is/1bcv',
+ 'stellar_url' => 'https://evnt.is/1bcu',
+ 'end_date' => $this->get_end_time(),
+ ];
+ $dependency = tribe( \Tribe__Dependency::class );
+
+ if ( $dependency->has_active_premium_plugin() ) {
+ return $this->get_template()->template( 'notices/tribe-stellar-sale-premium', $template_args, false );
+ } else {
+ return $this->get_template()->template( 'notices/tribe-stellar-sale', $template_args, false );
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Php_Version.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Php_Version.php
new file mode 100644
index 000000000..cfb18e4ee
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Php_Version.php
@@ -0,0 +1,106 @@
+ 'warning',
+ 'dismiss' => 1,
+ 'wrap' => 'p',
+ ],
+ [ $this, 'should_display' ]
+ );
+
+ }
+
+ /**
+ * Return the list of the Tribe active plugins
+ *
+ * @since 4.7.16
+ *
+ * @return string String of items
+ */
+ public function get_active_plugins() {
+
+ $active_plugins = Tribe__Dependency::instance()->get_active_plugins();
+
+ foreach ( $active_plugins as $active_plugin ) {
+
+ if ( ! $active_plugin['path'] ) {
+ continue;
+ }
+
+ $plugin_data = get_plugin_data( $active_plugin['path'] );
+ $plugins[] = $plugin_data['Name'];
+
+ }
+
+ return $this->implode_with_grammar( $plugins );
+
+ }
+
+ /**
+ * Implodes a list items using 'and' as the final separator and a comma everywhere else
+ *
+ * @param array $items List of items to implode
+ * @since 4.7.16
+ *
+ * @return string String of items
+ */
+ public function implode_with_grammar( $items ) {
+
+ $separator = _x( ', ', 'separator used in a list of items', 'tribe-common' );
+ $conjunction = _x( ' and ', 'the final separator in a list of two or more items', 'tribe-common' );
+ $output = $last_item = array_pop( $items );
+
+ if ( $items ) {
+ $output = implode( $separator, $items ) . $conjunction . $last_item;
+ }
+
+ return $output;
+ }
+
+ /**
+ * We only want to display notices for users
+ * who are in PHP < 5.6
+ *
+ * @since 4.7.16
+ *
+ * @return boolean
+ */
+ public function should_display() {
+ // Bail if the user is not admin or can manage plugins
+ if ( ! current_user_can( 'activate_plugins' ) ) {
+ return false;
+ }
+
+ return version_compare( PHP_VERSION, '7.4.0' ) < 0;
+ }
+
+ /**
+ * HTML for the PHP notice
+ *
+ * @since 4.7.16
+ *
+ * @return string
+ */
+ public function display_notice() {
+ /* Translators: %1$s list of plugins, %2$s current PHP version, %3$s open anchor html link for read more, %4$s open anchor html link for read more */
+ $text = _x(
+ 'Starting February 2023, %1$s will require PHP 7.4 or later. Currently, your site is using PHP version %2$s. Please update to a newer version. %3$sRead more%4$s.',
+ 'Message notifying users they need to upgrade PHP',
+ 'tribe-common'
+ );
+
+ $plugins = $this->get_active_plugins();
+
+ return sprintf( $text, $plugins, PHP_VERSION, '', '' );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Download.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Download.php
new file mode 100644
index 000000000..5efd1e234
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Download.php
@@ -0,0 +1,164 @@
+plugin_path = $plugin_path;
+
+ tribe_notice(
+ plugin_basename( $plugin_path ),
+ [ $this, 'show_inactive_plugins_alert' ]
+ );
+ }
+
+ /**
+ * Add a required plugin to the notice
+ *
+ * @since 4.8.3 Method introduced.
+ * @since 4.9 Added $version and $addon parameters.
+ * @since 4.9.12 Add $has_pue_notice param
+ * @since 4.9.17 Appended "+" to all version numbers to indicate "or any later version".
+ *
+ * @param string $name Name of the required plugin
+ * @param null $thickbox_url Download or purchase URL for plugin from within /wp-admin/ thickbox
+ * @param bool $is_active Indicates if the plugin is installed and active or not
+ * @param string $version Optional version number of the required plugin
+ * @param bool $addon Indicates if the plugin is an add-on for The Events Calendar or Event Tickets
+ * @param bool $has_pue_notice Indicates that we need to change the messaging due to expired key.
+ */
+ public function add_required_plugin( $name, $thickbox_url = null, $is_active = null, $version = null, $addon = false, $has_pue_notice = false ) {
+ $this->plugins_required[ $name ] = [
+ 'name' => $name,
+ 'thickbox_url' => $thickbox_url,
+ 'is_active' => $is_active,
+ 'version' => $version ? $version . '+' : null,
+ 'addon' => $addon,
+ 'has_pue_notice' => $has_pue_notice,
+ ];
+ }
+
+ /**
+ * Echoes the admin notice, attach to admin_notices
+ *
+ * @see \Tribe__Admin__Notice__Plugin_Download::add_required_plugin()
+ *
+ * @since 4.9.17 Altered the notice to remove "latest version" verbiage since "+" is now added to the version numbers.
+ */
+ public function show_inactive_plugins_alert() {
+ if ( ! current_user_can( 'activate_plugins' ) ) {
+ return;
+ }
+
+ $plugin_data = get_plugin_data( $this->plugin_path );
+ $req_plugins = [];
+
+ if ( empty( $this->plugins_required ) ) {
+ return;
+ }
+
+ // Make sure Thickbox is available and consistent appearance regardless of which admin page we're on
+ add_thickbox();
+
+ $has_pue_notices = false;
+
+ foreach ( $this->plugins_required as $req_plugin ) {
+ $item = $req_plugin['name'];
+ $version = empty( $req_plugin['version'] ) ? '' : ' (' . str_replace( '-dev', '', $req_plugin['version'] ) . ')';
+
+ if ( ! empty( $req_plugin['thickbox_url'] ) ) {
+ $item = sprintf(
+ '%3$s%4$s',
+ esc_attr( $req_plugin['thickbox_url'] ),
+ esc_attr( $req_plugin['name'] ),
+ esc_html( $item ),
+ esc_html( $version )
+ );
+ }
+
+ if ( false === $req_plugin['is_active'] ) {
+ $item = sprintf(
+ '%1$s',
+ $item
+ );
+ }
+
+ if ( ! empty( $req_plugin['addon'] ) ) {
+ $plugin_name[] = $req_plugin['name'];
+ }
+
+ $req_plugins[] = $item;
+
+ // If any of the items has PUE notice we will warn the user.
+ if ( $req_plugin['has_pue_notice'] ) {
+ $has_pue_notices = true;
+ }
+ }
+
+ // If empty then add in the default name.
+ if ( empty( $plugin_name[0] ) ) {
+ $plugin_name[] = $plugin_data['Name'];
+ }
+
+ $allowed_html = [
+ 'strong' => [],
+ 'a' => [ 'href' => [] ],
+ ];
+
+ $plugin_names_clean_text = wp_kses( $this->implode_with_grammar( $plugin_name ), $allowed_html );
+ $req_plugin_names_clean_text = wp_kses( $this->implode_with_grammar( $req_plugins ), $allowed_html );
+
+ $notice_html_content = '
' . esc_html__( 'To begin using %2$s, please install (or upgrade) and activate %3$s.', 'tribe-common' ) . '
';
+
+ $read_more_link = '' . esc_html__( 'Read more', 'tribe-common' ) . '.';
+ $pue_notice_text = esc_html__( 'There’s a new version of %1$s available, but your license is expired. You’ll need to renew your license to get access to the latest version. If you plan to continue using your current version of the plugin(s), be sure to use a compatible version of The Events Calendar. %2$s', 'tribe-common' );
+ $pue_notice_html = '
',
+ esc_attr( sanitize_title( $plugin_data['Name'] ) ),
+ $plugin_names_clean_text,
+ $req_plugin_names_clean_text
+ );
+ }
+
+ /**
+ * Implodes a list of items with proper grammar.
+ *
+ * If only 1 item, no grammar. If 2 items, just conjunction. If 3+ items, commas with conjunction.
+ *
+ * @param array $items List of items to implode
+ *
+ * @return string String of items
+ */
+ public function implode_with_grammar( $items ) {
+ $separator = _x( ', ', 'separator used in a list of items', 'tribe-common' );
+ $conjunction = _x( ' and ', 'the final separator in a list of two or more items', 'tribe-common' );
+ $output = $last_item = array_pop( $items );
+
+ if ( $items ) {
+ $output = implode( $separator, $items );
+
+ if ( 1 < count( $items ) ) {
+ $output .= $separator;
+ }
+
+ $output .= $conjunction . $last_item;
+ }
+
+ return $output;
+ }
+
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Upgrade_Notice.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Upgrade_Notice.php
new file mode 100644
index 000000000..819d85ba5
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Upgrade_Notice.php
@@ -0,0 +1,239 @@
+current_version = $current_version;
+ $this->plugin_path = $plugin_path;
+
+ add_action( "in_plugin_update_message-$plugin_path", [ $this, 'maybe_run' ] );
+ }
+
+ /**
+ * Test if there is a plugin upgrade notice and displays it if so.
+ *
+ * Expects to fire during "in_plugin_update_message-{plugin_path}", therefore
+ * this should only run if WordPress has detected that an upgrade is indeed
+ * available.
+ */
+ public function maybe_run() {
+ $this->test_for_upgrade_notice();
+
+ if ( $this->upgrade_notice ) {
+ $this->display_message();
+ }
+ }
+
+ /**
+ * Tests to see if an upgrade notice is available.
+ */
+ protected function test_for_upgrade_notice() {
+ $cache_key = $this->cache_key();
+ $this->upgrade_notice = get_transient( $cache_key );
+
+ if ( false === $this->upgrade_notice ) {
+ $this->discover_upgrade_notice();
+ }
+
+ set_transient( $cache_key, $this->upgrade_notice, $this->cache_expiration() );
+ }
+
+ /**
+ * Returns a cache key unique to the current plugin path and version, that
+ * still fits within the 45-char limit of regular WP transient keys.
+ *
+ * @return string
+ */
+ protected function cache_key() {
+ return 'tribe_plugin_upgrade_notice-' . hash( 'crc32b', $this->plugin_path . $this->current_version );
+ }
+
+ /**
+ * Returns the period of time (in seconds) for which to cache plugin upgrade messages.
+ *
+ * @return int
+ */
+ protected function cache_expiration() {
+ /**
+ * Number of seconds to cache plugin upgrade messages for.
+ *
+ * Defaults to one day, which provides a decent balance between efficiency savings
+ * and allowing for the possibility that some upgrade messages may be changed or
+ * rescinded.
+ *
+ * @var int $cache_expiration
+ */
+ return (int) apply_filters( 'tribe_plugin_upgrade_notice_expiration', DAY_IN_SECONDS, $this->plugin_path );
+ }
+
+ /**
+ * Looks at the current stable plugin readme.txt and parses to try and find the first
+ * available upgrade notice relating to a plugin version higher than this one.
+ *
+ * By default, WP SVN is the source.
+ */
+ protected function discover_upgrade_notice() {
+ /**
+ * The URL for the current plugin readme.txt file.
+ *
+ * @var string $url
+ * @var string $plugin_path
+ */
+ $readme_url = apply_filters( 'tribe_plugin_upgrade_readme_url',
+ $this->form_wp_svn_readme_url(),
+ $this->plugin_path
+ );
+
+ if ( ! empty( $readme_url ) ) {
+ $response = wp_safe_remote_get( $readme_url );
+ }
+
+ if ( ! empty( $response ) && ! is_wp_error( $response ) ) {
+ $readme = $response['body'];
+ }
+
+ if ( ! empty( $readme ) ) {
+ $this->parse_for_upgrade_notice( $readme );
+ $this->format_upgrade_notice();
+ }
+
+ /**
+ * The upgrade notice for the current plugin (may be empty).
+ *
+ * @var string $upgrade_notice
+ * @var string $plugin_path
+ */
+ return apply_filters( 'tribe_plugin_upgrade_notice',
+ $this->upgrade_notice,
+ $this->plugin_path
+ );
+ }
+
+ /**
+ * Forms the expected URL to the trunk readme.txt file as it is on WP SVN
+ * or an empty string if for any reason it cannot be determined.
+ *
+ * @return string
+ */
+ protected function form_wp_svn_readme_url() {
+ $parts = explode( '/', $this->plugin_path );
+ $slug = empty( $parts[0] ) ? '' : $parts[0];
+ return esc_url( "https://plugins.svn.wordpress.org/$slug/trunk/readme.txt" );
+ }
+
+ /**
+ * Given a standard Markdown-format WP readme.txt file, finds the first upgrade
+ * notice (if any) for a version higher than $this->current_version.
+ *
+ * @param string $readme
+ * @return string
+ */
+ protected function parse_for_upgrade_notice( $readme ) {
+ $in_upgrade_notice = false;
+ $in_version_notice = false;
+ $readme_lines = explode( "\n", $readme );
+
+ foreach ( $readme_lines as $line ) {
+ // Once we leave the Upgrade Notice section (ie, we encounter a new section header), bail
+ if ( $in_upgrade_notice && 0 === strpos( $line, '==' ) ) {
+ break;
+ }
+
+ // Look out for the start of the Upgrade Notice section
+ if ( ! $in_upgrade_notice && preg_match( '/^==\s*Upgrade\s+Notice\s*==/i', $line ) ) {
+ $in_upgrade_notice = true;
+ }
+
+ // Also test to see if we have left the version specific note (ie, we encounter a new sub heading or header)
+ if ( $in_upgrade_notice && $in_version_notice && 0 === strpos( $line, '=' ) ) {
+ break;
+ }
+
+ // Look out for the first applicable version-specific note within the Upgrade Notice section
+ if ( $in_upgrade_notice && ! $in_version_notice && preg_match( '/^=\s*\[?([0-9\.]{3,})\]?\s*=/', $line, $matches ) ) {
+ // Is this a higher version than currently installed?
+ if ( version_compare( $matches[1], $this->current_version, '>' ) ) {
+ $in_version_notice = true;
+ }
+ }
+
+ // Copy the details of the upgrade notice for the first higher version we find
+ if ( $in_upgrade_notice && $in_version_notice ) {
+ $this->upgrade_notice .= $line . "\n";
+ }
+ }
+ }
+
+ /**
+ * Convert the plugin version header and any links from Markdown to HTML.
+ */
+ protected function format_upgrade_notice() {
+ // Convert [links](http://...) to tags
+ $this->upgrade_notice = preg_replace(
+ '/\[([^\]]*)\]\(([^\)]*)\)/',
+ '${1}',
+ $this->upgrade_notice
+ );
+
+ // Convert =4.0= headings to
',
+ $this->upgrade_notice
+ );
+ }
+
+ /**
+ * Render the actual upgrade notice.
+ *
+ * Please note if plugin-specific styling is required for the message, you can
+ * use an ID generated by WordPress for one of the message's parent elements
+ * which takes the form "{plugin_name}-update". Example:
+ *
+ * #the-events-calendar-update .tribe-plugin-update-message { ... }
+ */
+ public function display_message() {
+ $notice = wp_kses_post( $this->upgrade_notice );
+ echo "
$notice
";
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Service_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Service_Provider.php
new file mode 100644
index 000000000..72ad60246
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Service_Provider.php
@@ -0,0 +1,66 @@
+hooks();
+ }
+
+ /**
+ * Set up hooks for classes.
+ *
+ * @since 4.14.2
+ */
+ private function hooks() {
+ add_action( 'tribe_plugins_loaded', [ $this, 'plugins_loaded'] );
+ }
+
+ /**
+ * Setup for things that require plugins loaded first.
+ *
+ * @since 4.14.2
+ */
+ public function plugins_loaded() {
+ tribe( 'pue.notices' );
+ tribe( 'admin.notice.php.version' );
+ tribe( WP_Version::class );
+
+ if ( defined( 'TRIBE_HIDE_MARKETING_NOTICES' ) ) {
+ return;
+ }
+
+ tribe( Marketing\Stellar_Sale::class );
+ tribe( Marketing\Black_Friday::class );
+ // EOY Sale disabled for 2022
+ // tribe( Marketing\End_Of_Year_Sale::class );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/WP_Version.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/WP_Version.php
new file mode 100644
index 000000000..8c3137957
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/WP_Version.php
@@ -0,0 +1,87 @@
+ 'warning',
+ 'dismiss' => 1,
+ 'priority' => -1,
+ 'wrap' => 'p',
+ ],
+ [ $this, 'wp_version_57_should_display' ]
+ );
+ }
+
+ /**
+ * Whether the WordPress 5.7 notice should display.
+ *
+ * @since 4.12.17
+ *
+ * @return boolean
+ */
+ public function wp_version_57_should_display() {
+ global $wp_version, $current_screen;
+
+ $screens = [
+ 'tribe_events_page_tribe-app-shop', // App shop.
+ 'events_page_tribe-app-shop', // App shop.
+ 'tribe_events_page_tribe-common', // Settings & Welcome.
+ 'tribe_events_page_tec-events-settings', // New Settings & Welcome.
+ 'events_page_tribe-common', // Settings & Welcome.
+ 'toplevel_page_tribe-common', // Settings & Welcome.
+ ];
+
+ // If not a valid screen, don't display.
+ if ( empty( $current_screen->id ) || ! in_array( $current_screen->id, $screens, true ) ) {
+ return false;
+ }
+
+ $wp_version_min_version_required = '5.8';
+ $common_version_required = '4.12.18-dev';
+
+ return
+ version_compare( Common::VERSION, $common_version_required, '<' )
+ && version_compare( $wp_version, $wp_version_min_version_required, '<' );
+ }
+
+ /**
+ * HTML for the WordPress 5.7 notice.
+ *
+ * @since 4.12.17
+ *
+ * @see https://evnt.is/wp5-7
+ *
+ * @return string
+ */
+ public function wp_version_57_display_notice() {
+ global $wp_version;
+ $is_wp_57 = version_compare( $wp_version, '5.7-beta', '>=' );
+ $html = '';
+
+ if ( $is_wp_57 ) {
+ $html .= esc_html__( 'You are using WordPress 5.7 which included a major jQuery update that may cause compatibility issues with past versions of The Events Calendar, Event Tickets and other plugins.', 'tribe-common' );
+ } else {
+ $html .= esc_html__( 'WordPress 5.7 includes a major jQuery update that may cause compatibility issues with past versions of The Events Calendar, Event Tickets and other plugins.', 'tribe-common' );
+ }
+ $html .= ' ' . esc_html__( 'Read more.', 'tribe-common' ) . '';
+
+ return $html;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notices.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notices.php
new file mode 100644
index 000000000..f1a2965b4
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notices.php
@@ -0,0 +1,821 @@
+doing_ajax() ) {
+ return;
+ }
+
+ // Hook the actual rendering of notices
+ add_action( 'current_screen', [ $this, 'hook' ], 20 );
+
+ // Add our notice dismissal script
+ tribe_asset(
+ Tribe__Main::instance(),
+ 'tribe-notice-dismiss',
+ 'notice-dismiss.js',
+ [ 'jquery' ],
+ null,
+ [
+ 'groups' => 'tec-admin-notices',
+ ]
+ );
+ }
+
+ /**
+ * This will happen on the `current_screen` and will hook to the correct actions and display the notices
+ *
+ * @since 4.3
+ *
+ * @return void
+ */
+ public function hook() {
+ $transients = $this->get_transients();
+
+ foreach ( $transients as $slug => $transient ) {
+ if ( $this->transient_notice_expired( $slug ) ) {
+ continue;
+ }
+ list( $html, $args, $expire ) = $transients[ $slug ];
+ $this->register( $slug, $html, $args );
+ }
+
+ foreach ( $this->notices as $notice ) {
+ if ( ! $this->showing_notice( $notice->slug ) ) {
+ continue;
+ }
+
+ add_action( $notice->action, $notice->callback, $notice->priority );
+ }
+ }
+
+ /**
+ * This will allow the user to Dismiss the Notice using JS.
+ *
+ * We will dismiss the notice without checking to see if the slug was already
+ * registered (via a call to exists()) for the reason that, during dismissal
+ * ajax request, some valid notices may not have been registered yet.
+ *
+ * @since 4.3
+ *
+ * @return void
+ */
+ public function maybe_dismiss() {
+ if ( empty( $_GET[ self::$meta_key ] ) ) {
+ wp_send_json( false );
+ }
+
+ $slug = sanitize_key( $_GET[ self::$meta_key ] );
+
+ // Send a JSON answer with the status of dismissal
+ wp_send_json( $this->dismiss( $slug ) );
+ }
+
+ /**
+ * Allows a Magic to remove the Requirement of creating a callback
+ *
+ * @since 4.3
+ *
+ * @param string $name Name of the method used to create the slug of the notice.
+ * @param array $arguments Which arguments were used, normally empty.
+ *
+ * @return string
+ */
+ public function __call( $name, $arguments ) {
+ // Transform from Method name to Notice number
+ $slug = preg_replace( '/render_/', '', $name, 1 );
+
+ if ( ! $this->exists( $slug ) ) {
+ return false;
+ }
+
+ $notice = $this->get( $slug );
+
+ if (
+ empty( $notice->active_callback )
+ || (
+ is_callable( $notice->active_callback )
+ && true == call_user_func( $notice->active_callback )
+ )
+ ) {
+ $content = $notice->content;
+ $wrap = isset( $notice->wrap ) ? $notice->wrap : false;
+
+ if ( is_array( $content ) && isset( $content[0] ) && $content[0] instanceof __PHP_Incomplete_Class ) {
+ // From a class that no longer exists (e.g. the plugin is not active), clean and bail.
+ $this->remove( $slug );
+ $this->remove_transient( $slug );
+
+ return false;
+ }
+
+ if ( is_callable( $content ) ) {
+ $content = call_user_func_array( $content, [ $notice ] );
+ }
+
+ if ( empty( $content ) ) {
+ // There is nothing to render, let's avoid the empty notice frame.
+ return false;
+ }
+
+ tribe_asset_enqueue_group( 'tec-admin-notices' );
+
+ // Return the rendered HTML.
+ $html = $this->render( $slug, $content, false, $wrap );
+
+ // Remove the notice and the transient (if any) since it's been rendered.
+ $this->remove( $slug );
+ $this->remove_transient( $slug );
+
+ return $html;
+ }
+
+ return false;
+ }
+
+ /**
+ * This is a helper to actually print the Message
+ *
+ * @since 4.3
+ *
+ * @param string $slug The name of the notice.
+ * @param string $content The content of the notice.
+ * @param boolean $return Echo or return the content.
+ * @param string|bool $wrap An optional HTML tag to wrap the content.
+ *
+ * @return bool|string
+ */
+ public function render( $slug, $content = null, $return = true, $wrap = false ) {
+ if ( ! $this->exists( $slug ) ) {
+ return false;
+ }
+
+ // Bail if we already rendered
+ if ( $this->is_rendered( $slug ) ) {
+ if ( $this->is_rendered_html( $slug, $content ) && ! $return ) {
+ echo $content;
+ }
+
+ return false;
+ }
+
+ $notice = $this->get( $slug );
+ $this->notices[ $slug ]->is_rendered = true;
+
+ $classes = [ 'tribe-dismiss-notice', 'notice' ];
+ $classes[] = sanitize_html_class( 'notice-' . $notice->type );
+ $classes[] = sanitize_html_class( 'tribe-notice-' . $notice->slug );
+
+ if ( $notice->dismiss ) {
+ $classes[] = 'is-dismissible';
+ }
+
+ if ( $notice->inline ) {
+ $classes[] = 'inline';
+ }
+
+ // Prevents Empty Notices
+ if ( empty( $content ) ) {
+ return false;
+ }
+
+ if ( is_string( $wrap ) ) {
+ $content = sprintf( '<%1$s>' . $content . '%1$s>', $wrap );
+ }
+
+ $html = sprintf( '
%s
', implode( ' ', $classes ), $notice->slug, $content );
+ tribe_asset_enqueue_group( 'tec-admin-notices' );
+
+ if ( ! $return ) {
+ echo $html;
+ }
+
+ return $html;
+ }
+
+ /**
+ * This is a helper to print the message surrounded by `p` tags.
+ *
+ * @since 4.3
+ *
+ * @param string $slug The name of the notice.
+ * @param string $content The content of the notice.
+ * @param boolean $return Echo or return the content.
+ *
+ * @return boolean|string
+ */
+ public function render_paragraph( $slug, $content = null, $return = true ) {
+ return $this->render( $slug, $content, $return, 'p' );
+ }
+
+ /**
+ * Checks if a given notice is rendered
+ *
+ * @since 4.7.10
+ *
+ * @param string $slug Which notice to check.
+ *
+ * @return boolean
+ */
+ public function is_rendered( $slug ) {
+ if ( ! $this->exists( $slug ) ) {
+ return false;
+ }
+
+ $notice = $this->get( $slug );
+
+ return isset( $notice->is_rendered ) ? $notice->is_rendered : false;
+ }
+
+ /**
+ * Checks if a given string is a notice rendered
+ *
+ * @since 4.7.10
+ *
+ * @param string $slug Which notice to check.
+ * @param string $html Which html string we are check.
+ *
+ * @return boolean
+ */
+ public function is_rendered_html( $slug, $html ) {
+ if ( ! $this->exists( $slug ) ) {
+ return false;
+ }
+
+ $search = sprintf( 'data-ref="%s"', $slug );
+
+ return false !== strpos( $html, $search );
+ }
+
+ /**
+ * Checks if a given user has dismissed a given notice.
+ *
+ * @since 4.13.0
+ *
+ * @param string $slug The name of the notice.
+ * @param int|null $user_id The user ID.
+ *
+ * @return boolean
+ */
+ public function has_user_dismissed( $slug, $user_id = null ) {
+
+ if ( is_null( $user_id ) ) {
+ $user_id = get_current_user_id();
+ }
+
+ $dismissed_notices = get_user_meta( $user_id, self::$meta_key );
+
+ if ( ! is_array( $dismissed_notices ) ) {
+ return false;
+ }
+
+ if ( ! in_array( $slug, $dismissed_notices ) ) {
+ return false;
+ }
+
+ $notice = $this->get( $slug );
+ if (
+ is_object( $notice )
+ && $notice->recurring
+ && $this->should_recurring_notice_show( $slug, $user_id )
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if a given user has dismissed a given notice.
+ *
+ * @since 4.3
+ * @deprecated 4.13.0 Deprecated in favor of correcting the typo.
+ *
+ * @param string $slug The Name of the Notice
+ * @param int|null $user_id The user ID
+ *
+ * @return boolean
+ */
+ public function has_user_dimissed( $slug, $user_id = null ) {
+ return $this->has_user_dismissed( $slug, $user_id );
+ }
+
+ /**
+ * Gets the last Dismissal for a given notice slug and user.
+ *
+ * @since 4.13.0
+ *
+ * @param string $slug Slug of the notice to look for.
+ * @param int|null $user_id Which user? If null will default to current user.
+ *
+ * @return false|\Tribe\Utils\Date_I18n
+ */
+ public function get_last_dismissal( $slug, $user_id = null ) {
+ if ( is_null( $user_id ) ) {
+ $user_id = get_current_user_id();
+ }
+
+ $dismissed_time = get_user_meta( $user_id, static::$meta_key_time_prefix . $slug, true );
+
+ if ( ! is_numeric( $dismissed_time ) ) {
+ return false;
+ }
+
+ return Dates::build_date_object( $dismissed_time );
+ }
+
+ /**
+ * Determines if a given notice needs to be re-displayed in case of recurring notice.
+ *
+ * @since 4.13.0
+ *
+ * @param string $slug Slug of the notice to look for.
+ * @param int|null $user_id Which user? If null will default to current user.
+ *
+ * @return false|\Tribe\Utils\Date_I18n
+ */
+ public function should_recurring_notice_show( $slug, $user_id = null ) {
+ $notice = $this->get( $slug );
+ if ( ! is_object( $notice ) ) {
+ return false;
+ }
+
+ if ( ! $notice->recurring || ! $notice->recurring_interval ) {
+ return false;
+ }
+
+ if ( is_null( $user_id ) ) {
+ $user_id = get_current_user_id();
+ }
+
+ $interval = Dates::interval( $notice->recurring_interval );
+ $last_dismissal = $this->get_last_dismissal( $slug, $user_id );
+ if ( ! $last_dismissal ) {
+ return false;
+ }
+
+ $next_dismissal = $last_dismissal->add( $interval );
+ $now = Dates::build_date_object( 'now' );
+
+ if ( $now >= $next_dismissal ) {
+ delete_user_meta( $user_id, self::$meta_key, $slug );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * A Method to actually add the Meta value telling that this notice has been dismissed
+ *
+ * @since 4.3
+ *
+ * @param string $slug The Name of the Notice
+ * @param int|null $user_id The user ID
+ *
+ * @return boolean
+ */
+ public function dismiss( $slug, $user_id = null ) {
+ if ( is_null( $user_id ) ) {
+ $user_id = get_current_user_id();
+ }
+
+ // If this user has dismissed we don't care either
+ if ( $this->has_user_dismissed( $slug, $user_id ) ) {
+ return true;
+ }
+
+ update_user_meta( $user_id, static::$meta_key_time_prefix . $slug, time() );
+
+ return add_user_meta( $user_id, self::$meta_key, $slug, false );
+ }
+
+ /**
+ * Removes the User meta holding if a notice was dismissed
+ *
+ * @param string $slug The Name of the Notice
+ * @param int|null $user_id The user ID
+ *
+ * @return boolean
+ */
+ public function undismiss( $slug, $user_id = null ) {
+ if ( is_null( $user_id ) ) {
+ $user_id = get_current_user_id();
+ }
+
+ // If this user has dismissed we don't care either
+ if ( ! $this->has_user_dismissed( $slug, $user_id ) ) {
+ return false;
+ }
+
+ return delete_user_meta( $user_id, self::$meta_key, $slug );
+ }
+
+ /**
+ * Undismisses the specified notice for all users.
+ *
+ * @since 4.3
+ *
+ * @param string $slug
+ *
+ * @return int
+ */
+ public function undismiss_for_all( $slug ) {
+ $user_query = new WP_User_Query( [
+ 'meta_key' => self::$meta_key,
+ 'meta_value' => $slug,
+ ] );
+
+ $affected = 0;
+
+ foreach ( $user_query->get_results() as $user ) {
+ if ( $this->undismiss( $slug, $user->ID ) ) {
+ $affected ++;
+ }
+ }
+
+ return $affected;
+ }
+
+ /**
+ * Register a Notice and attach a callback to the required action to display it correctly
+ *
+ * @since 4.3
+ *
+ * @param string $slug Slug to save the notice
+ * @param callable|string $callback A callable Method/Function to actually display the notice
+ * @param array $arguments Arguments to Setup a notice
+ * @param callable|null $active_callback An optional callback that should return bool values
+ * to indicate whether the notice should display or not.
+ *
+ * @return stdClass
+ */
+ public function register( $slug, $callback, $arguments = [], $active_callback = null ) {
+ // Prevent weird stuff here
+ $slug = sanitize_key( $slug );
+
+ $defaults = [
+ 'callback' => null,
+ 'content' => null,
+ 'action' => 'admin_notices',
+ 'priority' => 10,
+ 'expire' => false,
+ 'dismiss' => false,
+ 'inline' => false,
+ 'recurring' => false,
+ 'recurring_interval' => null,
+ 'type' => 'error',
+ 'is_rendered' => false,
+ 'wrap' => false,
+ ];
+
+ $defaults['callback'] = [ $this, 'render_' . $slug ];
+ $defaults['content'] = $callback;
+
+ if ( is_callable( $active_callback ) ) {
+ $defaults['active_callback'] = $active_callback;
+ }
+
+ // Merge Arguments
+ $notice = (object) wp_parse_args( $arguments, $defaults );
+
+ // Enforce this one
+ $notice->slug = $slug;
+
+ // Clean these
+ $notice->priority = absint( $notice->priority );
+ $notice->expire = (bool) $notice->expire;
+ $notice->recurring = (bool) $notice->recurring;
+
+ if ( ! is_callable( $notice->dismiss ) ) {
+ $notice->dismiss = (bool) $notice->dismiss;
+ }
+ if ( ! is_callable( $notice->inline ) ) {
+ $notice->inline = (bool) $notice->inline;
+ }
+
+ // Set the Notice on the array of notices
+ $this->notices[ $slug ] = $notice;
+
+ // Return the notice Object because it might be modified
+ return $notice;
+ }
+
+ /**
+ * Create a transient Admin Notice easily.
+ *
+ * A transient admin notice is a "fire-and-forget" admin notice that will display once registered and
+ * until dismissed (if dismissible) without need, on the side of the source code, to register it on each request.
+ *
+ * @since 4.7.7
+ *
+ * @param string $slug Slug to save the notice
+ * @param string $html The notice output HTML code
+ * @param array $arguments Arguments to Setup a notice
+ * @param int $expire After how much time (in seconds) the notice will stop showing.
+ *
+ * @return stdClass Which notice was registered
+ */
+ public function register_transient( $slug, $html, $arguments = [], $expire = null ) {
+ $notices = $this->get_transients();
+ $notices[ $slug ] = [ $html, $arguments, time() + $expire ];
+ $this->set_transients( $notices );
+ }
+
+ /**
+ * Removes a transient notice based on its slug.
+ *
+ * @since 4.7.7
+ *
+ * @param string $slug
+ */
+ public function remove_transient( $slug ) {
+ $notices = $this->get_transients();
+ unset( $notices[ $slug ] );
+ $this->set_transients( $notices );
+ }
+
+ /**
+ * Removes a notice based on its slug.
+ *
+ * @since 4.3
+ *
+ * @param string $slug
+ *
+ * @return bool
+ */
+ public function remove( $slug ) {
+ if ( ! $this->exists( $slug ) ) {
+ return false;
+ }
+
+ unset( $this->notices[ $slug ] );
+
+ return true;
+ }
+
+ /**
+ * Gets the configuration for the Notices
+ *
+ * @since 4.3
+ *
+ * @param string $slug
+ *
+ * @return object|array|null
+ */
+ public function get( $slug = null ) {
+ if ( is_null( $slug ) ) {
+ return $this->notices;
+ }
+
+ // Prevent weird stuff here
+ $slug = sanitize_key( $slug );
+
+ if ( ! empty( $this->notices[ $slug ] ) ) {
+ // I want to avoid modifying the registered value.
+ $notice = $this->notices[ $slug ];
+
+ if ( is_callable( $notice->inline ) ) {
+ $notice->inline = call_user_func( $notice->inline, $notice );
+ }
+
+ if ( is_callable( $notice->dismiss ) ) {
+ $notice->dismiss = call_user_func( $notice->dismiss, $notice );
+ }
+
+ return $notice;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if a given notice exists
+ *
+ * @since 4.3
+ *
+ * @param string $slug
+ *
+ * @return bool
+ */
+ public function exists( $slug ) {
+ return is_object( $this->get( $slug ) ) ? true : false;
+ }
+
+ /**
+ * Returns an array of registered transient notices.
+ *
+ * @since 4.7.7
+ *
+ * @return array An associative array in the shape [ => [ , , ] ]
+ */
+ protected function get_transients() {
+ $cached = tribe( 'cache' )['transient_admin_notices'];
+
+ if ( false !== $cached ) {
+ return $cached;
+ }
+
+ $transient = self::$transient_notices_name;
+ $notices = get_transient( $transient );
+ $notices = is_array( $notices ) ? $notices : [];
+
+ if ( $this->did_prune_transients ) {
+ $this->did_prune_transients = true;
+ foreach ( $notices as $key => $notice ) {
+ list( $html, $args, $expire_at ) = $notice;
+
+ if ( $expire_at < time() ) {
+ unset( $notices[ $key ] );
+ }
+ }
+ }
+
+ tribe( 'cache' )['transient_admin_notices'] = $notices;
+
+ return $notices;
+ }
+
+ /**
+ * Updates/sets the transient notices transient.
+ *
+ * @since 4.7.7
+ *
+ * @param array $notices An associative array in the shape [ => [ , , ] ]
+ */
+ protected function set_transients( $notices ) {
+ $transient = self::$transient_notices_name;
+ set_transient( $transient, $notices, MONTH_IN_SECONDS );
+ }
+
+ /**
+ * Checks whether a specific transient admin notices is being shown or not, depending on its expiration and
+ * dismissible status.
+ *
+ *
+ * @since 4.11.1
+ *
+ * @param string|array $slug The slug, or slugs, of the transient notices to check. This is the same slug used
+ * to register the transient notice in the `tribe_transient_notice` function or the
+ * `Tribe__Admin__Notices::register_transient()` method.
+ *
+ * @return bool Whether the transient notice is showing or not.
+ */
+ public function showing_transient_notice( $slug ) {
+ $transient_notices = (array) $this->get_transients();
+
+ return isset( $transient_notices[ $slug ] )
+ && ! $this->has_user_dismissed( $slug )
+ && ! $this->transient_notice_expired( $slug );
+ }
+
+ /**
+ * Checks whether a transient notice expired or not.
+ *
+ * @since 4.11.1
+ *
+ * @param string|array $slug The slug, or slugs, of the transient notices to check. This is the same slug used
+ * to register the transient notice in the `tribe_transient_notice` function or the
+ * `Tribe__Admin__Notices::register_transient()` method.
+ *
+ * @return bool Whether the transient notice is expired or not.
+ */
+ protected function transient_notice_expired( $slug ) {
+ $transients = (array) $this->get_transients();
+
+ if ( ! isset( $transients[ $slug ] ) ) {
+ return true;
+ }
+
+ list( $html, $args, $expire ) = $transients[ $slug ];
+ if ( $expire < time() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether a notice is being shown or not; the result takes the notice callback and dismissible status into
+ * account.
+ *
+ * @since 4.11.1
+ *
+ * @param string|array $slug The slug, or slugs, of the transient notices to check. This is the same slug used
+ * to register the transient notice in the `tribe_transient_notice` function or the
+ * `Tribe__Admin__Notices::register_transient()` method.
+ *
+ * @return bool Whether the notice is showing or not.
+ */
+ public function showing_notice( $slug ) {
+ if ( ! isset( $this->notices[ $slug ] ) ) {
+ return false;
+ }
+
+ $notice = $this->notices[ $slug ];
+ if ( $notice->dismiss && $this->has_user_dismissed( $notice->slug ) ) {
+ return false;
+ }
+
+ if (
+ ! empty( $notice->active_callback )
+ && is_callable( $notice->active_callback )
+ && false == call_user_func( $notice->active_callback )
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Pages.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Pages.php
new file mode 100644
index 000000000..3a05e4f84
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Pages.php
@@ -0,0 +1,309 @@
+
+ */
+ private $pages = [];
+
+ /**
+ * Get registered pages.
+ *
+ * @since 4.15.0
+ *
+ * @return array $pages {
+ * Array containing the registered pages.
+ *
+ * @type array $page_id {
+ * @type string id Id to reference the page.
+ * @type array title Page title. Used in menus and breadcrumbs.
+ * @type string|null parent Parent ID. Null for new top level page.
+ * @type string path Path for this page, full path in app context; ex /analytics/report
+ * @type string capability Capability needed to access the page.
+ * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
+ * @type int position Menu item position.
+ * @type int order Navigation item order.
+ * @type callable callback The function to be called to output the content for the page.
+ * }
+ * }
+ */
+ public function get_pages() {
+ /**
+ * Filters the list of registered TEC admin pages.
+ *
+ * @since 4.15.0
+ *
+ * @param array $pages {
+ * Array containing the registered pages to be filtered
+ *
+ * @type array $page_id {
+ * @type string id Id to reference the page.
+ * @type array title Page title. Used in menus and breadcrumbs.
+ * @type string|null parent Parent ID. Null for new top level page.
+ * @type string path Path for this page, full path in app context; ex /analytics/report
+ * @type string capability Capability needed to access the page.
+ * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
+ * @type int position Menu item position.
+ * @type int order Navigation item order.
+ * @type callable callback The function to be called to output the content for the page.
+ * }
+ * }
+ */
+ $pages = apply_filters( 'tec_admin_pages', $this->pages );
+
+ return $pages;
+ }
+
+ /**
+ * Adds a page to `tec-admin`.
+ *
+ * @since 4.15.0
+ *
+ * @param array $options {
+ * Array describing the page.
+ *
+ * @type string id Id to reference the page.
+ * @type string title Page title. Used in menus and breadcrumbs.
+ * @type string|null parent Parent ID. Null for new top level page.
+ * @type string path Path for this page, full path in app context; ex /analytics/report
+ * @type string capability Capability needed to access the page.
+ * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
+ * @type int position Menu item position.
+ * @type int order Navigation item order.
+ * @type callable callback The function to be called to output the content for the page.
+ * }
+ *
+ * @return string $page The resulting page's hook_suffix.
+ *
+ */
+ public function register_page( $options = [] ) {
+ $defaults = [
+ 'id' => null,
+ 'parent' => null,
+ 'title' => '',
+ 'capability' => self::get_capability(),
+ 'path' => '',
+ 'icon' => '',
+ 'position' => null,
+ 'callback' => [ __CLASS__, 'render_page' ],
+ ];
+
+ $options = wp_parse_args( $options, $defaults );
+
+ if ( is_null( $options['parent'] ) ) {
+ $page = add_menu_page(
+ $options['title'],
+ $options['title'],
+ $options['capability'],
+ $options['path'],
+ $options['callback'],
+ $options['icon'],
+ $options['position']
+ );
+ } else {
+ $page = add_submenu_page(
+ $options['parent'],
+ $options['title'],
+ $options['title'],
+ $options['capability'],
+ $options['path'],
+ $options['callback']
+ );
+ }
+
+ $this->connect_page( $options );
+
+ return $page;
+ }
+
+ /**
+ * Get the current page.
+ *
+ * @since 4.15.0
+ *
+ * @return string|boolean Current page or false if not registered with this controller.
+ */
+ public function get_current_page() {
+ if ( is_null( $this->current_page ) ) {
+ $this->determine_current_page();
+ }
+
+ return $this->current_page;
+ }
+
+ /**
+ * Determine the current page.
+ *
+ * @since 4.15.0
+ *
+ * @return string|boolean Current page or false if not registered with this controller.
+ */
+ public function determine_current_page() {
+ $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
+
+ if ( is_null( $current_screen ) ) {
+ $this->current_page = tribe_get_request_var( 'page' );
+ return $this->current_page;
+ }
+
+ $this->current_page = $current_screen->id;
+
+ return $this->current_page;
+ }
+
+ /**
+ * Connect an existing page to wp-admin.
+ *
+ * @since 4.15.0
+ *
+ * @param array $options {
+ * Array describing the page.
+ *
+ * @type string id Id to reference the page.
+ * @type string|array title Page title. Used in menus and breadcrumbs.
+ * @type string|null parent Parent ID. Null for new top level page.
+ * @type string path Path for this page. E.g. admin.php?page=wc-settings&tab=checkout
+ * @type string capability Capability needed to access the page.
+ * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
+ * @type int position Menu item position.
+ * }
+ */
+ public function connect_page( $options = [] ) {
+ if ( ! is_array( $options['title'] ) ) {
+ $options['title'] = array( $options['title'] );
+ }
+
+ /**
+ * Filter the options when connecting or registering a page.
+ *
+ * @param array $options {
+ * Array describing the page.
+ *
+ * @type string id Id to reference the page.
+ * @type string|array title Page title. Used in menus and breadcrumbs.
+ * @type string|null parent Parent ID. Null for new top level page.
+ * @type string screen_id The screen ID that represents the connected page. (Not required for registering).
+ * @type string path Path for this page. E.g. admin.php?page=wc-settings&tab=checkout
+ * @type string capability Capability needed to access the page.
+ * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
+ * @type int position Menu item position.
+ * @type boolean js_page If this is a JS-powered page.
+ * }
+ */
+ $options = apply_filters( 'tec_admin_pages_connect_page_options', $options );
+
+ $this->pages[ $options['id'] ] = $options;
+ }
+
+ /**
+ * Get the capability.
+ *
+ * @param string $capability The capability required for a TEC page to be displayed to the user.
+ *
+ * @since 4.15.0
+ *
+ * @return string The capability required for a TEC page to be displayed to the user.
+ */
+ public static function get_capability( $capability = 'manage_options' ) {
+ /**
+ * Filters the default capability for Tribe admin pages.
+ *
+ * @param string $capability The capability required for a TEC page to be displayed to the user.
+ *
+ * @todo: We'll need to deprecate this one in favor of the one below.
+ */
+ $capability = apply_filters( 'tribe_common_event_page_capability', $capability );
+
+ /**
+ * Filters the default capability for TEC admin pages.
+ *
+ * @param string $capability The capability required for a TEC page to be displayed to the user.
+ *
+ * @since 4.15.0
+ */
+ $capability = apply_filters( 'tec_admin_pages_capability', $capability );
+
+ return $capability;
+ }
+
+ /**
+ * Define if is a `tec` admin page (registered).
+ *
+ * @since 4.15.0
+ *
+ * @param string $page_id The ID of the page to check if is a `tec` admin page.
+ *
+ * @return boolean True if is a `tec` admin page, false otherwise.
+ */
+ public function is_tec_page( $page_id = '' ) {
+ return in_array( $page_id, array_keys( $this->pages ), true );
+ }
+
+ /**
+ * Get pages with tabs.
+ * @since 4.15.0
+ *
+ * @param array $pages The list of pages with tabs.
+ * @return array $pages The list of pages with tabs, filtered.
+ */
+ public function get_pages_with_tabs( $pages = [] ) {
+ /**
+ * Filters the pages with tabs.
+ *
+ * @param array $pages Pages with tabs.
+ *
+ * @since 4.15.0
+ */
+ return apply_filters(
+ 'tec_admin_pages_with_tabs',
+ $pages
+ );
+ }
+
+ /**
+ * Check if the current page has tabs.
+ *
+ * @since 4.15.0
+ *
+ * @param string $page The page slug.
+ * @return boolean True if the page has tabs, false otherwise.
+ */
+ public function has_tabs( $page = '' ) {
+ if ( empty( $page ) ) {
+ $page = $this->get_current_page();
+ }
+
+ return in_array( $page, $this->get_pages_with_tabs() );
+ }
+
+ /**
+ * Generic page.
+ *
+ * @since 4.15.0
+ */
+ public static function render_page() {
+ ?>
+
+ should_setup_pages() ) {
+ return;
+ }
+
+ $page_title = esc_html__( 'Troubleshooting', 'tribe-common' );
+ $menu_title = esc_html__( 'Troubleshooting', 'tribe-common' );
+
+ $capability = $this->get_required_capability();
+
+ $where = Tribe__Settings::instance()->get_parent_slug();
+
+ $this->admin_page = add_submenu_page(
+ $where,
+ $page_title,
+ $menu_title,
+ $capability,
+ static::MENU_SLUG,
+ [
+ $this,
+ 'do_menu_page',
+ ]
+ );
+ }
+
+ /**
+ * Gets the required capability for the troubleshooting page.
+ *
+ * @since 4.14.2
+ *
+ * @return string Which capability we required for the troubleshooting page.
+ */
+ public function get_required_capability() {
+ /**
+ * Allows third party filtering of capability required to see the Troubleshooting page.
+ *
+ * @since 4.14.2
+ *
+ * @param string $capability Which capability we are using as the one required for the
+ * troubleshooting page.
+ * @param static $troubleshooting The current instance of the class that handles this page.
+ */
+ $capability = apply_filters( 'tec_troubleshooting_capability', 'install_plugins', $this );
+ return $capability;
+ }
+
+ /**
+ * Hooked to admin_body_class to add a class for troubleshooting page.
+ *
+ * @since 4.15.0
+ *
+ * @param string $classes a space separated string of classes to be added to body.
+ *
+ * @return string $classes a space separated string of classes to be added to body.
+ */
+ public function admin_body_class( $classes ) {
+ if ( ! $this->is_current_page() ) {
+ return $classes;
+ }
+
+ $classes .= ' tec-troubleshooting';
+ return $classes;
+ }
+
+ /**
+ * Adds the troubleshooting menu to the the WP admin bar under events.
+ *
+ * @since 4.14.2
+ *
+ */
+ public function add_toolbar_item() {
+ $capability = $this->get_required_capability();
+
+ if ( ! current_user_can( $capability ) ) {
+ return;
+ }
+
+ global $wp_admin_bar;
+
+ $wp_admin_bar->add_menu( [
+ 'id' => 'tec-troubleshooting',
+ 'title' => esc_html__( 'Troubleshooting', 'tribe-common' ),
+ 'href' => Tribe__Settings::instance()->get_url( [ 'page' => static::MENU_SLUG ] ),
+ 'parent' => 'tribe-events-settings-group',
+ ] );
+ }
+
+ /**
+ * Checks if the current page is the troubleshooting page.
+ *
+ * @since 4.14.2
+ *
+ * @return boolean returns true if the current page is the troubleshooting page.
+ */
+ public function is_current_page() {
+ if ( ! Tribe__Settings::instance()->should_setup_pages() || ! did_action( 'admin_menu' ) ) {
+ return false;
+ }
+
+ if ( is_null( $this->admin_page ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ 'Function was called before it is possible to accurately determine what the current page is.',
+ '4.5.6'
+ );
+ return false;
+ }
+
+ global $current_screen;
+
+ $troubleshooting_pages = [
+ 'tribe_events_page_tec-troubleshooting',
+ 'tickets_page_tec-tickets-troubleshooting',
+ ];
+
+ return in_array( $current_screen->id, $troubleshooting_pages );
+ }
+
+ /**
+ * Renders the Troubleshooting page.
+ *
+ * @since 4.14.2
+ *
+ */
+ public function do_menu_page() {
+ tribe_asset_enqueue( 'tribe-admin-help-page' );
+ $main = Tribe__Main::instance();
+ include_once Tribe__Main::instance()->plugin_path . 'src/admin-views/troubleshooting.php';
+ }
+
+ /**
+ * This method checks if there are any active issues that need to be flagged.
+ *
+ * @since 4.14.2
+ *
+ * @return boolean returns true if there are any active issues.
+ */
+ public function is_any_issue_active() {
+ $issues = $this->get_issues_found();
+ $active_issues = wp_list_pluck( $issues, 'active' );
+ return in_array( true, $active_issues );
+ }
+
+ /**
+ * Checks if any active TEC plugins require an update.
+ *
+ * @since 4.14.2
+ *
+ * @return boolean returns true is any of the plugins requires an update.
+ */
+ public function is_any_tec_plugin_out_of_date() {
+ $current = get_site_transient( 'update_plugins' );
+ $plugins = [];
+ if ( defined( 'TRIBE_EVENTS_FILE' ) ) {
+ $plugins[] = TRIBE_EVENTS_FILE;
+ }
+ if ( defined( 'EVENTS_CALENDAR_PRO_FILE' ) ) {
+ $plugins[] = EVENTS_CALENDAR_PRO_FILE;
+ }
+ if ( defined( 'EVENT_TICKETS_PLUS_FILE' ) ) {
+ $plugins[] = EVENT_TICKETS_PLUS_FILE;
+ }
+ if ( defined( 'EVENTS_VIRTUAL_FILE' ) ) {
+ $plugins[] = EVENTS_VIRTUAL_FILE;
+ }
+ if ( defined( 'EVENT_TICKETS_MAIN_PLUGIN_FILE' ) ) {
+ $plugins[] = EVENT_TICKETS_MAIN_PLUGIN_FILE;
+ }
+ if ( defined( 'TRIBE_EVENTS_FILTERBAR_FILE' ) ) {
+ $plugins[] = TRIBE_EVENTS_FILTERBAR_FILE;
+ }
+ if ( defined( 'EVENTS_COMMUNITY_TICKETS_FILE' ) ) {
+ $plugins[] = EVENTS_COMMUNITY_TICKETS_FILE;
+ }
+ if ( defined( 'EVENTS_COMMUNITY_FILE' ) ) {
+ $plugins[] = EVENTS_COMMUNITY_FILE;
+ }
+ if ( defined( 'EVENTBRITE_PLUGIN_FILE' ) ) {
+ $plugins[] = EVENTBRITE_PLUGIN_FILE;
+ }
+ if ( defined( 'TRIBE_APM_FILE' ) ) {
+ $plugins[] = TRIBE_APM_FILE;
+ }
+ if ( defined( 'IMAGE_WIDGET_PLUS_DIR' ) ) {
+ $plugins[] = IMAGE_WIDGET_PLUS_DIR;
+ }
+ $plugins = array_map( static function( $file ) {
+ $file = \str_replace( WP_PLUGIN_DIR . '/', '', $file );
+ return $file;
+ }, $plugins );
+
+ foreach ( $plugins as $file ) {
+ if ( ! isset( $current->response[ $file ] ) ) {
+ continue;
+ }
+ $response = $current->response[ $file ];
+ if ( ! empty( $response->new_version ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if any of the issues defined are active.
+ *
+ * @since 4.14.2
+ *
+ * @param string $slug the slug of active issue.
+ *
+ * @return boolean returns a boolean value for each individual issue depending on whether it is active or not.
+ */
+ public function is_active_issue( $slug ) {
+ if ( 'timezone' === $slug ) {
+ return Timezones::is_utc_offset( Timezones::wp_timezone_string() );
+ }
+ if ( 'geolocation' === $slug && class_exists( 'Tribe__Events__Google__Maps_API_Key' ) ) {
+ $key = \tribe_get_option( 'google_maps_js_api_key', false );
+ return empty( $key ) || Tribe__Events__Google__Maps_API_Key::$default_api_key === $key ;
+ }
+ if ( 'out-of-date' === $slug ) {
+ return $this->is_any_tec_plugin_out_of_date();
+ }
+ return false;
+ }
+
+ /**
+ * Displays issues found in the UI.
+ *
+ * @since 4.14.2
+ *
+ * @return array of issues which are displayed on the troubleshooting page.
+ */
+ public function get_issues_found() {
+ $issues_found = apply_filters( 'tec_help_troubleshooting_issues_found', [
+ [
+ 'title' => __( 'Site time zone uses UTC', 'tribe-common' ),
+ 'description' => __( 'When using The Events Calendar, we highly recommend that you use a geographic timezone such as "America/Los_Angeles" and avoid using a UTC timezone offset such as “UTC+9”. Choosing a UTC timezone for your site or individual events may cause problems when importing events or with Daylight Saving Time. Go to your the General WordPress settings to adjust your site timezone.', 'tribe-common' ),
+ 'more_info' => 'http://evnt.is/1ad3',
+ 'resolve_text' => __( 'Adjust your timezone', 'tribe-common' ),
+ 'fix' => '/wp-admin/options-general.php',
+ 'active' => $this->is_active_issue( 'timezone' ),
+ ],
+ [
+ 'title' => __( 'Install max has been reached', 'tribe-common' ),
+ 'description' => __( 'License keys can only be used on a limited number of sites, which varies depending on your license level. You\'ll need to remove the license from one or more other site\'s in order to use it on this one.', 'tribe-common' ),
+ 'more_info' => 'https://evnt.is/1aqz',
+ 'resolve_text' => __( 'Manage your licenses', 'tribe-common' ),
+ 'fix' => 'https://evnt.is/1aq-',
+ 'active' => $this->is_active_issue( 'install-max' ),
+ ],
+ [
+ 'title' => __( 'Default Google Maps API key', 'tribe-common' ),
+ 'description' => __( 'The Events Calendar comes with an API key for basic maps functionality. If you’d like to use more advanced features like custom map pins, dynamic map loads, or Events Calendar Pro\'s Location Search and advanced Map View, you’ll need to get your own Google Maps API key and add it to Events > Settings > Integrations', 'tribe-common' ),
+ 'more_info' => 'https://evnt.is/1aqx',
+ 'resolve_text' => __( 'Enter a custom API key', 'tribe-common' ),
+ 'fix' => '/wp-admin/edit.php?page=tec-events-settings&tab=addons&post_type=tribe_events#tribe-field-google_maps_js_api_key',
+ 'active' => $this->is_active_issue( 'geolocation' ),
+ ],
+ [
+ 'title' => __( 'Plugin(s) are out of date', 'tribe-common' ),
+ 'description' => __( 'It\'s important to use the most recent versions of our plugins so that you have access to the latest features, bug fixes, and security updates. Plugin functionality can be comprimised if your site is running outdated or mis-matched versions.', 'tribe-common' ),
+ 'more_info' => 'https://evnt.is/1aqy',
+ 'resolve_text' => __( 'Check for updates', 'tribe-common' ),
+ 'fix' => '/wp-admin/update-core.php',
+ 'active' => $this->is_active_issue( 'out-of-date' ),
+ ],
+ ] );
+
+ return $issues_found;
+ }
+
+ /**
+ * Defines common troubleshooting issues and displays them in the UI.
+ *
+ * @since 4.14.2
+ *
+ * @return array of common issues which are displayed on the troubleshooting page.
+ */
+ public function get_common_issues() {
+ $common_issues = apply_filters( 'tec_help_troubleshooting_issues', [
+ [
+ 'issue' => __( 'Common Error Messages', 'tribe-common' ),
+ 'solution' => __( 'Here’s an overview of %s and what they mean.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1as0',
+ 'link_label' => 'common error messages',
+ ],
+ [
+ 'issue' => __( 'My calendar doesn’t look right.', 'tribe-common' ),
+ 'solution' => __( 'This can happen when other plugins try to improve performance. %s.' ),
+ 'link' => 'https://theeventscalendar.com/knowledgebase/k/troubleshooting-the-most-common-installation-issues/#layout-issue',
+ 'link_label' => 'More info',
+ ],
+ [
+ 'issue' => __( 'I installed the calendar and it crashed my site.', 'tribe-common' ),
+ 'solution' => __( '%s and other common installation issues.', 'tribe-common' ),
+ 'link' => 'https://theeventscalendar.com/knowledgebase/k/troubleshooting-the-most-common-installation-issues/#fatal-errors',
+ 'link_label' => 'Find solutions to this',
+ ],
+ [
+ 'issue' => __( 'I keep getting “Page Not Found” on events.', 'tribe-common' ),
+ 'solution' => __( 'There are a few %s to resolve and prevent 404 errors.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1as2',
+ 'link_label' => 'things you can do',
+ ],
+ ] );
+
+ return $common_issues;
+ }
+
+ /**
+ * Fired to display notices in the admin pages where the method is called.
+ *
+ * @since 4.14.2
+ *
+ * @param string $page the page which the action is being applied.
+ *
+ */
+ public function admin_notice( $page ) {
+ do_action( 'tec_admin_notice_area', $page );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/Main.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/Main.php
new file mode 100644
index 000000000..5f220d0e5
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/Main.php
@@ -0,0 +1,97 @@
+template = new \Tribe__Template();
+ $this->template->set_template_origin( \Tribe__Main::instance() );
+ $this->template->set_template_folder( 'src/admin-views/notices/upsell' );
+ $this->template->set_template_context_extract( true );
+ $this->template->set_template_folder_lookup( false );
+ }
+
+ return $this->template;
+ }
+
+ /**
+ * Checks if upsell should be rendered.
+ *
+ * @since 4.14.17
+ *
+ * @return boolean
+ */
+ private function should_render() {
+ if ( function_exists( 'tec_should_hide_upsell' ) ) {
+ return ! tec_should_hide_upsell();
+ }
+ if ( defined( 'TRIBE_HIDE_UPSELL' ) ) {
+ return ! tribe_is_truthy( TRIBE_HIDE_UPSELL );
+ }
+ return true;
+ }
+
+ /**
+ * Render upsell notice.
+ *
+ * @since 4.14.17
+ *
+ * @param array $args Array of arguments that will ultimately be sent to the template.
+ * @param bool $echo Whether or not to echo the HTML. Defaults to true.
+ *
+ * @return string HTML of upsell notice.
+ */
+ public function render( $args, $echo = true ) {
+ // Check if upsell should be rendered.
+ if ( ! $this->should_render() ) {
+ return;
+ }
+
+ // Default args for the container.
+ $args = wp_parse_args( $args, [
+ 'classes' => [],
+ 'text' => '',
+ 'link_target' => '_blank',
+ 'icon_url' => tribe_resource_url( 'images/icons/circle-bolt.svg', false, null, \Tribe__Main::instance() ),
+ 'link' => [],
+ ] );
+
+ // Default args for the link.
+ $args['link'] = wp_parse_args( $args['link'], [
+ 'classes' => [],
+ 'text' => '',
+ 'url' => '',
+ 'target' => '_blank',
+ 'rel' => 'noopener noreferrer',
+ ] );
+
+ $template = $this->get_template();
+ return $template->template( 'main', $args, $echo );
+ }
+}
\ No newline at end of file
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/README.md b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/README.md
new file mode 100644
index 000000000..aa0b1012e
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/README.md
@@ -0,0 +1,65 @@
+# Upsell Notices
+
+To add an upsell notice, use the following code:
+
+```
+tribe( \Tribe\Admin\Upsell_Notice\Main::class )->render( [
+ 'text' => 'Text to explain what you are promoting.',
+ 'link' => [
+ 'text' => 'Text for the link.',
+ 'url' => 'https://url.com/to/more/info',
+ ],
+] );
+```
+
+## Customizing the notice container
+
+There are a couple of classes you can add that will display the upsell notice with different styles.
+
+- `.tec-admin__upsell--rounded-corners` - Adds a rounded-corner, light gray background around the entire notice.
+- `.tec-admin__upsell--rounded-corners-text` - Adds a rounded-corner, light gray background around the notice text, only.
+
+Example:
+```
+tribe( \Tribe\Admin\Upsell_Notice\Main::class )->render( [
+ 'classes' => [
+ 'tec-admin__upsell--rounded-corners'
+ ],
+ 'text' => 'Text to explain what you are promoting.',
+ 'link' => [
+ 'text' => 'Text for the link.',
+ 'url' => 'https://url.com/to/more/info',
+ ],
+] );
+```
+
+## Customizing the notice link
+
+Likewise, you can also add these classes to the link array to change the appearance.
+
+- `.tec-admin__upsell-link--dark` - Changes the color to a dark color, instead of the default blue.
+- `.tec-admin__upsell-link--underlined` - Adds an underline to the link text.
+
+You can also change the following attributes of the link:
+
+- `target` - Default is '_blank'.
+- `rel` - Default is 'nofollow noreferrer'.
+
+Example:
+```
+tribe( \Tribe\Admin\Upsell_Notice\Main::class )->render( [
+ 'classes' => [
+ 'tec-admin__upsell--rounded-corners-text'
+ ],
+ 'text' => 'Text to explain what you are promoting.',
+ 'link' => [
+ 'classes' => [
+ 'tec-admin__upsell-link--dark',
+ 'tec-admin__upsell-link--underlined',
+ ],
+ 'text' => 'Text for the link.',
+ 'url' => 'https://url.com/to/more/info',
+ 'target' => '_parent'
+ ],
+] );
+```
\ No newline at end of file
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Wysiwyg.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Wysiwyg.php
new file mode 100644
index 000000000..f2395e0cc
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Wysiwyg.php
@@ -0,0 +1,143 @@
+name = $name;
+ $this->value = $value;
+ $default_args = [
+ 'teeny' => true,
+ 'wpautop' => true,
+ 'textarea_name' => $name,
+ ];
+ $this->args = wp_parse_args( $args, $default_args );
+ }
+
+ /**
+ * Filters editor buttons.
+ *
+ * @since 5.0.12
+ *
+ * @param array $buttons Array of buttons to include.
+ *
+ * @return array Filtered array of buttons.
+ */
+ public function filter_buttons( $buttons ) {
+ if (
+ empty( $this->args )
+ || ! isset( $this->args['buttons'] )
+ || empty( $this->args['buttons'] )
+ ) {
+ return $buttons;
+ }
+
+ return $this->args['buttons'];
+ }
+
+ /**
+ * Filter 2nd row of buttons.
+ *
+ * @since 5.0.12
+ *
+ * @param array $buttons Array of buttons to include.
+ *
+ * @return array Filtered array of buttons.
+ */
+ public function maybe_filter_buttons_2( $buttons ) {
+ if (
+ empty( $this->args ) ||
+ ! isset( $this->args['buttons_2'] ) ||
+ empty( $this->args['buttons_2'] )
+ ) {
+ return $buttons;
+ }
+
+ return $this->args['buttons_2'];
+ }
+
+ /**
+ * Get HTML of editor.
+ *
+ * @since 5.0.12
+ *
+ * @return string HTML of editor
+ */
+ public function get_html() {
+ // Add button filters.
+ add_filter( 'teeny_mce_buttons', [ $this, 'filter_buttons' ] );
+ add_filter( 'tiny_mce_buttons', [ $this, 'filter_buttons' ] );
+ add_filter( 'mce_buttons', [ $this, 'filter_buttons' ] );
+ add_filter( 'mce_buttons_2', [ $this, 'maybe_filter_buttons_2' ] );
+
+ // Get HTML of editor.
+ ob_start();
+ wp_editor( html_entity_decode( ( $this->value ) ), sanitize_html_class( $this->name ), $this->args );
+ $html = ob_get_clean();
+
+ // Remove button filters.
+ remove_filter( 'teeny_mce_buttons', [ $this, 'filter_buttons' ] );
+ remove_filter( 'tiny_mce_buttons', [ $this, 'filter_buttons' ] );
+ remove_filter( 'mce_buttons', [ $this, 'filter_buttons' ] );
+ remove_filter( 'mce_buttons_2', [ $this, 'maybe_filter_buttons_2' ] );
+
+ return $html;
+ }
+
+ /**
+ * Renders editor HTML.
+ *
+ * @since 5.0.12
+ *
+ * @return void
+ */
+ public function render_html() {
+ echo $this->get_html();
+ }
+
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Dropdown.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Dropdown.php
new file mode 100644
index 000000000..071d977cf
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Dropdown.php
@@ -0,0 +1,340 @@
+ $search Search string from Select2
+ * @param int $page When we deal with pagination
+ * @param array $args Which arguments we got from the Template
+ * @param string $source What source it is
+ *
+ * @return array
+ */
+ public function search_terms( $search, $page, $args, $source ) {
+ $data = [];
+
+ if ( empty( $args['taxonomy'] ) ) {
+ $this->error( esc_attr__( 'Cannot look for Terms without a taxonomy', 'tribe-common' ) );
+ }
+
+ // We always want all the fields so we overwrite it
+ $args['fields'] = isset( $args['fields'] ) ? $args['fields'] : 'all';
+ $args['hide_empty'] = isset( $args['hide_empty'] ) ? $args['hide_empty'] : false;
+
+ if ( ! empty( $search ) ) {
+ if ( ! is_array( $search ) ) {
+ // For older pieces that still use Select2 format.
+ $args['search'] = $search;
+ } else {
+ // Newer SelectWoo uses a new search format.
+ $args['search'] = $search['term'];
+ }
+ }
+
+ // On versions older than 4.5 taxonomy goes as an Param
+ if ( version_compare( $GLOBALS['wp_version'], '4.5', '<' ) ) {
+ $terms = get_terms( $args['taxonomy'], $args );
+ } else {
+ $terms = get_terms( $args );
+ }
+
+ $results = [];
+
+ // Respect the parent/child_of argument if set
+ $parent = ! empty( $args['child_of'] ) ? (int) $args['child_of'] : 0;
+ $parent = ! empty( $args['parent'] ) ? (int) $args['parent'] : $parent;
+
+ if ( empty( $args['search'] ) ) {
+ $this->sort_terms_hierarchically( $terms, $results, $parent );
+ $results = $this->convert_children_to_array( $results );
+ } else {
+ foreach ( $terms as $term ) {
+ // Prep for Select2
+ $term->id = $term->term_id;
+ $term->text = $term->name;
+ $term->breadcrumbs = [];
+
+ if ( 0 !== (int) $term->parent ) {
+ $ancestors = get_ancestors( $term->id, $term->taxonomy );
+ $ancestors = array_reverse( $ancestors );
+ foreach ( $ancestors as $ancestor ) {
+ $ancestor = get_term( $ancestor );
+ $term->breadcrumbs[] = $ancestor->name;
+ }
+ }
+
+ $results[] = $term;
+ }
+ }
+
+ foreach ( $results as $result ) {
+ $result->text = wp_specialchars_decode( wp_kses( $result->text, [] ) );
+ }
+
+ $data['results'] = $results;
+ $data['taxonomies'] = get_taxonomies();
+
+ return $data;
+ }
+
+ /**
+ * Search for Posts using Select2
+ *
+ * @since 4.12.17
+ *
+ * @param string|array $search Search string from Select2.
+ * @param int $page Page we want when we're dealing with pagination.
+ * @param array $args Arguments to pass to the query.
+ * @param string|int $selected Selected item ID.
+ *
+ * @return array An Array of results.
+ */
+ public function search_posts( $search, $page = 1, $args = [], $selected = null ) {
+ if ( ! empty( $search ) ) {
+ if ( is_array( $search ) ) {
+ // Newer SelectWoo uses a new search format.
+ $args['s'] = $search['term']; // post?
+ } else {
+ // For older pieces that still use Select2 format.
+ $args['s'] = $search;
+ }
+ }
+
+ $args['paged'] = $page;
+ $args['update_post_meta_cache'] = false;
+ $args['update_post_term_cache'] = false;
+
+ $results = new WP_Query( $args );
+ $has_pagination = $results->post_count < $results->found_posts;
+
+ return $this->format_posts_for_dropdown( $results->posts, $selected, $has_pagination );
+ }
+
+ /**
+ * Formats a given array of posts to be displayed into the Dropdown.js module with SelectWoo.
+ *
+ * @since 4.12.17
+ *
+ * @param array $posts
+ * @param null|int $selected
+ * @param boolean $pagination
+ *
+ * @return array
+ */
+ public function format_posts_for_dropdown( array $posts, $selected = null, $pagination = false ) {
+ $data = [
+ 'posts' => [],
+ 'pagination' => $pagination,
+ ];
+
+ // Skip when we don't have posts
+ if ( empty( $posts ) ) {
+ return $data;
+ }
+
+ foreach ( $posts as $post ) {
+ if ( ! $post instanceof \WP_Post ) {
+ $post = get_post( $post );
+ }
+
+ // Skip non WP Post Objects.
+ if ( ! $post instanceof \WP_Post ) {
+ continue;
+ }
+
+ // Prep for Select2.
+ $data['posts'][] = [
+ 'id' => $post->ID,
+ 'text' => ! empty( $post->post_title_formatted ) ? $post->post_title_formatted : $post->post_title,
+ 'selected' => ! empty( $selected ) && (int) $post->ID === (int) $selected,
+ ];
+ }
+
+ return $data;
+ }
+
+ /**
+ * Sorts all the Terms for Select2 hierarchically.
+ *
+ * @since 4.6
+ *
+ * @param array &$terms Array of Terms from `get_terms`.
+ * @param array &$into Variable where we will store the.
+ * @param integer $parent Used for the recursion.
+ *
+ * @return array
+ */
+ public function sort_terms_hierarchically( &$terms, &$into, $parent = 0 ) {
+ foreach ( $terms as $i => $term ) {
+ if ( $term->parent === $parent ) {
+ // Prep for Select2
+ $term->id = $term->term_id;
+ $term->text = $term->name;
+
+ $into[ $term->term_id ] = $term;
+ unset( $terms[ $i ] );
+ }
+ }
+
+ foreach ( $into as $term ) {
+ $term->children = [];
+ $this->sort_terms_hierarchically( $terms, $term->children, $term->term_id );
+ }
+ }
+
+ /**
+ * Makes sure we have arrays for the JS data for Select2
+ *
+ * @since 4.6
+ *
+ * @param object|array $results The Select2 results
+ *
+ * @return array
+ */
+ public function convert_children_to_array( $results ) {
+ if ( isset( $results->children ) ) {
+ $results->children = $this->convert_children_to_array( $results->children );
+ if ( empty( $results->children ) ) {
+ unset( $results->children );
+ }
+ } else {
+ foreach ( $results as $key => $item ) {
+ $item = $this->convert_children_to_array( $item );
+ }
+ }
+
+ if ( empty( $results ) ) {
+ return [];
+ }
+
+ return array_values( (array) $results );
+ }
+
+ /**
+ * Parses the Params coming from Select2 Search box
+ *
+ * @since 4.6
+ *
+ * @param array $params Params to overwrite the defaults
+ *
+ * @return object
+ */
+ public function parse_params( $params ) {
+ $defaults = [
+ 'search' => null,
+ 'page' => 0,
+ 'args' => [],
+ 'source' => null,
+ ];
+
+ $arguments = wp_parse_args( $params, $defaults );
+
+ // Return Object just for the sake of making it simpler to read
+ return (object) $arguments;
+ }
+
+ /**
+ * The default Method that will route all the AJAX calls from our Dropdown AJAX requests
+ * It is like a Catch All on `wp_ajax_tribe_dropdown` and `wp_ajax_nopriv_tribe_dropdown`
+ *
+ * @since 4.6
+ *
+ * @return void
+ */
+ public function route() {
+ // Push all POST params into a Default set of data
+ $args = $this->parse_params( empty( $_POST ) ? [] : $_POST );
+
+ if ( empty( $args->source ) ) {
+ $this->error( esc_attr__( 'Missing data source for this dropdown', 'tribe-common' ) );
+ }
+
+ // Define a Filter to allow external calls to our Select2 Dropdowns.
+ $filter = sanitize_key( 'tribe_dropdown_' . $args->source );
+ if ( has_filter( $filter ) ) {
+ $data = apply_filters( $filter, [], $args->search, $args->page, $args->args, $args->source );
+ } else {
+ $data = call_user_func_array( [ $this, $args->source ], array_values( (array) $args ) );
+ }
+
+ // If we've got a empty dataset we return an error.
+ if ( empty( $data ) ) {
+ $this->error( esc_attr__( 'Empty data set for this dropdown', 'tribe-common' ) );
+ } else {
+ $this->success( $data );
+ }
+ }
+
+ /**
+ * Prints a success message and ensures that we don't hit bugs on Select2
+ *
+ * @since 4.6
+ *
+ * @param array $data
+ *
+ * @return void
+ */
+ private function success( $data ) {
+ // We need a Results item for Select2 Work
+ if ( ! isset( $data['results'] ) ) {
+ $data['results'] = [];
+ }
+
+ wp_send_json_success( $data );
+ }
+
+ /**
+ * Prints an error message and ensures that we don't hit bugs on Select2
+ *
+ * @since 4.6
+ *
+ * @param string $message
+ *
+ * @return void
+ */
+ private function error( $message ) {
+ $data = [
+ 'message' => $message,
+ 'results' => [],
+ ];
+
+ wp_send_json_error( $data );
+ }
+
+ /**
+ * Avoid throwing fatals or notices on sources that are invalid
+ *
+ * @since 4.6
+ *
+ * @param string $name
+ * @param mixed $arguments
+ *
+ * @return void
+ */
+ public function __call( $name, $arguments ) {
+ $message = __( 'The "%s" source is invalid and cannot be reached on "%s" instance.', 'tribe-common' );
+
+ return $this->error( sprintf( $message, $name, __CLASS__ ) );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Operations.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Operations.php
new file mode 100644
index 000000000..09b54ae99
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Operations.php
@@ -0,0 +1,22 @@
+register_assets();
+ }
+
+ /**
+ * Adds the page to the admin menu
+ */
+ public function add_menu_page() {
+ if ( ! Tribe__Settings::instance()->should_setup_pages() ) {
+ return;
+ }
+
+ $page_title = esc_html__( 'Event Add-Ons', 'tribe-common' );
+ $menu_title = esc_html__( 'Event Add-Ons', 'tribe-common' );
+ $capability = apply_filters( 'tribe_events_addon_page_capability', 'install_plugins' );
+
+ $where = Tribe__Settings::instance()->get_parent_slug();
+
+ $this->admin_page = add_submenu_page(
+ $where,
+ $page_title,
+ $menu_title,
+ $capability,
+ self::MENU_SLUG,
+ [
+ $this,
+ 'do_menu_page',
+ ]
+ );
+ }
+
+ /**
+ * Adds a link to the shop app to the WP admin bar
+ */
+ public function add_toolbar_item() {
+
+ $capability = apply_filters( 'tribe_events_addon_page_capability', 'install_plugins' );
+
+ // prevent users who cannot install plugins from seeing addons link
+ if ( current_user_can( $capability ) ) {
+ global $wp_admin_bar;
+
+ $wp_admin_bar->add_menu( [
+ 'id' => 'tribe-events-app-shop',
+ 'title' => esc_html__( 'Event Add-Ons', 'tribe-common' ),
+ 'href' => Tribe__Settings::instance()->get_url( [ 'page' => self::MENU_SLUG ] ),
+ 'parent' => 'tribe-events-settings-group',
+ ] );
+ }
+ }
+
+ /**
+ * Registers the plugin assets
+ */
+ protected function register_assets() {
+ tribe_assets(
+ Tribe__Main::instance(),
+ [
+ [ 'tribe-app-shop-css', 'app-shop.css' ],
+ [ 'tribe-app-shop-js', 'app-shop.js', [ 'jquery' ] ],
+ ],
+ 'admin_enqueue_scripts',
+ [
+ 'conditionals' => [ $this, 'is_current_page' ],
+ ]
+ );
+ }
+
+ /**
+ * Checks if the current page is the app shop
+ *
+ * @since 4.5.7
+ *
+ * @return bool
+ */
+ public function is_current_page() {
+ if ( ! Tribe__Settings::instance()->should_setup_pages() || ! did_action( 'admin_menu' ) ) {
+ return false;
+ }
+
+ if ( is_null( $this->admin_page ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ 'Function was called before it is possible to accurately determine what the current page is.',
+ '4.5.6'
+ );
+ return false;
+ }
+
+ return Tribe__Admin__Helpers::instance()->is_screen( $this->admin_page );
+ }
+
+ /**
+ * Renders the Shop App page
+ */
+ public function do_menu_page() {
+ $main = Tribe__Main::instance();
+ $products = $this->get_all_products();
+ $bundles = $this->get_bundles();
+ $extensions = $this->get_extensions();
+ $stellar_brands = $this->get_stellar_brands();
+ include_once Tribe__Main::instance()->plugin_path . 'src/admin-views/app-shop.php';
+ }
+
+ /**
+ * Gets all products from the API
+ *
+ * @return array|WP_Error
+ */
+ private function get_all_products() {
+ $all_products = tribe( 'plugins.api' )->get_products();
+
+ $products = [
+ 'the-events-calendar' => (object) $all_products['the-events-calendar'],
+ 'events-calendar-pro' => (object) $all_products['events-calendar-pro'],
+ 'events-virtual' => (object) $all_products['events-virtual'],
+ 'event-aggregator' => (object) $all_products['event-aggregator'],
+ 'event-automator' => (object) $all_products['event-automator'],
+ 'event-tickets' => (object) $all_products['event-tickets'],
+ 'event-tickets-plus' => (object) $all_products['event-tickets-plus'],
+ 'promoter' => (object) $all_products['promoter'],
+ 'tribe-filterbar' => (object) $all_products['tribe-filterbar'],
+ 'events-community' => (object) $all_products['events-community'],
+ 'events-community-tickets' => (object) $all_products['events-community-tickets'],
+ 'tribe-eventbrite' => (object) $all_products['tribe-eventbrite'],
+ 'image-widget-plus' => (object) $all_products['image-widget-plus'],
+ ];
+
+ return $products;
+ }
+
+ /**
+ * Gets product bundles
+ *
+ * @return array|WP_Error
+ */
+ private function get_bundles() {
+ $bundles = [
+ (object) [
+ 'title' => __( 'Events Marketing Bundle', 'tribe-common' ),
+ 'logo' => 'images/logo/bundle-event-marketing.svg',
+ 'link' => 'https://evnt.is/1aj3',
+ 'discount' => __( 'Save over 20%', 'tribe-common' ),
+ 'description' => __( 'Ticket sales, attendee management, and email marketing for your events', 'tribe-common' ),
+ 'includes' => [
+ 'events-calendar-pro',
+ 'event-tickets-plus',
+ 'promoter',
+ ],
+ ],
+ (object) [
+ 'title' => __( 'Event Importer Bundle', 'tribe-common' ),
+ 'logo' => 'images/logo/bundle-event-importer.svg',
+ 'link' => 'https://evnt.is/1aj2',
+ 'discount' => __( 'Save over 25%', 'tribe-common' ),
+ 'description' => __( 'Fill your calendar with events from across the web, including Google Calendar, Meetup, and more.', 'tribe-common' ),
+ 'includes' => [
+ 'events-calendar-pro',
+ 'tribe-filterbar',
+ 'event-aggregator'
+ ],
+ ],
+ (object) [
+ 'title' => __( 'Virtual Events Marketing Bundle', 'tribe-common' ),
+ 'logo' => 'images/logo/bundle-virtual-events.svg',
+ 'link' => 'http://evnt.is/ve-bundle',
+ 'discount' => __( 'Save over 20%', 'tribe-common' ),
+ 'description' => __( 'Streamline your online events and increase revenue.', 'tribe-common' ),
+ 'includes' => [
+ 'events-calendar-pro',
+ 'event-tickets-plus',
+ 'events-virtual',
+ 'promoter',
+ ],
+ 'features' => [
+ __( 'Sell tickets and earn revenue for online events', 'tribe-common' ),
+ __( 'Zoom integration', 'tribe-common' ),
+ __( 'Automated emails optimized for virtual events', 'tribe-common' ),
+ __( 'Add recurring events', 'tribe-common' ),
+ ],
+ ],
+ (object) [
+ 'title' => __( 'Community Manager Bundle', 'tribe-common' ),
+ 'logo' => 'images/logo/bundle-community-manager.svg',
+ 'link' => 'https://evnt.is/1aj4',
+ 'discount' => __( 'Save over 20%', 'tribe-common' ), /* code review: fix this */
+ 'description' => __( 'Handle event submissions with ticket sales and everything you need to build a robust community.', 'tribe-common' ),
+ 'includes' => [
+ 'event-tickets-plus',
+ 'events-community',
+ 'events-community-tickets',
+ 'tribe-filterbar',
+ ],
+ ],
+ (object) [
+ 'title' => __( 'Ultimate Bundle', 'tribe-common' ),
+ 'logo' => 'images/logo/bundle-ultimate.svg',
+ 'link' => 'https://evnt.is/1aj5',
+ 'discount' => __( 'Save over 20%', 'tribe-common' ), /* code review: fix this */
+ 'description' => __( 'All of our premium events management plugins at a deep discount.', 'tribe-common' ),
+ 'includes' => [
+ 'events-calendar-pro',
+ 'event-tickets-plus',
+ //'events-virtual', // not yet added to the bundle
+ 'events-community',
+ 'events-community-tickets',
+ 'tribe-filterbar',
+ 'event-aggregator',
+ 'tribe-eventbrite',
+ //'promoter', // not yet added to the bundle
+ ],
+ ],
+
+ ];
+
+ return $bundles;
+ }
+
+ /**
+ * Gets product extensions
+ *
+ * @return array|WP_Error
+ */
+ private function get_extensions() {
+ $extensions = [
+ (object) [
+ 'title' => __( 'Website URL CTA', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1aj6',
+ 'image' => 'images/shop/extension-web-url-cta.jpg',
+ 'description' => __( 'Create a strong call-to-action for attendees to "Join Webinar" instead of only sharing a website address.', 'tribe-common' ),
+ ],
+ (object) [
+ 'title' => __( 'Link Directly to Webinar', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1aj7',
+ 'image' => 'images/shop/extension-link-to-webinar.jpg',
+ 'description' => __( 'When users click on the event title, they’ll be taken right to the source of your event, offering a direct route to join.', 'tribe-common' ),
+ ],
+ (object) [
+ 'title' => __( 'Events Happening Now', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1aj8',
+ 'image' => 'images/shop/extension-events-happening-now.jpg',
+ 'description' => __( 'Use this shortcode to display events that are currently in progress, like webinars and livestreams.', 'tribe-common' ),
+ ],
+ (object) [
+ 'title' => __( 'Custom Venue Links', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1aj9',
+ 'image' => 'images/shop/extension-custom-venue-links.jpg',
+ 'description' => __( 'Turn the venue name for your event into a clickable URL — a great way to link directly to a venue’s website or a virtual meeting.', 'tribe-common' ),
+ ],
+ (object) [
+ 'title' => __( 'Adjust Label', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1aja',
+ 'image' => 'images/shop/extension-change-label.jpg',
+ 'description' => __( 'Change "Events" to "Webinars," or "Venues" to "Livestream," or "Organizers" to "Hosts." Tailor your calendar for virtual events and meetings.', 'tribe-common' ),
+ ],
+ (object) [
+ 'title' => __( 'Reach Attendees', 'tribe-common' ),
+ 'link' => 'https://evnt.is/1ajc',
+ 'image' => 'images/shop/extension-advanced-options.jpg',
+ 'description' => __( 'From registration to attendance history, view every step of the event lifecycle with this HubSpot integration.', 'tribe-common' ),
+ ],
+ ];
+
+ return $extensions;
+ }
+
+ /**
+ * Gets Stellar brands
+ *
+ * @return array|WP_Error
+ */
+ private function get_stellar_brands() {
+ $stellar_brands = [
+ (object) [
+ 'image' => 'images/shop/stellar-learndash-cta.jpg',
+ 'logo' => 'images/shop/stellar-learndash-logo.png',
+ 'title' => __( 'The online course platform created by e-learning experts.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/learndash',
+ 'linktext' => __( 'Add Courses', 'tribe-common' ),
+ 'description' => __( 'Trusted to power learning programs for major universities, startups, entrepreneurs, and bloggers worldwide.', 'tribe-common' ),
+ ],
+ (object) [
+ 'image' => 'images/shop/stellar-ithemes-cta.jpg',
+ 'logo' => 'images/shop/stellar-ithemes-logo.png',
+ 'title' => __( 'Foundational favorites: iThemes Security and Developer Toolkit.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/ithemes',
+ 'linktext' => __( 'Add Security', 'tribe-common' ),
+ 'description' => __( 'iThemes Security, the WordPress security plugin that’s easy to use. Built with performance in mind.', 'tribe-common' ),
+ ],
+ (object) [
+ 'image' => 'images/shop/stellar-rcp-cta.jpg',
+ 'logo' => 'images/shop/stellar-rcp-logo.png',
+ 'title' => __( 'Built with developers in mind.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/rcp',
+ 'linktext' => __( 'Add Content Restriction', 'tribe-common' ),
+ 'description' => __( 'Restrict Content Pro is flexible, easy to extend, and chock full of action hooks and filters, making it easy to modify and tweak to your specific needs.', 'tribe-common' ),
+ ],
+ (object) [
+ 'image' => 'images/shop/stellar-kadence-cta.jpg',
+ 'logo' => 'images/shop/stellar-kadence-logo.png',
+ 'title' => __( 'Build better WordPress websites with Kadence.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/kadencewp',
+ 'linktext' => __( 'Add Starter Templates', 'tribe-common' ),
+ 'description' => __( 'Kadence lets you unlock your creativity in the WordPress Block Editor with expertly designed blocks, a robust theme, and a massive library of starter templates.', 'tribe-common' ),
+ ],
+ (object) [
+ 'image' => 'images/shop/stellar-iconic-cta.jpg',
+ 'logo' => 'images/shop/stellar-iconic-logo.png',
+ 'title' => __( 'Sales-boosting WooCommerce plugins.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/iconic',
+ 'linktext' => __( 'Add Commerce Tools', 'tribe-common' ),
+ 'description' => __( 'Easy-to-use WooCommerce plugins work perfectly together, with any theme. Create a fast and profitable eCommerce store without any technical knowledge.
+ ', 'tribe-common' ),
+ ],
+ (object) [
+ 'image' => 'images/shop/stellar-give-cta.jpg',
+ 'logo' => 'images/shop/stellar-give-logo.png',
+ 'title' => __( 'The best WordPress donation plugin.', 'tribe-common' ),
+ 'link' => 'https://evnt.is/givewp',
+ 'linktext' => __( 'Add Donations', 'tribe-common' ),
+ 'description' => __( 'GiveWP makes it easy to raise money online with donation forms, donor databases, and fundraising reporting.', 'tribe-common' ),
+ ],
+ ];
+
+ return $stellar_brands;
+ }
+
+ /**
+ * Static Singleton Factory Method
+ *
+ * @return Tribe__App_Shop
+ */
+ public static function instance() {
+ if ( ! isset( self::$instance ) ) {
+ $className = __CLASS__;
+ self::$instance = new $className;
+ }
+
+ return self::$instance;
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Data.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Data.php
new file mode 100644
index 000000000..2dd995734
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Data.php
@@ -0,0 +1,67 @@
+objects[ $object_name ] = $data;
+ }
+
+ /**
+ * Outputs the
+ * @internal
+ */
+ public function render_json() {
+ if ( empty( $this->objects ) ) {
+ return;
+ }
+
+ echo '';
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Factory.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Factory.php
new file mode 100644
index 000000000..fee20c577
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Factory.php
@@ -0,0 +1,56 @@
+get_asset_class_name( $name );
+
+ // `Jquery_Resize` to `Tribe__Asset__Jquery_Resize`
+ $full_class_name = $this->get_asset_full_class_name( $class_name );
+
+ return class_exists( $full_class_name ) ? new $full_class_name() : false;
+ }
+
+ protected function get_asset_class_name( $name ) {
+ // `jquery-resize` to `Jquery_Resize`
+ $class_name = str_replace( ' ', '_', ucwords( str_replace( '-', ' ', $name ) ) );
+
+ return $class_name;
+ }
+
+ /**
+ * @param string $class_name
+ *
+ * @return string
+ */
+ private function get_asset_full_class_name( $class_name ) {
+ // `Jquery_Resize` to `Tribe__Asset__Jquery_Resize`
+ $full_class_name = $this->get_asset_class_name_prefix() . $class_name;
+
+ return $full_class_name;
+ }
+
+ /**
+ * @return string
+ */
+ protected function get_asset_class_name_prefix() {
+ return 'Tribe__Asset__';
+ }
+
+ /**
+ * @return Tribe__Asset__Factory
+ */
+ public static function instance() {
+ static $instance;
+
+ if ( ! $instance ) {
+ $instance = new self;
+ }
+
+ return $instance;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Assets.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Assets.php
new file mode 100644
index 000000000..a9cb962b8
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Assets.php
@@ -0,0 +1,949 @@
+get( $handle ) ) {
+ return (string) $tag;
+ }
+
+ // Bail when not dealing with JS assets.
+ if ( 'js' !== $asset->type ) {
+ return (string) $tag;
+ }
+
+ // Only go forward if there is any print before or after.
+ if ( empty( $asset->print_before ) && empty( $asset->print_after ) ) {
+ return (string) $tag;
+ }
+
+ $before = '';
+ if ( ! empty( $asset->print_before ) ) {
+ $before = (string) ( is_callable( $asset->print_before ) ? call_user_func( $asset->print_before, $asset ) : $asset->print_before );
+ }
+
+ $after = '';
+ if ( ! empty( $asset->print_after ) ) {
+ $after = (string) ( is_callable( $asset->print_after ) ? call_user_func( $asset->print_after, $asset ) : $asset->print_after );
+ }
+
+ $tag = $before . (string) $tag . $after;
+
+ return $tag;
+ }
+
+ /**
+ * Handles adding localization data, when attached to `script_loader_tag` which allows dependencies to load in their
+ * localization data as well.
+ *
+ * @since 4.13.0
+ *
+ * @param string $tag Tag we are filtering.
+ * @param string $handle Which is the ID/Handle of the tag we are about to print.
+ *
+ * @return string Script tag with the localization variable HTML attached to it.
+ */
+ public function filter_add_localization_data( $tag, $handle ) {
+ // Only filter for own filters.
+ if ( ! $asset = $this->get( $handle ) ) {
+ return $tag;
+ }
+
+ // Bail when not dealing with JS assets.
+ if ( 'js' !== $asset->type ) {
+ return $tag;
+ }
+
+ // Only localize on JS and if we have data.
+ if ( empty( $asset->localize ) ) {
+ return $tag;
+ }
+
+ global $wp_scripts;
+
+ // Makes sure we have an Array of Localize data.
+ if ( is_object( $asset->localize ) ) {
+ $localization = [ $asset->localize ];
+ } else {
+ $localization = (array) $asset->localize;
+ }
+
+ /**
+ * Check to ensure we haven't already localized it before.
+ *
+ * @since 4.5.8
+ */
+ foreach ( $localization as $localize ) {
+ if ( in_array( $localize->name, $this->localized ) ) {
+ continue;
+ }
+
+ // If we have a Callable as the Localize data we execute it.
+ if ( is_callable( $localize->data ) ) {
+ $localize->data = call_user_func( $localize->data, $asset );
+ }
+
+ wp_localize_script( $asset->slug, $localize->name, $localize->data );
+
+ $this->localized[] = $localize->name;
+ }
+
+ // Fetch the HTML for all the localized data.
+ ob_start();
+ $wp_scripts->print_extra_script( $asset->slug, true );
+ $localization_html = ob_get_clean();
+
+ // After printing it remove data;|
+ $wp_scripts->add_data( $asset->slug, 'data', '' );
+
+ return $localization_html . $tag;
+ }
+
+ /**
+ * Filters the Script tags to attach Async and/or Defer based on the rules we set in our Asset class.
+ *
+ * @since 4.13.0
+ *
+ * @param string $tag Tag we are filtering.
+ * @param string $handle Which is the ID/Handle of the tag we are about to print.
+ *
+ * @return string Script tag with the defer and/or async attached.
+ */
+ public function filter_tag_async_defer( $tag, $handle ) {
+ // Only filter for our own filters.
+ if ( ! $asset = $this->get( $handle ) ) {
+ return $tag;
+ }
+
+ // Bail when not dealing with JS assets.
+ if ( 'js' !== $asset->type ) {
+ return $tag;
+ }
+
+ // When async and defer are false we bail with the tag.
+ if ( ! $asset->defer && ! $asset->async ) {
+ return $tag;
+ }
+
+ $tag_has_async = false !== strpos( $tag, ' async ' );
+ $tag_has_defer = false !== strpos( $tag, ' defer ' );
+ $replacement = '\n"
+ . $tag
+ . "\n";
+ }
+
+ return $tag;
+ }
+
+ /**
+ * After select2 is loaded to the FE we add one scripts after to prevent select2 from breaking.
+ *
+ * @since 4.13.2
+ * @since 4.14.18 Ensure we don't run this in the admin.
+ *
+ * @param string $tag The \n";
+
+ return $tag;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Autoloader.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Autoloader.php
new file mode 100644
index 000000000..90a01e027
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Autoloader.php
@@ -0,0 +1,297 @@
+register_prefix( 'Tribe__Admin__', $this_dir . '/src/Tribe/admin' );
+ * $autoloader->register_prefix( 'Tribe__Admin__', $this_dir . '/src/Tribe/another-dir' );
+ * $autoloader->register_prefix( 'Tribe__Utils__', $this_dir . '/src/Tribe/another-dir' );
+ *
+ * // register a direct class to path
+ * $autoloader->register_class( 'Tribe__Some_Class', $this_dir . '/some/path/to/Some_Class.php' );
+ *
+ * // register a fallback dir to be searched for the class before giving up
+ * $autoloader->add_fallback_dir( $this_dir . '/all-the-classes' );
+ *
+ * // calls `spl_autoload_register`
+ * $autoloader->register_autoloader();
+ *
+ * // class will be searched in the path
+ * // `/var/www/site/wp-content/plugins/the-events-calendar/src/Tribe/admin/Some_Class.php'
+ * // and
+ * // `/var/www/site/wp-content/plugins/the-events-calendar/src/Tribe/another-dir/Some_Class.php'
+ * $i = new Tribe__Admin__Some_Class();
+ *
+ * // class will be searched in the path
+ * // `/var/www/site/wp-content/plugins/the-events-calendar/utils/some-dir/Some_Util.php'
+ * $i = new Tribe__Utils__Some_Util();
+ *
+ * // class will be searched in the path
+ * // `/var/www/site/wp-content/plugins/the-events-calendar/deprecated/Tribe_DeprecatedClass.php'
+ * $i = new Tribe_DeprecatedClass();
+ */
+ class Tribe__Autoloader {
+
+ /**
+ * @var Tribe__Autoloader
+ */
+ protected static $instance;
+
+ /**
+ * An arrays of arrays each containing absolute paths.
+ *
+ * Paths are stored trimming any trailing `/`.
+ * E.g. `/var/www/tribe-pro/wp-content/plugins/the-events-calendar/src/Tribe`
+ *
+ * @var string[][]
+ */
+ protected $prefixes;
+
+ /**
+ * An array of registered prefixes with unique slugs.
+ *
+ * @var string[]
+ */
+ protected $prefix_slugs;
+
+ /**
+ * The string acting as a directory separator in a class name.
+ *
+ * E.g.: given `__` as `$dir_separator` then `Admin__Metabox__Some_Metabox`
+ * will map to `/Admin/Metabox/SomeMetabox.php`.
+ *
+ * @var string
+ */
+ protected $dir_separator = '__';
+
+ /** @var string[] */
+ protected $fallback_dirs = [];
+
+ /**
+ * @var array
+ */
+ protected $class_paths = [];
+
+ /**
+ * Returns the singleton instance of the class.
+ *
+ * @return Tribe__Autoloader
+ */
+ public static function instance() {
+ if ( ! self::$instance instanceof Tribe__Autoloader ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Registers prefixes and root dirs using an array.
+ *
+ * Same as calling `register_prefix` on each one.
+ *
+ * @param array $prefixes_to_root_dirs
+ */
+ public function register_prefixes( array $prefixes_to_root_dirs ) {
+ foreach ( $prefixes_to_root_dirs as $prefix => $root_dir ) {
+ $this->register_prefix( $prefix, $root_dir );
+ }
+ }
+
+ /**
+ * Associates a class prefix to an absolute path.
+ *
+ * @param string $prefix A class prefix, e.g. `Tribe__Admin__`
+ * @param string $root_dir The absolute path to the dir containing
+ * the prefixed classes.
+ * @param string $slug An optional unique slug to associate to the prefix.
+ */
+ public function register_prefix( $prefix, $root_dir, $slug = '' ) {
+ $root_dir = $this->normalize_root_dir( $root_dir );
+
+ // Determine if we need to normalize the $prefix.
+ $is_namespaced = false !== strpos( $prefix, '\\' );
+
+ if ( $is_namespaced ) {
+ // If the prefix is a namespace, then normalize it.
+ $prefix = trim( $prefix, '\\' ) . '\\';
+ }
+
+ if ( ! isset( $this->prefixes[ $prefix ] ) ) {
+ $this->prefixes[ $prefix ] = [];
+ }
+
+ $this->prefixes[ $prefix ][] = $root_dir;
+
+ // Let's make sure we're not adding duplicates.
+ $this->prefixes[ $prefix ] = array_unique( $this->prefixes[ $prefix ] );
+
+ if ( $slug ) {
+ $this->prefix_slugs[ $slug ] = $prefix;
+ }
+ }
+
+ /**
+ * Triggers the registration of the autoload method in the SPL
+ * autoload register.
+ */
+ public function register_autoloader() {
+ spl_autoload_register( [ $this, 'autoload' ] );
+ }
+
+ /**
+ * Includes the file defining a class.
+ *
+ * This is the function that's registered as an autoloader.
+ *
+ * @param string $class
+ */
+ public function autoload( $class ) {
+ $include_path = $this->get_class_path( $class );
+ if ( ! empty( $include_path ) ) {
+ include_once( $include_path );
+ }
+ }
+
+ private function normalize_root_dir( $root_dir ) {
+ return rtrim( $root_dir, '/' );
+ }
+
+ protected function get_prefixed_path( $class ) {
+ foreach ( $this->prefixes as $prefix => $dirs ) {
+ $is_namespaced = false !== strpos( $prefix, '\\' );
+
+ if ( strpos( $class, $prefix ) !== 0 ) {
+ continue;
+ }
+
+ $class_name = str_replace( $prefix, '', $class );
+
+ if ( ! $is_namespaced ) {
+ $class_path_frag = implode( '/', explode( $this->dir_separator, $class_name ) ) . '.php';
+ } else {
+ $class_path_frag = implode( '/', explode( '\\', $class_name ) ) . '.php';
+ }
+
+ foreach ( $dirs as $dir ) {
+ $path = $dir . '/' . $class_path_frag;
+ if ( ! file_exists( $path ) ) {
+ // check if the file exists in lowercase
+ $class_path_frag = strtolower( $class_path_frag );
+ $path = $dir . '/' . $class_path_frag;
+ }
+ if ( ! file_exists( $path ) ) {
+ continue;
+ }
+
+ return $path;
+ }
+ }
+ return false;
+ }
+
+ protected function get_fallback_path( $class ) {
+ foreach ( $this->fallback_dirs as $fallback_dir ) {
+ $include_path = $fallback_dir . '/' . $class . '.php';
+ if ( ! file_exists( $include_path ) ) {
+ // check if the file exists in lowercase
+ $class = strtolower( $class );
+ $include_path = $fallback_dir . '/' . $class . '.php';
+ }
+ if ( ! file_exists( $include_path ) ) {
+ continue;
+ }
+
+ return $include_path;
+ }
+ }
+
+ /**
+ * Gets the absolute path to a class file.
+ *
+ * @param string $class The class name
+ *
+ * @return string Either the absolute path to the class file or an
+ * empty string if the file was not found.
+ */
+ public function get_class_path( $class ) {
+ $prefixed_path = $this->get_prefixed_path( $class );
+ if ( $prefixed_path ) {
+ return $prefixed_path;
+ }
+
+ $class_path = ! empty( $this->class_paths[ $class ] ) ? $this->class_paths[ $class ] :false;
+ if ( $class_path ) {
+ return $class_path;
+ }
+
+ $fallback_path = $this->get_fallback_path( $class );
+
+ return $fallback_path ? $fallback_path : '';
+ }
+
+ /**
+ * Get the registered prefix by slug
+ *
+ * @param string $slug Unique slug for registered prefix.
+ *
+ * @return false|string Either the prefix registered to the
+ * unique slug or false if not found.
+ */
+ public function get_prefix_by_slug( $slug ) {
+ $prefix = false;
+
+ if ( isset( $this->prefix_slugs[ $slug ] ) ) {
+ $prefix = $this->prefix_slugs[ $slug ];
+ }
+
+ return $prefix;
+ }
+
+ /**
+ * Adds a folder to search for classes that were not found among
+ * the prefixed ones.
+ *
+ * This is the method to use to register a directory of deprecated
+ * classes.
+ *
+ * @param string $dir An absolute path dto a dir.
+ */
+ public function add_fallback_dir( $dir ) {
+ if ( in_array( $dir, $this->fallback_dirs ) ) {
+ return;
+ }
+ $this->fallback_dirs[] = $this->normalize_root_dir( $dir );
+ }
+
+ /**
+ * @return string
+ */
+ public function get_dir_separator() {
+ return $this->dir_separator;
+ }
+
+ /**
+ * @param string $dir_separator
+ */
+ public function set_dir_separator( $dir_separator ) {
+ $this->dir_separator = $dir_separator;
+ }
+
+ public function register_class( $class, $path ) {
+ $this->class_paths[ $class ] = $path;
+ }
+ }
+ }
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache.php
new file mode 100644
index 000000000..3dbf9e83f
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache.php
@@ -0,0 +1,646 @@
+get_id( $id, $expiration_trigger );
+
+ /**
+ * Filters the expiration for cache objects to provide the ability
+ * to make non-persistent objects be treated as persistent.
+ *
+ * @since 4.8
+ *
+ * @param int $expiration Cache expiration time.
+ * @param string $id Cache ID.
+ * @param mixed $value Cache value.
+ * @param string|array $expiration_trigger Action that triggers automatic expiration.
+ * @param string $key Unique cache key based on Cache ID and expiration trigger last run time.
+ */
+ $expiration = apply_filters( 'tribe_cache_expiration', $expiration, $id, $value, $expiration_trigger, $key );
+
+ if ( self::NON_PERSISTENT === $expiration ) {
+ $group = 'tribe-events-non-persistent';
+ $expiration = 1;
+
+ // Add so we know what group to use in the future.
+ $this->non_persistent_keys[ $id ] = $id;
+ } else {
+ $group = 'tribe-events';
+ }
+
+ return wp_cache_set( $key, $value, $group, $expiration );
+ }
+
+ /**
+ * @param $id
+ * @param $value
+ * @param int $expiration
+ * @param string|array $expiration_trigger
+ *
+ * @return bool
+ */
+ public function set_transient( $id, $value, $expiration = 0, $expiration_trigger = '' ) {
+ if ( $this->data_size_over_packet_size( $value ) ) {
+ return false;
+ }
+
+ return set_transient( $this->get_id( $id, $expiration_trigger ), $value, $expiration );
+ }
+
+ /**
+ * Get cached data. Optionally set data if not previously set.
+ *
+ * Note: When a default value or callback is specified, this value gets set in the cache.
+ *
+ * @param string $id The key for the cached value.
+ * @param string|array $expiration_trigger Optional. Hook to trigger cache invalidation.
+ * @param mixed $default Optional. A default value or callback that returns a default value.
+ * @param int $expiration Optional. When the default value expires, if it gets set.
+ * @param mixed $args Optional. Args passed to callback.
+ *
+ * @return mixed
+ */
+ public function get( $id, $expiration_trigger = '', $default = false, $expiration = 0, $args = [] ) {
+ $group = isset( $this->non_persistent_keys[ $id ] ) ? 'tribe-events-non-persistent' : 'tribe-events';
+ $value = wp_cache_get( $this->get_id( $id, $expiration_trigger ), $group );
+
+ // Value found.
+ if ( false !== $value ) {
+ return $value;
+ }
+
+ if ( is_callable( $default ) ) {
+ // A callback has been specified.
+ $value = $default( ...$args );
+ } else {
+ // Default is a value.
+ $value = $default;
+ }
+
+ // No need to set a cache value to false since non-existent values return false.
+ if ( false !== $value ) {
+ $this->set( $id, $value, $expiration, $expiration_trigger );
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param string $id
+ * @param string|array $expiration_trigger
+ *
+ * @return mixed
+ */
+ public function get_transient( $id, $expiration_trigger = '' ) {
+ return get_transient( $this->get_id( $id, $expiration_trigger ) );
+ }
+
+ /**
+ * @param string $id
+ * @param string|array $expiration_trigger
+ *
+ * @return bool
+ */
+ public function delete( $id, $expiration_trigger = '' ) {
+ $group = isset( $this->non_persistent_keys[ $id ] ) ? 'tribe-events-non-persistent' : 'tribe-events';
+
+ // Delete from non-persistent keys list.
+ if ( 'tribe-events-non-persistent' === $group ) {
+ unset( $this->non_persistent_keys[ $id ] );
+ }
+
+ return wp_cache_delete( $this->get_id( $id, $expiration_trigger ), $group );
+ }
+
+ /**
+ * @param string $id
+ * @param string|array $expiration_trigger
+ *
+ * @return bool
+ */
+ public function delete_transient( $id, $expiration_trigger = '' ) {
+ return delete_transient( $this->get_id( $id, $expiration_trigger ) );
+ }
+
+ /**
+ * Purge all expired tribe_ transients.
+ *
+ * This uses a modification of the the query from https://core.trac.wordpress.org/ticket/20316
+ *
+ * @since 4.11.0
+ *
+ * @return void Just execute the database SQL no return required.
+ */
+ public function delete_expired_transients() {
+ if ( tribe_get_var( 'has_deleted_expired_transients', false ) ) {
+ return;
+ }
+
+ global $wpdb;
+
+ $time = time();
+
+ $sql = "
+ DELETE
+ a,
+ b
+ FROM
+ {$wpdb->options} a
+ INNER JOIN {$wpdb->options} b
+ ON b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )
+ AND b.option_value < {$time}
+ WHERE
+ a.option_name LIKE '\_transient\_tribe\_%'
+ AND a.option_name NOT LIKE '\_transient\_timeout\_tribe\_%'
+ ";
+
+ /**
+ * Allow third party filtering of the SQL used for deleting expired transients.
+ *
+ * @since 4.11.5
+ *
+ * @param string $sql The SQL we execute to delete all the expired transients.
+ * @param int $time Time we are using to determine what is expired.
+ */
+ $sql = apply_filters( 'tribe_cache_delete_expired_transients_sql', $sql, $time );
+
+ if ( empty( $sql ) ) {
+ return;
+ }
+
+ $wpdb->query( $sql );
+
+ // Set the variable to prevent this call from running twice.
+ tribe_set_var( 'has_deleted_expired_transients', true );
+ }
+
+ /**
+ * Flag if we should delete
+ *
+ * @since 4.11.5
+ *
+ * @param boolean $value If we should delete transients or not on shutdown.
+ *
+ * @return void No return for setting the flag.
+ */
+ public function flag_required_delete_transients( $value = true ) {
+ tribe_set_var( 'should_delete_expired_transients', $value );
+ }
+
+ /**
+ * Runs on hook `shutdown` and will delete transients on the end of the request.
+ *
+ * @since 4.11.5
+ *
+ * @return void No return for action hook method.
+ */
+ public function maybe_delete_expired_transients() {
+ if ( ! tribe_get_var( 'should_delete_expired_transients', false ) ) {
+ return;
+ }
+
+ $this->delete_expired_transients();
+ }
+
+ /**
+ * @param string $key
+ * @param string|array $expiration_trigger
+ *
+ * @return string
+ */
+ public function get_id( $key, $expiration_trigger = '' ) {
+ if ( is_array( $expiration_trigger ) ) {
+ $triggers = $expiration_trigger;
+ } elseif ( 'tribe-events-non-persistent' !== $expiration_trigger && 'tribe-events' !== $expiration_trigger ) {
+ $triggers = array_filter( explode( '|', $expiration_trigger ) );
+ }
+
+ $last = 0;
+ foreach ( $triggers as $trigger ) {
+ // Bail on empty trigger otherwise it creates a `tribe_last_` opt on the DB.
+ if ( empty( $trigger ) ) {
+ continue;
+ }
+
+ $occurrence = $this->get_last_occurrence( $trigger );
+
+ if ( $occurrence > $last ) {
+ $last = $occurrence;
+ }
+ }
+
+ $last = empty( $last ) ? '' : $last;
+ $id = $key . $last;
+ if ( strlen( $id ) > 80 ) {
+ $id = 'tribe_' . md5( $id );
+ }
+
+ return $id;
+ }
+
+ /**
+ * Returns the time of an action last occurrence.
+ *
+ * @since 4.9.14 Changed the return value type from `int` to `float`.
+ * @since 5.0.17 No longer memoizes the first triggered timestamp.
+ *
+ * @param string $action The action to return the time for.
+ *
+ * @return float The time (microtime) an action last occurred, or the current microtime if it never occurred.
+ */
+ public function get_last_occurrence( $action ) {
+ $last_action = (float) get_option( 'tribe_last_' . $action, null );
+
+ if ( ! $last_action ) {
+ $last_action = microtime( true );
+ $this->set_last_occurrence( $action, $last_action );
+ }
+
+ return $last_action;
+ }
+
+ /**
+ * Sets the time (microtime) for an action last occurrence.
+ *
+ * @since 4.9.14 Changed the type of the time stored from an `int` to a `float`.
+ *
+ * @param string $action The action to record the last occurrence of.
+ * @param int|float $timestamp The timestamp to assign to the action last occurrence or the current time (microtime).
+ *
+ * @return boolean IF we were able to set the last occurrence or not.
+ */
+ public function set_last_occurrence( $action, $timestamp = 0 ) {
+ if ( empty( $timestamp ) ) {
+ $timestamp = microtime( true );
+ }
+ $updated = update_option( 'tribe_last_' . $action, (float) $timestamp );
+
+ // For performance reasons we will only expire cache once per request, when needed.
+ if ( $updated ) {
+ $this->flag_required_delete_transients( true );
+ }
+
+ return $updated;
+ }
+
+ /**
+ * Builds a key from an array of components and an optional prefix.
+ *
+ * @param mixed $components Either a single component of the key or an array of key components.
+ * @param string $prefix
+ * @param bool $sort Whether component arrays should be sorted or not to generate the key; defaults to
+ * `true`.
+ *
+ * @return string The resulting key.
+ */
+ public function make_key( $components, $prefix = '', $sort = true ) {
+ $key = '';
+ $components = is_array( $components ) ? $components : [ $components ];
+ foreach ( $components as $component ) {
+ if ( $sort && is_array( $component ) ) {
+ $is_associative = count( array_filter( array_keys( $component ), 'is_numeric' ) ) < count( array_keys( $component ) );
+ if ( $is_associative ) {
+ ksort( $component );
+ } else {
+ sort( $component );
+ }
+ }
+ $key .= maybe_serialize( $component );
+ }
+
+ return $this->get_id( $prefix . md5( $key ) );
+ }
+
+ /**
+ * Whether a offset exists.
+ *
+ * @since 4.11.0
+ * @since 5.0.13 Will check against cache expiration. Previously would give false positive
+ * if expiration had passed but was cached recently. Will now consider null not set.
+ *
+ * @param mixed $offset An offset to check for.
+ *
+ * @return boolean Whether the offset exists in the cache.
+ *@link http://php.net/manual/en/arrayaccess.offsetexists.php
+ *
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists( $offset ): bool {
+ $value = $this->get( $offset );
+
+ return $value !== false && $value !== null;
+ }
+
+ /**
+ * Offset to retrieve.
+ *
+ * @link http://php.net/manual/en/arrayaccess.offsetget.php
+ *
+ * @since 4.11.0
+ *
+ * @param mixed $offset The offset to retrieve.
+ *
+ * @return mixed Can return all value types.
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet( $offset ) {
+ return $this->get( $offset );
+ }
+
+ /**
+ * Offset to set.
+ *
+ * @since 4.11.0
+ *
+ * @link http://php.net/manual/en/arrayaccess.offsetset.php
+ *
+ * @param mixed $offset The offset to assign the value to.
+ * @param mixed $value The value to set.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet( $offset, $value ): void {
+ $this->set( $offset, $value, self::NON_PERSISTENT );
+ }
+
+ /**
+ * Offset to unset.
+ *
+ * @since 4.11.0
+ *
+ * @link http://php.net/manual/en/arrayaccess.offsetunset.php
+ *
+ * @param mixed $offset The offset to unset.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset( $offset ): void {
+ $this->delete( $offset );
+ }
+
+ /**
+ * Removes a group of the cache, for now only `non_persistent` is supported.
+ *
+ * @since 4.14.13
+ *
+ * @return bool
+ */
+ public function reset( $group = 'non_persistent' ) {
+ if ( 'non_persistent' !== $group ) {
+ return false;
+ }
+ $this->non_persistent_keys = [];
+ return true;
+ }
+
+ /**
+ * Warms up the caches for a collection of posts.
+ *
+ * @since 4.10.2
+ *
+ * @param array|int $post_ids A post ID, or a collection of post IDs.
+ * @param bool $update_post_meta_cache Whether to warm-up the post meta cache for the posts or not.
+ */
+ public function warmup_post_caches( $post_ids, $update_post_meta_cache = false ) {
+ if ( empty( $post_ids ) ) {
+ return;
+ }
+
+ $post_ids = (array) $post_ids;
+
+ global $wpdb;
+
+ $already_cached_ids = [];
+ foreach ( $post_ids as $post_id ) {
+ if ( wp_cache_get( $post_id, 'posts' ) instanceof \WP_Post ) {
+ $already_cached_ids[] = $post_id;
+ }
+ }
+
+ $required = array_diff( $post_ids, $already_cached_ids );
+
+ if ( empty( $required ) ) {
+ return;
+ }
+
+ /** @var Tribe__Feature_Detection $feature_detection */
+ $feature_detection = tribe( 'feature-detection' );
+ $limit = $feature_detection->mysql_limit_for_example( 'post_result' );
+
+ /**
+ * Filters the LIMIT that should be used to warm-up post caches and postmeta caches (if the
+ * `$update_post_meta_cache` parameter is `true`).
+ *
+ * Lower this value on less powerful hosts. Return `0` to disable the warm-up completely, and `-1` to remove the
+ * limit (not recommended).
+ *
+ * @since 4.10.2
+ *
+ * @param int $limit The number of posts whose caches will be warmed up, per query.
+ */
+ $limit = (int) apply_filters( 'tribe_cache_warmup_post_cache_limit', min( $limit, count( $post_ids ) ) );
+
+ if ( 0 === $limit ) {
+ // Warmup disabled.
+ return;
+ }
+
+ $buffer = $post_ids;
+ $page = 0;
+
+ do {
+ $limit_clause = $limit < 0 ? sprintf( 'LIMIT %d,%d', $limit * $page, $limit ) : '';
+ $page++;
+ $these_ids = array_splice( $buffer, 0, $limit );
+ $interval = implode( ',', array_map( 'absint', $these_ids ) );
+ $posts_query = "SELECT * FROM {$wpdb->posts} WHERE ID IN ({$interval}) {$limit_clause}";
+ $post_objects = $wpdb->get_results( $posts_query );
+ if ( is_array( $post_objects ) && ! empty( $post_objects ) ) {
+ foreach ( $post_objects as $post_object ) {
+ $post = new \WP_Post( $post_object );
+ wp_cache_set( $post_object->ID, $post, 'posts' );
+ }
+
+ if ( $update_post_meta_cache ) {
+ update_meta_cache( 'post', $these_ids );
+ }
+ }
+ } while ( ! empty( $post_objects ) && is_array( $post_objects ) && count( $post_objects ) < count( $post_ids ) );
+ }
+
+ /**
+ * If NOT using an external object caching system, then check if the size, in bytes, of the data
+ * to write to the database would fit into the `max_allowed_packet` setting or not.
+ *
+ * @since 4.12.14
+ *
+ * @param string|array|object $value The value to check.
+ *
+ * @return bool Whether the data, in its serialized form, would fit into the current database `max_allowed_packet`
+ * setting or not.
+ */
+ public function data_size_over_packet_size( $value ) {
+ if ( wp_using_ext_object_cache() ) {
+ // We cannot know and that is a concern of the external caching system.
+ return false;
+ }
+
+ try {
+ $serialized_value = maybe_serialize( $value );
+ $size = strlen( $serialized_value );
+ } catch ( Exception $e ) {
+ // The underlying function would run into the same issue, bail and do not set the transient.
+ return true;
+ }
+
+ /** @var Tribe__Feature_Detection $feature_detection */
+ $feature_detection = tribe( 'feature-detection' );
+
+ // If the size of the string is above 90% of the database `max_allowed_packet` setting, then it should not be written to the db.
+ return $size > ( $feature_detection->get_mysql_max_packet_size() * .9 );
+ }
+
+ /**
+ * Returns a transient that might have been stored, due ot its size, in chunks.
+ *
+ * @since 4.13.3
+ *
+ * @param string $id The name of the transients to return.
+ * @param string|array $expiration_trigger The transient expiration trigger(s).
+ *
+ * @return false|mixed Either the transient value, joined back into one, or `false` to indicate
+ * the transient was not found or was malformed.
+ */
+ public function get_chunkable_transient( $id, $expiration_trigger = '' ) {
+ $transient = $this->get_id( $id, $expiration_trigger );
+
+ if ( wp_using_ext_object_cache() ) {
+ return get_transient( $transient );
+ }
+
+ $chunks = [];
+ $i = 0;
+ do {
+ $chunk_transient = $transient . '_' . $i++;
+ $chunk = get_transient( $chunk_transient );
+ $chunks[ $chunk_transient ] = (string) $chunk;
+ } while ( ! empty( $chunk ) );
+
+ // Remove any piece of data that was added but is not relevant.
+ $chunks = array_filter( $chunks );
+
+ if ( empty( $chunks ) ) {
+ return false;
+ }
+
+ try {
+ $data = implode( '', $chunks );
+ $is_serialized = preg_match( '/^[aO]:\\d+:/', $data );
+ $unserialized = maybe_unserialize( implode( '', $chunks ) );
+
+ if ( is_string( $unserialized ) && $unserialized === $data && $is_serialized ) {
+ // Something was messed up.
+ return false;
+ }
+
+ return $unserialized;
+ } catch ( Exception $e ) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets a transient in the database with the knowledge that, if too large to be stored in one
+ * DB row, it will be chunked.
+ *
+ * The method will redirect to the `set_transient` function if the site is using object caching.
+ *
+ *
+ * @since 4.13.3
+ *
+ * @param string $id The transient ID.
+ * @param mixed $value The value to store, that could be chunked.
+ * @param int $expiration The transient expiration, in seconds.
+ * @param string|array $expiration_trigger The transient expiration trigger(s).
+ *
+ * @return bool Whether the transient, or the transient chunks, have been stored correctly or not.
+ */
+ public function set_chunkable_transient( $id, $value, $expiration = 0, $expiration_trigger = '' ) {
+ $transient = $this->get_id( $id, $expiration_trigger );
+
+ if ( wp_using_ext_object_cache() ) {
+ return $this->set_transient( $transient, $value, $expiration );
+ }
+
+ $inserted = [];
+ $serialized_value = maybe_serialize( $value );
+ $chunk_size = tribe( 'feature-detection' )->get_mysql_max_packet_size() * 0.9;
+ $chunks = str_split( $serialized_value, $chunk_size );
+ foreach ( $chunks as $i => $chunk ) {
+ $chunk_transient = $transient . '_' . $i;
+
+ $set = set_transient( $chunk_transient, $chunk, $expiration );
+
+ if ( ! $set ) {
+ foreach ( $inserted as $transient_to_delete ) {
+ delete_transient( $transient_to_delete );
+ }
+
+ return false;
+ }
+
+ $inserted[] = $chunk_transient;
+ }
+
+ return true;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache_Listener.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache_Listener.php
new file mode 100644
index 000000000..bc428398a
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache_Listener.php
@@ -0,0 +1,215 @@
+cache = new Tribe__Cache();
+ }
+
+ /**
+ * Run the init functionality (like add_hooks).
+ *
+ * @return void
+ */
+ public function init() {
+ $this->add_hooks();
+ }
+
+ /**
+ * Add the hooks necessary.
+ *
+ * @return void
+ */
+ private function add_hooks() {
+ add_action( 'save_post', [ $this, 'save_post' ], 0, 2 );
+ add_action( 'updated_option', [ $this, 'update_last_updated_option' ], 10, 3 );
+ add_action( 'updated_option', [ $this, 'update_last_save_post' ], 10, 3 );
+ add_action( 'generate_rewrite_rules', [ $this, 'generate_rewrite_rules' ] );
+ add_action( 'clean_post_cache', [ $this, 'save_post' ], 0, 2 );
+ }
+
+ /**
+ * Run the caching functionality that is executed on save post.
+ *
+ * @param int $post_id The post_id.
+ * @param WP_Post $post The current post object being saved.w
+ */
+ public function save_post( $post_id, $post ) {
+ if ( in_array( $post->post_type, Tribe__Main::get_post_types() ) ) {
+ $this->cache->set_last_occurrence( self::TRIGGER_SAVE_POST );
+ }
+ }
+
+ /**
+ * Run the caching functionality that is executed on saving tribe calendar options.
+ *
+ * @see 'updated_option'
+ *
+ * @param string $option_name Name of the updated option.
+ * @param mixed $old_value The old option value.
+ * @param mixed $value The new option value.
+ */
+ public function update_last_save_post( $option_name, $old_value, $value ) {
+ $triggers = [
+ 'tribe_events_calendar_options' => true,
+ 'permalink_structure' => true,
+ 'rewrite_rules' => true,
+ 'start_of_week' => true,
+ ];
+
+ $triggers = $this->filter_action_last_occurrence_triggers( $triggers, static::TRIGGER_SAVE_POST, func_get_args() );
+
+ if ( ! empty( $triggers[ $option_name ] ) ) {
+ $this->cache->set_last_occurrence( self::TRIGGER_SAVE_POST );
+ }
+ }
+
+ /**
+ * Run the caching functionality that is executed on saving tribe calendar options.
+ *
+ * @see 'updated_option'
+ *
+ * @since 4.11.0
+ *
+ * @param string $option_name Name of the updated option.
+ * @param mixed $old_value The old option value.
+ * @param mixed $value The new option value.
+ */
+ public function update_last_updated_option( $option_name, $old_value, $value ) {
+ $triggers = [
+ 'active_plugins' => true,
+ 'tribe_events_calendar_options' => true,
+ 'permalink_structure' => true,
+ 'rewrite_rules' => true,
+ 'start_of_week' => true,
+ 'sidebars_widgets' => true,
+ 'stylesheet' => true,
+ 'template' => true,
+ 'WPLANG' => true,
+ ];
+
+ $triggers = $this->filter_action_last_occurrence_triggers( $triggers, static::TRIGGER_UPDATED_OPTION, func_get_args() );
+
+ if ( ! empty( $triggers[ $option_name ] ) ) {
+ $this->cache->set_last_occurrence( self::TRIGGER_UPDATED_OPTION );
+ }
+ }
+
+ /**
+ * Filtering for last occurrence triggers.
+ *
+ * @since 4.13.2
+ *
+ * @param array $triggers Which options will trigger this given action last occurrence.
+ * @param string $action Which action this trigger will set.
+ * @param array $args Which arguments from the updated option method.
+ *
+ * @return array
+ */
+ public function filter_action_last_occurrence_triggers( array $triggers = [], $action = '', array $args = [] ) {
+
+ /**
+ * Filters the contents of which options will trigger expiring a given actions cache.
+ *
+ * @since 4.13.2
+ *
+ * @param array $triggers Which options will trigger this given action last occurrence.
+ * @param string $action Which action this trigger will set.
+ * @param array $args Which arguments from the updated option method.
+ */
+ $triggers = apply_filters( 'tribe_cache_last_occurrence_option_triggers', $triggers, $action, $args );
+
+ /**
+ * Filters the contents of which options will trigger expiring a given actions cache.
+ * Allows filtering a specific action.
+ *
+ * @since 4.13.2
+ *
+ * @param array $triggers Which options will trigger this given action last occurrence.
+ * @param string $action Which action this trigger will set.
+ * @param array $args Which arguments from the updated option method.
+ */
+ return (array) apply_filters( "tribe_cache_last_occurrence_option_triggers:{$action}", $triggers, $action, $args );
+ }
+
+ /**
+ * For any hook that doesn't need any additional filtering
+ *
+ * @param $method
+ * @param $args
+ */
+ public function __call( $method, $args ) {
+ $this->cache->set_last_occurrence( $method );
+ }
+
+ /**
+ * Instance method of the cache listener.
+ *
+ * @return Tribe__Cache_Listener
+ */
+ public static function instance() {
+ if ( empty( self::$instance ) ) {
+ self::$instance = self::create_listener();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Create a cache listener.
+ *
+ * @return Tribe__Cache_Listener
+ */
+ private static function create_listener() {
+ $listener = new self();
+ $listener->init();
+
+ return $listener;
+ }
+
+ /**
+ * Run the caching functionality that is executed when rewrite rules are generated.
+ *
+ * @since 4.9.11
+ */
+ public function generate_rewrite_rules() {
+ $this->cache->set_last_occurrence( self::TRIGGER_GENERATE_REWRITE_RULES );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Changelog_Reader.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Changelog_Reader.php
new file mode 100644
index 000000000..43041367e
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Changelog_Reader.php
@@ -0,0 +1,53 @@
+version_count = (int) $version_count;
+ $this->readme_file = empty( $readme_file ) ? $this->default_readme_file() : $readme_file;
+ }
+
+ protected function default_readme_file() {
+ return dirname( Tribe__Main::instance()->plugin_path ) . '/readme.txt';
+ }
+
+ public function get_changelog() {
+ $contents = $this->extract_changelog_section();
+ $lines = explode( "\n", $contents );
+
+ $sections = [];
+ $current_section = '';
+ foreach ( $lines as $line ) {
+ $line = trim( $line );
+ if ( substr( $line, 0, 1 ) == '=' ) {
+ if ( count( $sections ) >= $this->version_count ) {
+ break;
+ }
+ $header = trim( $line, '= ' );
+ $current_section = esc_html( $header );
+ $sections[ $current_section ] = [];
+ } elseif ( strlen( $line ) > 0 ) {
+ $message = trim( $line, '* ' );
+ $sections[ $current_section ][] = esc_html( $message );
+ }
+ }
+ return $sections;
+ }
+
+ protected function extract_changelog_section() {
+ $contents = $this->get_readme_file_contents();
+ $start = strpos( $contents, '== Changelog ==' );
+ if ( $start === false ) {
+ return '';
+ }
+ $start += 16; // account for the length of the header
+ $end = strpos( $contents, '==', $start );
+ return trim( substr( $contents, $start, $end - $start ) );
+ }
+
+ protected function get_readme_file_contents() {
+ return file_get_contents( $this->readme_file );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Container.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Container.php
new file mode 100644
index 000000000..56ba83353
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Container.php
@@ -0,0 +1,358 @@
+doSomething();
+ *
+ * Need the class built immediately? Build it and register it:
+ *
+ * tribe_singleton( 'tec.admin.class', new Tribe__Admin__Class() );
+ *
+ * // some code later...
+ *
+ * tribe( 'tec.admin.class' )->doSomething();
+ *
+ * Need a very custom way to build the class? Register a callback:
+ *
+ * tribe_singleton( 'tec.admin.class', array( Tribe__Admin__Class__Factory, 'make' ) );
+ *
+ * // some code later...
+ *
+ * tribe( 'tec.admin.class' )->doSomething();
+ *
+ * Or register the methods that should be called on the object after its construction:
+ *
+ * tribe_singleton( 'tec.admin.class', 'Tribe__Admin__Class', array( 'hook', 'register' ) );
+ *
+ * // some code later...
+ *
+ * // the `hook` and `register` methods will be called on the built instance.
+ * tribe( 'tec.admin.class' )->doSomething();
+ *
+ * The class will be built only once (if passing the class name or a callback function), stored
+ * and the same instance will be returned from that moment on.
+ *
+ * @param string $slug The human-readable and catchy name of the class.
+ * @param string|object|callable $class The full class name or an instance of the class
+ * or a callback that will return the instance of the class.
+ * @param array $after_build_methods An array of methods that should be called on
+ * the built object after the `__construct` method; the methods
+ * will be called only once after the singleton instance
+ * construction.
+ */
+ function tribe_singleton( $slug, $class, array $after_build_methods = null ) {
+ Tribe__Container::init()->singleton( $slug, $class, $after_build_methods );
+ }
+}
+
+if ( ! function_exists( 'tribe_register' ) ) {
+ /**
+ * Registers a class.
+ *
+ * Each call to obtain an instance of this class made using the `tribe( $slug )` function
+ * will return a new instance; the instances are built just in time (if not passing an
+ * object instance, in that case it will work as a singleton) and on the first request.
+ * The container will call the class `__construct` method on the class (if not passing an object
+ * or a callback function) and will try to automagically resolve dependencies.
+ *
+ * Example use:
+ *
+ * tribe_register( 'tec.some', 'Tribe__Some' );
+ *
+ * // some code later...
+ *
+ * // class is built here
+ * $some_one = tribe( 'tec.some' )->doSomething();
+ *
+ * // $some_two !== $some_one
+ * $some_two = tribe( 'tec.some' )->doSomething();
+ *
+ * Need the class built immediately? Build it and register it:
+ *
+ * tribe_register( 'tec.admin.class', new Tribe__Admin__Class() );
+ *
+ * // some code later...
+ *
+ * // $some_two === $some_one
+ * // acts like a singleton
+ * $some_one = tribe( 'tec.some' )->doSomething();
+ * $some_two = tribe( 'tec.some' )->doSomething();
+ *
+ * Need a very custom way to build the class? Register a callback:
+ *
+ * tribe_register( 'tec.some', array( Tribe__Some__Factory, 'make' ) );
+ *
+ * // some code later...
+ *
+ * // $some_two !== $some_one
+ * $some_one = tribe( 'tec.some' )->doSomething();
+ * $some_two = tribe( 'tec.some' )->doSomething();
+ *
+ * Or register the methods that should be called on the object after its construction:
+ *
+ * tribe_singleton( 'tec.admin.class', 'Tribe__Admin__Class', array( 'hook', 'register' ) );
+ *
+ * // some code later...
+ *
+ * // the `hook` and `register` methods will be called on the built instance.
+ * tribe( 'tec.admin.class' )->doSomething();
+ *
+ * @param string $slug The human-readable and catchy name of the class.
+ * @param string|object|callable $class The full class name or an instance of the class
+ * or a callback that will return the instance of the class.
+ * @param array $after_build_methods An array of methods that should be called on
+ * the built object after the `__construct` method; the methods
+ * will be called each time after the instance construction.
+ */
+ function tribe_register( $slug, $class, array $after_build_methods = null ) {
+ Tribe__Container::init()->bind( $slug, $class, $after_build_methods );
+ }
+}
+
+if ( ! function_exists( 'tribe' ) ) {
+ /**
+ * Returns a ready to use instance of the requested class.
+ *
+ * Example use:
+ *
+ * tribe_singleton( 'common.main', 'Tribe__Main');
+ *
+ * // some code later...
+ *
+ * tribe( 'common.main' )->do_something();
+ *
+ * @param string|null $slug_or_class Either the slug of a binding previously registered using `tribe_singleton` or
+ * `tribe_register` or the full class name that should be automagically created or
+ * `null` to get the container instance itself.
+ *
+ * @return mixed|object|Tribe__Container The instance of the requested class. Please note that the cardinality of
+ * the class is controlled registering it as a singleton using `tribe_singleton`
+ * or `tribe_register`; if the `$slug_or_class` parameter is null then the
+ * container itself will be returned.
+ */
+ function tribe( $slug_or_class = null ) {
+ $container = Tribe__Container::init();
+
+ return null === $slug_or_class ? $container : $container->make( $slug_or_class );
+ }
+}
+
+if ( ! function_exists( 'tribe_set_var' ) ) {
+ /**
+ * Registers a value under a slug in the container.
+ *
+ * Example use:
+ *
+ * tribe_set_var( 'tec.url', 'http://example.com' );
+ *
+ * @param string $slug The human-readable and catchy name of the var.
+ * @param mixed $value The variable value.
+ */
+ function tribe_set_var( $slug, $value ) {
+ $container = Tribe__Container::init();
+ $container->setVar( $slug, $value );
+ }
+}
+
+if ( ! function_exists( 'tribe_get_var' ) ) {
+ /**
+ * Returns the value of a registered variable.
+ *
+ * Example use:
+ *
+ * tribe_set_var( 'tec.url', 'http://example.com' );
+ *
+ * $url = tribe_get_var( 'tec.url' );
+ *
+ * @param string $slug The slug of the variable registered using `tribe_set_var`.
+ * @param null $default The value that should be returned if the variable slug
+ * is not a registered one.
+ *
+ * @return mixed Either the registered value or the default value if the variable
+ * is not registered.
+ */
+ function tribe_get_var( $slug, $default = null ) {
+ $container = Tribe__Container::init();
+
+ try {
+ $var = $container->getVar( $slug );
+ } catch ( InvalidArgumentException $e ) {
+ return $default;
+ }
+
+ return $var;
+ }
+}
+
+if ( ! function_exists( 'tribe_unset_var' ) ) {
+ /**
+ * Returns the value of a registered variable.
+ *
+ * Example use:
+ *
+ * tribe_set_var( 'tec.url', 'http://example.com' );
+ *
+ * tribe_unset_var( 'tec.url' );
+ *
+ * @since 4.11.0
+ *
+ * @param string $slug The slug of the variable registered using `tribe_unset_var`.
+ *
+ * @return void
+ */
+ function tribe_unset_var( $slug ) {
+ $container = Tribe__Container::init();
+ try {
+ $container->offsetUnset( $slug );
+ } catch ( Exception $e ) {}
+ }
+}
+
+if ( ! function_exists( 'tribe_isset_var' ) ) {
+ /**
+ * Returns the value of a registered variable.
+ *
+ * Example use:
+ *
+ * tribe_set_var( 'tec.url', 'http://example.com' );
+ *
+ * tribe_isset_var( 'tec.url' );
+ *
+ * @since 4.11.0
+ *
+ * @param string $slug The slug of the variable checked using `tribe_isset_var`.
+ *
+ * @return boolean Either a the given slug exists.
+ */
+ function tribe_isset_var( $slug ) {
+ $container = Tribe__Container::init();
+ return $container->offsetExists( $slug );
+ }
+}
+
+if ( ! function_exists( 'tribe_register_provider' ) ) {
+ /**
+ * Registers a service provider in the container.
+ *
+ * Service providers must implement the `use ServiceProviderInterface` interface or extend
+ * the `ServiceProvider` class.
+ *
+ * @see ServiceProvider
+ * @see ServiceProviderInterface
+ *
+ * @param string $provider_class
+ */
+ function tribe_register_provider( $provider_class ) {
+ $container = Tribe__Container::init();
+
+ if ( $provider_class === 'Tribe\Tickets\Admin\Home\Service_Provider' ) {
+ /**
+ * Prevent binding a poorly located service provider registration in ET pre 5.6.0
+ * and places it after ET Main::bind_implementations().
+ *
+ * @todo: Remove this after TEC 7.5 after enough time has passed.
+ */
+ add_action(
+ 'tribe_tickets_plugin_loaded',
+ static function() use ( $container ) {
+ $container->register( Tribe\Tickets\Admin\Home\Service_Provider::class );
+ }
+ );
+ } else {
+ $container->register( $provider_class );
+ }
+ }
+
+ if ( ! function_exists( 'tribe_callback' ) ) {
+ /**
+ * Returns a lambda function suitable to use as a callback; when called the function will build the implementation
+ * bound to `$classOrInterface` and return the value of a call to `$method` method with the call arguments.
+ *
+ * @since 4.7
+ * @since 4.6.2 Included the $argsN params
+ *
+ * @param string $slug A class or interface fully qualified name or a string slug.
+ * @param string $method The method that should be called on the resolved implementation with the
+ * specified array arguments.
+ * @param mixed [$argsN] (optional) Any number of arguments that will be passed down to the Callback
+ *
+ * @return callable A PHP Callable based on the Slug and Methods passed
+ */
+ function tribe_callback( $slug, $method ) {
+ $container = Tribe__Container::init();
+ $arguments = func_get_args();
+ $is_empty = 2 === count( $arguments );
+
+ if ( $is_empty ) {
+ $callable = $container->callback( $slug, $method );
+ } else {
+ $callback = $container->callback( 'callback', 'get' );
+ $callable = call_user_func_array( $callback, $arguments );
+ }
+
+ return $callable;
+ }
+ }
+
+ if ( ! function_exists( 'tribe_callback_return' ) ) {
+ /**
+ * Returns a tribe_callback for a very simple Return value method
+ *
+ * Example of Usage:
+ *
+ * add_filter( 'admin_title', tribe_callback_return( __( 'Ready to work.' ) ) );
+ *
+ * @since 4.6.2
+ *
+ * @param mixed $value The value to be returned
+ *
+ * @return callable A PHP Callable based on the Slug and Methods passed
+ */
+ function tribe_callback_return( $value ) {
+ return tribe_callback( 'callback', 'return_value', $value );
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Context.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context.php
new file mode 100644
index 000000000..04f753a20
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context.php
@@ -0,0 +1,1655 @@
+ => [ 'read' => , 'write' => ] ].
+ * The key is used to identify the property that will be accessible with the `get` and
+ * 'dangerously_set_global_context' method, e.g. `$context->get( 'event_display', 'list' );`.
+ * The locations is a list of locations the context will search, top to bottom, left to right, to find a value that's
+ * not empty or the default one, here's a list of supported lookup locations:
+ *
+ * request_var - look into $_GET, $_POST, $_PUT, $_DELETE, $_REQUEST.
+ * query_var - get the value from the main WP_Query object query vars.
+ * query_prop - get the value from a property of the main WP_Query object.
+ * tribe_option - get the value from a Tribe option.
+ * option - get the value from a database option.
+ * transient - get the value from a transient.
+ * constant - get the value from a constant, can also be a class constant with ::.
+ * global_var - get the value from a global variable
+ * static_prop - get the value from a class static property, format: `array( $class, $prop )`.
+ * prop - get the value from a tribe() container binding, format `array( $binding, $prop )`.
+ * static_method - get the value from a class static method.
+ * method - get the value calling a method on a tribe() container binding.
+ * func - get the value from a function or a closure.
+ * filter - get the value by applying a filter.
+ * location_func - get the value by applying a callback to the value of a location.
+ *
+ * For each location additional arguments can be specified:
+ * orm_arg - if `false` then the location will never produce an ORM argument, if provided the ORM arg produced bye the
+ * location will have this name.
+ * orm_transform - if provided the value of the location will be obtained by passing it as an argument to a callable.
+ *
+ * As the Context locations increase in number it would be impractical to define them inline here.
+ * The locations will be loaded by the `Tribe__Context::populate_locations` method from the `Context/locations.php`
+ * file.
+ *
+ * @var array
+ */
+ protected static $locations = [];
+
+ /**
+ * A utility static property keeping track of write locations that
+ * will be defined as associative arrays.
+ *
+ * @var array
+ */
+ protected static $associative_locations = [
+ self::TRANSIENT,
+ self::METHOD,
+ self::STATIC_METHOD,
+ self::PROP,
+ self::STATIC_PROP,
+ ];
+
+ /**
+ * Whether the static dynamic locations were set or not.
+ *
+ * @var bool
+ */
+ protected static $did_populate_locations = false;
+
+ /**
+ * A list of override locations to read and write from.
+ *
+ * This list has the same format and options as the static `$locations` property
+ * but allows a context instance to override, or add, read and write locations.
+ *
+ * @var array
+ */
+ protected $override_locations = [];
+
+ /**
+ * Whether the context of the current HTTP request is an AJAX one or not.
+ *
+ * @var bool
+ */
+ protected $doing_ajax;
+
+ /**
+ * Whether the context of the current HTTP request is a Cron one or not.
+ *
+ * @var bool
+ */
+ protected $doing_cron;
+
+ /**
+ * A request-based array cache to store the values fetched by the context.
+ *
+ * @var array
+ */
+ protected $request_cache = [];
+
+ /**
+ * Whether this context should use the default locations or not.
+ * This flag property is set to `false` when a context is obtained using
+ * the `set_locations` method; it will otherwise be set to `true`.
+ *
+ * @var bool
+ */
+ protected $use_default_locations = true;
+
+ /**
+ * An instance of the post state handler.
+ *
+ * @since 5.0.13
+ *
+ * @var Post_Request_Type
+ */
+ protected Post_Request_Type $post_state;
+
+ /**
+ * Tribe__Context constructor.
+ *
+ * since 5.0.13
+ *
+ * @param Post_Request_Type|null $post_state An instance of the post state handler.
+ */
+ public function __construct( Post_Request_Type $post_state = null ) {
+ $this->post_state = $post_state ?: tribe( Post_Request_Type::class );
+ }
+
+ /**
+ * Whether we are currently creating a new post, a post of post type(s) or not.
+ *
+ * @since 4.7.7
+ * @since 5.0.13 Extracted the logic to the `TEC\Common\Context\Post_Request_Type` class.
+ *
+ * @param null $post_type The optional post type to check.
+ *
+ * @return bool Whether we are currently creating a new post, a post of post type(s) or not.
+ */
+ public function is_new_post( $post_type = null ) {
+ return $this->post_state->is_new_post( $post_type );
+ }
+
+ /**
+ * Whether we are currently editing a post(s), post type(s) or not.
+ *
+ * @since 4.7.7
+ * @since 5.0.13 Extracted the logic to the `TEC\Common\Context\Post_Request_Type` class.
+ *
+ * @param null|array|string|int $post_or_type A post ID, post type, an array of post types or post IDs, `null`
+ * to just make sure we are currently editing a post.
+ *
+ * @return bool
+ */
+ public function is_editing_post( $post_or_type = null ): bool {
+ return $this->post_state->is_editing_post( $post_or_type );
+ }
+
+ /**
+ * Helper function to indicate whether the current execution context is AJAX.
+ *
+ * This method exists to allow us test code that behaves differently depending on the execution
+ * context.
+ *
+ * @since 4.7.12
+ * @since 4.9.5 Removed the $doing_ajax parameter.
+ *
+ * @return boolean
+ */
+ public function doing_ajax() {
+ return function_exists( 'wp_doing_ajax' )
+ ? wp_doing_ajax()
+ : defined( 'DOING_AJAX' ) && DOING_AJAX;
+ }
+
+ /**
+ * Checks whether the context of the current HTTP request is a Cron one or not.
+ *
+ * @since 4.7.23
+ * @since 4.9.5 Removed the $doing_cron parameter.
+ *
+ * @return bool Whether the context of the current HTTP request is a Cron one or not.
+ */
+ public function doing_cron() {
+ return function_exists( 'wp_doing_cron' )
+ ? wp_doing_cron()
+ : defined( 'DOING_CRON' ) && DOING_CRON;
+ }
+
+ /**
+ * Gets a value reading it from the location(s) defined in the `Tribe__Context::$props
+ *
+ * @since 4.9.5
+ *
+ * @param string $key The key of the variable to fetch.
+ * @param mixed|null $default The default value to return if not found.
+ * @param bool $force Whether to force the re-fetch of the value from the context or
+ * not; defaults to `false`.
+ *
+ * @return mixed The value from the first location that can provide it or the default
+ * value if not found.
+ */
+ public function get( $key, $default = null, $force = false ) {
+ /**
+ * Filters the value of a context variable skipping all of its logic.
+ *
+ * @since 4.9.5
+ *
+ * @param mixed $value The value for the key before it's fetched from the context.
+ * @param string $key The key of the value to fetch from the context.
+ * @param mixed $default The default value that should be returned if the value is
+ * not set in the context.
+ * @param bool $force Whether to force the re-fetch of the value from the context or
+ * not; defaults to `false`.
+ */
+ $value = apply_filters( "tribe_context_pre_{$key}", null, $key, $default, $force );
+ if ( null !== $value ) {
+ return $value;
+ }
+
+ $value = $default;
+ $locations = $this->get_locations();
+ $found = false;
+
+ if ( ! $force && isset( $this->request_cache[ $key ] ) ) {
+ $value = $this->request_cache[ $key ];
+ } elseif ( ! empty( $locations[ $key ]['read'] ) ) {
+ foreach ( $locations[ $key ]['read'] as $location => $keys ) {
+ $the_value = $this->$location( (array) $keys, $default );
+
+ if ( $default !== $the_value && static::NOT_FOUND !== $the_value ) {
+ $found = true;
+ $value = $the_value;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Filters the value fetched from the context for a key.
+ *
+ * Useful for testing and local override.
+ *
+ * @since 4.9.5
+ *
+ * @param mixed $value The value as fetched from the context.
+ */
+ $value = apply_filters( "tribe_context_{$key}", $value );
+
+ // Only cache if the value was found.
+ if ( $found ) {
+ $this->request_cache[ $key ] = $value;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Alters the context.
+ *
+ * Due to its immutable nature setting values on the context will NOT modify the
+ * context but return a modified clone.
+ * If you need to modify the global context update the location(s) it should read from
+ * and call the `refresh` method.
+ * Example: `$widget_context = tribe_context()->alter( $widget_args );`.
+ *
+ * @since 4.9.5
+ *
+ * @param array $values An associative array of key-value pairs to modify the context.
+ *
+ * @return \Tribe__Context A clone, with modified, values, of the context the method was called on.
+ */
+ public function alter( array $values ) {
+ $clone = clone $this;
+
+ $clone->request_cache = array_merge( $clone->request_cache, $values );
+
+ return $clone;
+ }
+
+ /**
+ * Clears the context cache forcing a re-fetch of the variables from the context.
+ *
+ * @since 4.9.5
+ *
+ * @param string $key An optional specific key to refresh, if passed only this key
+ * will be refreshed.
+ */
+ public function refresh( $key = null ) {
+ if ( null !== $key ) {
+ unset( $this->request_cache[ $key ] );
+ } else {
+ $this->request_cache = [];
+ }
+ }
+
+ /**
+ * Returns the read and write locations set on the context.
+ *
+ * @since 4.9.5
+ *
+ * @return array An array of read and write location in the shape of the `Tribe__Context::$locations` one,
+ * `[ => [ 'read' => , 'write' => ] ]`.
+ */
+ public function get_locations() {
+ $this->populate_locations();
+
+ $locations = $this->use_default_locations
+ ? array_merge( self::$locations, $this->override_locations )
+ : $this->override_locations;
+
+ if ( $this->use_default_locations ) {
+ /**
+ * Filters the locations registered in the Context.
+ *
+ * @since 4.10.2
+ *
+ * @param $locations array An array of read and write location in the shape of the `Tribe__Context::$locations` one,
+ * `[ => [ 'read' => , 'write' => ] ]`.
+ * @param $context Tribe__Context Current instance of the context.
+ */
+ $locations = apply_filters( 'tribe_context_locations', $locations, $this );
+ }
+
+ return $locations;
+ }
+
+ /**
+ * Reads the value from one or more $_REQUEST vars.
+ *
+ * @since 4.9.5
+ *
+ * @param array $request_vars The list of request vars to lookup, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function request_var( array $request_vars, $default ) {
+ $value = $default;
+
+ foreach ( $request_vars as $request_var ) {
+ $the_value = tribe_get_request_var( $request_var, self::NOT_FOUND );
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more global WP_Query object query variables.
+ *
+ * @since 4.9.5
+ *
+ * @param array $query_vars The list of query vars to look up, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function query_var( array $query_vars, $default ) {
+ $value = $default;
+
+ global $wp_query;
+
+ if ( ! $wp_query instanceof \WP_Query ) {
+ return $value;
+ }
+
+ foreach ( $query_vars as $query_var ) {
+ $the_value = $wp_query->get( $query_var, self::NOT_FOUND );
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more global WP_Query object properties.
+ *
+ * @since 4.9.5
+ *
+ * @param array $query_props The list of properties to look up, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function query_prop( array $query_props, $default ) {
+ $value = $default;
+
+ global $wp_query;
+ foreach ( $query_props as $query_prop ) {
+ $the_value = isset( $wp_query->{$query_prop} ) ? $wp_query->{$query_prop} : self::NOT_FOUND;
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one more more `tribe_option`s.
+ *
+ * @since 4.9.5
+ *
+ * @param array $tribe_options The list of `tribe_option`s to lookup, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function tribe_option( array $tribe_options, $default ) {
+ $value = $default;
+
+ foreach ( $tribe_options as $option_name ) {
+ $the_value = tribe_get_option( $option_name, self::NOT_FOUND );
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more options.
+ *
+ * @since 4.9.5
+ *
+ * @param array $options The list of options to lookup, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function option( array $options, $default ) {
+ $value = $default;
+
+ foreach ( $options as $option_name ) {
+ $the_value = get_option( $option_name, self::NOT_FOUND );
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more transients.
+ *
+ * @since 4.9.5
+ *
+ * @param array $transients The list of transients to lookup, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function transient( array $transients, $default ) {
+ $value = $default;
+
+ foreach ( $transients as $transient ) {
+ $the_value = get_transient( $transient );
+ if ( false !== $the_value ) {
+ $value = $the_value;
+ /*
+ * This will fail when the value is actually `false`.
+ */
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more constants.
+ *
+ * @since 4.9.5
+ *
+ * @param array $constants The list of constants to lookup, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function constant( array $constants, $default ) {
+ $value = $default;
+
+ foreach ( $constants as $constant ) {
+ $the_value = defined( $constant ) ? constant( $constant ) : self::NOT_FOUND;
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more global variable.
+ *
+ * @since 4.9.5
+ *
+ * @param array $global_vars The list of global variables to look up, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function global_var( array $global_vars, $default ) {
+ $value = $default;
+
+ foreach ( $global_vars as $var ) {
+ $the_value = isset( $GLOBALS[ $var ] ) ? $GLOBALS[ $var ] : self::NOT_FOUND;
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more class static properties.
+ *
+ * @since 4.9.5
+ *
+ * @param array $classes_and_props An associative array in the shape [ => ].
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function static_prop( array $classes_and_props, $default ) {
+ $value = $default;
+
+ foreach ( $classes_and_props as $class => $prop ) {
+ if ( class_exists( $class ) ) {
+ // PHP 5.2 compat, on PHP 5.3+ $class::$$prop
+ $vars = get_class_vars( $class );
+ $the_value = isset( $vars[ $prop ] ) ? $vars[ $prop ] : self::NOT_FOUND;
+
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more properties of implementations bound in the `tribe()` container.
+ *
+ * @since 4.9.5
+ *
+ * @param array $bindings_and_props An associative array in the shape [ => ].
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ protected function prop( array $bindings_and_props, $default ) {
+ $value = $default;
+
+ foreach ( $bindings_and_props as $binding => $prop ) {
+ $the_value = tribe()->offsetExists( $binding ) && property_exists( tribe( $binding ), $prop )
+ ? tribe( $binding )->{$prop}
+ : self::NOT_FOUND;
+
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the values from one or more static class methods.
+ *
+ * @since 4.9.5
+ *
+ * @param array $classes_and_methods An associative array in the shape [ => ].
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first value that's not equal to the default one, the default value
+ * otherwise.
+ */
+ protected function static_method( array $classes_and_methods, $default ) {
+ $value = $default;
+
+ foreach ( $classes_and_methods as $class => $method ) {
+ $the_value = class_exists( $class ) && method_exists( $class, $method )
+ ? call_user_func( [ $class, $method ] )
+ : self::NOT_FOUND;
+
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more methods called on implementations bound in the `tribe()` container.
+ *
+ * @since 4.9.5
+ *
+ * @param array $bindings_and_methods An associative array in the shape [ => ].
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first value that's not equal to the default one, the default value
+ * otherwise.
+ */
+ protected function method( array $bindings_and_methods, $default ) {
+ $value = $default;
+ $the_value = self::NOT_FOUND;
+
+ foreach ( $bindings_and_methods as $binding => $method ) {
+ if ( tribe()->offsetExists( $binding ) ) {
+ $implementation = tribe( $binding );
+ if ( method_exists( $implementation, $method ) ) {
+ $the_value = $implementation->$method();
+ }
+ }
+
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reads the value from one or more functions until one returns a value that's not the default one.
+ *
+ * @since 4.9.5
+ *
+ * @param array $functions An array of functions to call, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first value that's not equal to the default one, the default value
+ * otherwise.
+ */
+ protected function func( array $functions, $default ) {
+ $value = $default;
+ $the_value = self::NOT_FOUND;
+
+ foreach ( $functions as $function ) {
+ if ( is_callable( $function ) || function_exists( $function ) ) {
+ $the_value = $function();
+ }
+
+ if ( $the_value !== self::NOT_FOUND ) {
+ $value = $the_value;
+ break;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Modifies the global context using the defined write locations to persist the altered values.
+ *
+ * Please keep in mind this will set the the global context for the whole request and, when the
+ * write location is an option, to the database.
+ * With great power comes great responsibility: think a lot before using this.
+ *
+ * @param array|null $fields An optional whitelist or blacklist of fields to write
+ * depending on the value of the `$whitelist` parameter;
+ * defaults to writing all available fields.
+ * @param bool $whitelist Whether the list of fields provided in the `$fields`
+ * parameter should be treated as a whitelist (`true`) or
+ * blacklist (`false`).
+ *
+ * @since 4.9.5
+ */
+ public function dangerously_set_global_context( array $fields = null, $whitelist = true ) {
+ $locations = $this->get_locations();
+
+ if ( null !== $fields ) {
+ $locations = $whitelist
+ ? array_intersect_key( $locations, array_combine( $fields, $fields ) )
+ : array_diff_key( $locations, array_combine( $fields, $fields ) );
+ }
+
+ /**
+ * Here we intersect with the request cache to only write values we've actually read
+ * or modified. If none of the two happened then there's no need to write anything.
+ */
+ foreach ( array_intersect_key( $this->request_cache, $locations ) as $key => $value ) {
+ if ( ! isset( $locations[ $key ]['write'] ) ) {
+ continue;
+ }
+
+ foreach ( (array) $locations[ $key ]['write'] as $location => $targets ) {
+ $targets = (array) $targets;
+ $write_func = 'write_' . $location;
+
+ foreach ( $targets as $arg_1 => $arg_2 ) {
+ if ( self::FUNC === $location && is_array( $arg_2 ) && is_callable( $arg_2 ) ) {
+ // Handles write functions specified as an array.
+ $location_args = [ $arg_2 ];
+ } else {
+ $location_args = in_array( $location, self::$associative_locations, true )
+ ? [ $arg_1, $arg_2 ]
+ : (array) $arg_2;
+ }
+
+ $args = array_merge( $location_args, [ $value ] );
+
+ call_user_func_array( [ $this, $write_func ], $args );
+ }
+ }
+ }
+ }
+
+ /**
+ * Writes an altered context value to a request var.
+ *
+ * @since 4.9.5
+ *
+ * @param string $request_var The request var to write.
+ * @param mixed $value The value to set on the request var.
+ */
+ protected function write_request_var( $request_var, $value ) {
+ if ( isset( $_REQUEST ) ) {
+ $_REQUEST[ $request_var ] = $value;
+ }
+ if ( isset( $_GET ) ) {
+ $_GET[ $request_var ] = $value;
+ }
+ if ( isset( $_POST ) ) {
+ $_POST[ $request_var ] = $value;
+ }
+ }
+
+ /**
+ * Writes an altered context value to a global WP_Query object properties.
+ *
+ * @since 4.9.5
+ *
+ * @param string $query_prop The global WP_Query object property to write.
+ * @param mixed $value The value to set on the query property.
+ */
+ protected function write_query_prop( $query_prop, $value ) {
+ global $wp_query;
+
+ if ( ! $wp_query instanceof WP_Query ) {
+ return;
+ }
+
+ $wp_query->{$query_prop} = $value;
+ }
+
+ /**
+ * Writes an altered context value to a global WP_Query object query var.
+ *
+ * @since 4.9.5
+ *
+ * @param string $query_var The global WP_Query query var to write.
+ * @param mixed $value The value to set on the query var.
+ */
+ protected function write_query_var( $query_var, $value ) {
+ global $wp_query;
+
+ if ( ! $wp_query instanceof WP_Query ) {
+ return;
+ }
+
+ $wp_query->set( $query_var, $value );
+ }
+
+ /**
+ * Writes an altered context value to a `tribe_option`.
+ *
+ * @since 4.9.5
+ *
+ * @param string $tribe_option The `tribe_option` to write.
+ * @param mixed $value The value to set on the `tribe_option`.
+ */
+ protected function write_tribe_option( $tribe_option, $value ) {
+ tribe_update_option( $tribe_option, $value );
+ }
+
+ /**
+ * Writes an altered context value to an option.
+ *
+ * @since 4.9.5
+ *
+ * @param string $option_name The option to write.
+ * @param mixed $value The value to set on the option.
+ */
+ protected function write_option( $option_name, $value ) {
+ update_option( $option_name, $value );
+ }
+
+ /**
+ * Writes an altered context value to a transient.
+ *
+ * @since 4.9.5
+ *
+ * @param string $transient The transient to write.
+ * @param int $expiration The transient expiration time, in seconds.
+ * @param mixed $value The value to set on the transient.
+ */
+ protected function write_transient( $transient, $expiration, $value ) {
+ set_transient( $transient, $value, $expiration );
+ }
+
+ /**
+ * Writes an altered context value to a constant.
+ *
+ * @since 4.9.5
+ *
+ * @param string $constant The constant to define.
+ * @param mixed $value The value to set on the constant.
+ */
+ protected function write_constant( $constant, $value ) {
+ if ( defined( $constant ) ) {
+ return;
+ }
+ define( $constant, $value );
+ }
+
+ /**
+ * Writes an altered context value to a global var.
+ *
+ * @since 4.9.5
+ *
+ * @param string $global_var The global var to set.
+ * @param mixed $value The value to set on the global_var.
+ */
+ protected function write_global_var( $global_var, $value ) {
+ $GLOBALS[ $global_var ] = $value;
+ }
+
+ /**
+ * Writes an altered context value setting a public static property on a class.
+ *
+ * @since 4.9.5
+ *
+ * @param string $class The class to set the static public property on.
+ * @param string $prop The static public property to set.
+ * @param mixed $value The value to set on the property.
+ */
+ protected function write_static_prop( $class, $prop, $value ) {
+ if ( ! ( class_exists( $class ) && property_exists( $class, $prop ) ) ) {
+ return;
+ }
+
+ $class::$$prop = $value;
+ }
+
+ /**
+ * Writes an altered context value setting a public property on a `tribe()` binding.
+ *
+ * @since 4.9.5
+ *
+ * @param string $binding The container binding to set the public property on.
+ * @param string $prop The public property to set.
+ * @param mixed $value The value to set on the property.
+ */
+ protected function write_prop( $binding, $prop, $value ) {
+ if ( ! tribe()->offsetExists( $binding ) ) {
+ return;
+ }
+
+ $implementation = tribe( $binding );
+
+ if ( ! property_exists( $implementation, $prop ) ) {
+ return;
+ }
+
+ $implementation->{$prop} = $value;
+ }
+
+ /**
+ * Writes an altered context value calling a public static method on a class.
+ *
+ * @since 4.9.5
+ *
+ * @param string $class The class to call the public static method on.
+ * @param string $method The static method to call.
+ * @param mixed $value The value to pass to the public static method.
+ */
+ protected function write_static_method( $class, $method, $value ) {
+ if ( ! class_exists( $class ) ) {
+ return;
+ }
+ call_user_func( [ $class, $method ], $value );
+ }
+
+ /**
+ * Writes an altered context value calling a public method on a `tribe()` binding.
+ *
+ * @since 4.9.5
+ *
+ * @param string $binding The `tribe()` container binding to call the public method on.
+ * @param string $method The method to call.
+ * @param mixed $value The value to pass to the public method.
+ */
+ protected function write_method( $binding, $method, $value ) {
+ if ( ! tribe()->offsetExists( $binding ) ) {
+ return;
+ }
+ call_user_func( [ tribe( $binding ), $method ], $value );
+ }
+
+ /**
+ * Writes an altered context value calling a function or closure.
+ *
+ * @since 4.9.5
+ *
+ * @param callable $func function, closure or callable to call.
+ * @param mixed $value The value to pass to the callable.
+ */
+ protected function write_func( $func, $value ) {
+ if ( ! is_callable( $func ) ) {
+ return;
+ }
+ call_user_func( $func, $value );
+ }
+
+ /**
+ * Adds/replaces read and write locations to a context.
+ *
+ * Locations are merged with an `array_merge` call. To refine the locations get them first with the
+ * `get_locations` method.
+ *
+ * @since 4.9.5
+ *
+ * @param array $locations An array of read and write locations to add to the context.
+ * The array should have the same shape as the static `$locations`
+ * one: `[ => [ 'read' => , 'write' => ] ]`.
+ *
+ *
+ * @return \Tribe__Context A clone of the current context with the additional read and
+ * write locations added.
+ */
+ public function add_locations( array $locations ) {
+ $clone = clone $this;
+ $clone->override_locations = array_merge( $clone->override_locations, $locations );
+
+ return $clone;
+ }
+
+ /**
+ * Sets, replacing them, the locations used by this context.
+ *
+ *
+ * @since 4.9.5
+ *
+ * @param array $locations An array of locations to replace the current ones.
+ * @param bool $use_default_locations Whether the context should use the default
+ * locations defined in the static `$locations`
+ * property or not.
+ *
+ * @return \Tribe__Context A clone of the current context with modified locations.
+ */
+ public function set_locations( array $locations, $use_default_locations = true ) {
+ $clone = clone $this;
+ $clone->override_locations = $locations;
+ $clone->use_default_locations = (bool) $use_default_locations;
+
+ return $clone;
+ }
+
+ /**
+ * Returns an array representation of the context.
+ *
+ * @since 4.9.5
+ *
+ * @return array An associative array of the context keys and values.
+ */
+ public function to_array( ) {
+ $locations = array_keys( array_merge( $this->get_locations(), $this->request_cache ) );
+ $dump = [];
+
+ foreach ( $locations as $location ) {
+ $the_value = $this->get( $location, self::NOT_FOUND );
+
+ if ( self::NOT_FOUND === $the_value ) {
+ continue;
+ }
+
+ $dump[ $location ] = $the_value;
+ }
+
+ return $dump;
+ }
+
+ /**
+ * Returns the current context state in a format suitable to hydrate a Redux-like
+ * store on the front-end.
+ *
+ * This method is a filtered wrapper around the the `Tribe__Context::to_array` method to allow the
+ * customization of the format when producing a store-compatible state.
+ *
+ * @param array|null $fields An optional whitelist or blacklist of fields to include
+ * depending on the value of the `$whitelist` parameter;
+ * defaults to returning all available fields.
+ * @param bool $whitelist Whether the list of fields provided in the `$fields`
+ * parameter should be treated as a whitelist (`true`) or
+ * blacklist (`false`).
+ *
+ * @since 4.9.5
+ *
+ * @return array
+ */
+ public function get_state( array $fields = null, $whitelist = true ) {
+ $state = $this->to_array();
+ $is_global_context = tribe_context() === $this;
+
+ if ( null !== $fields ) {
+ $state = $whitelist
+ ? array_intersect_key( $state, array_combine( $fields, $fields ) )
+ : array_diff_key( $state, array_combine( $fields, $fields ) );
+ }
+
+ /**
+ * Filters the Redux store compatible state produced from the current context.
+ *
+ * @since 4.9.5
+ *
+ * @param array $state The Redux store compatible state produced from the current context.
+ * @param bool $is_global_context Whether the context producing the state is the global one
+ * or a modified clone of it.
+ * @param Tribe__Context The context object producing the state.
+ */
+ $state = apply_filters( 'tribe_context_state', $state, $is_global_context, $this );
+
+ if ( $is_global_context ) {
+ /**
+ * Filters the Redux store compatible state produced from the global context.
+ *
+ * While the `tribe_context_state` filter will apply to all contexts producing a
+ * state this filter will only apply to the global context.
+ *
+ * @since 4.9.5
+ *
+ * @param array $state The Redux store compatible state produced from the global context.
+ * @param Tribe__Context The global context object producing the state.
+ */
+ $state = apply_filters( 'tribe_global_context_state', $state, $this );
+ }
+
+ return $state;
+ }
+
+ /**
+ * Returns an array of ORM arguments generated from the current context values.
+ *
+ * @since 4.9.5
+ *
+ * @param array|null $fields An optional whitelist or blacklist of fields to include
+ * depending on the value of the `$whitelist` parameter;
+ * defaults to returning all available fields.
+ * @param bool $whitelist Whether the list of fields provided in the `$fields`
+ * parameter should be treated as a whitelist (`true`) or
+ * blacklist (`false`).
+ *
+ * @return array A map of ORM fields produced from the context current values.
+ */
+ public function get_orm_args( array $fields = null, $whitelist = true ) {
+ $locations = $this->get_locations();
+ $dump = $this->to_array();
+ $orm_args = [];
+ $is_global_context = tribe_context() === $this;
+
+ foreach ( $dump as $key => $value ) {
+ $alias = isset( $locations[ $key ]['orm_arg'] )
+ ? $locations[ $key ]['orm_arg']
+ : $key;
+
+ if ( false === $alias ) {
+ // Do not provide the variable as an ORM arg.
+ continue;
+ }
+
+ if ( isset( $locations[ $key ]['orm_transform'] ) ) {
+ $value = call_user_func( $locations[ $key ]['orm_transform'], $value );
+ }
+
+ $orm_args[ $alias ] = $value;
+ }
+
+ if ( null !== $fields ) {
+ /*
+ * Only keep wanted fields, the filtering is done on the resolved aliases,
+ * from the perspective of the client code that might ignore the source keys.
+ */
+ $orm_args = $whitelist
+ ? array_intersect_key( $orm_args, array_combine( $fields, $fields ) )
+ : array_diff_key( $orm_args, array_combine( $fields, $fields ) );
+ }
+
+ /**
+ * Filters the ORM arguments produced from the current context.
+ *
+ * @since 4.9.5
+ *
+ * @param array $orm_args The ORM args produced from the current context.
+ * @param bool $is_global_context Whether the context producing the ORM args is the global one
+ * or a modified clone of it.
+ * @param Tribe__Context The context object producing the ORM args.
+ */
+ $orm_args = apply_filters( 'tribe_context_orm_args', $orm_args, $is_global_context, $this );
+
+ if ( $is_global_context ) {
+ /**
+ * Filters the ORM arguments produced from the global context.
+ *
+ * While the `tribe_context_orm_args` filter will apply to all contexts producing ORM
+ * args this filter will only apply to the global context.
+ *
+ * @since 4.9.5
+ *
+ * @param array $orm_args The ORM args produced from the global context.
+ * @param Tribe__Context The global context object producing the ORM args.
+ */
+ $orm_args = apply_filters( 'tribe_global_context_orm_args', $orm_args, $this );
+ }
+
+ return $orm_args;
+ }
+
+ /**
+ * Sets some locations that can only be set at runtime.
+ *
+ * Using a flag locations are added only once per request.
+ *
+ * @since 4.9.8
+ */
+ protected function populate_locations() {
+ if ( static::$did_populate_locations ) {
+ return;
+ }
+
+ // To improve the class readability, and as a small optimization, locations are loaded from a file.
+ static::$locations = include __DIR__ . '/Context/locations.php';
+
+ /**
+ * Filters the locations registered in the Context.
+ *
+ * @since 4.9.8
+ *
+ * @param array $locations An array of locations registered on the Context object.
+ */
+ static::$locations = apply_filters( 'tribe_context_locations', static::$locations, $this );
+
+ static::$did_populate_locations = true;
+ }
+
+ /**
+ * Just dont...
+ * Unless you very specifically know what you are doing **DO NOT USE THIS METHOD**!
+ *
+ * Please keep in mind this will set force the context to repopulate all locations for the whole request, expensive
+ * and very dangerous overall since it could affect all this things we hold dear in the request.
+ *
+ * With great power comes great responsibility: think a lot before using this.
+ *
+ * @since 4.13.0
+ */
+ public function dangerously_repopulate_locations() {
+ static::$did_populate_locations = false;
+ $this->populate_locations();
+ }
+
+ /**
+ * Reads (gets) the value applying one or more filters.
+ *
+ * @since 4.9.8
+ *
+ * @param array $filters The list of filters to apply, in order.
+ * @param mixed $default The default value to return.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ public function filter( array $filters, $default ) {
+ foreach ( $filters as $filter ) {
+ $the_value = apply_filters( $filter, $default );
+ if ( $the_value !== $default ) {
+ return $the_value;
+ }
+ }
+
+ return $default;
+ }
+
+ /**
+ * Reads (gets) the value reading it from a query var parsed from the global `$wp` object.
+ *
+ * @since 4.9.8
+ *
+ * @param array $vars The list of variables to read, in order.
+ * @param mixed $default The default value to return if no variable was parsed.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ public function wp_parsed( array $vars, $default ) {
+ /** @var WP $wp */
+ global $wp;
+
+ if ( ! $wp instanceof WP || empty($wp->query_vars) ) {
+ return $default;
+ }
+
+ return Arr::get_first_set( (array) $wp->query_vars, $vars, $default );
+ }
+
+ /**
+ * Reads (gets) the value reading it from a query var parsed from the query matched by the global `$wp` object.
+ *
+ * @since 4.9.8
+ *
+ * @param array $vars The list of variables to read, in order.
+ * @param mixed $default The default value to return if no variable was parsed.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ public function wp_matched_query( array $vars, $default ) {
+ /** @var WP $wp */
+ global $wp;
+
+ if ( ! $wp instanceof WP || empty( $wp->matched_query ) ) {
+ return $default;
+ }
+
+ parse_str( $wp->matched_query, $query_vars );
+
+ return Arr::get_first_set( (array) $query_vars, $vars, $default );
+ }
+
+ /**
+ * Maps an input array to the corresponding read locations.
+ *
+ * The resulting array can be used as input for the `alter_values` method.
+ * The main use of this method is to leverage the Context knowledge of the read locations, and their types, to
+ * "translate" an array of values to an array of valid read sources. As an example this is useful to "translate"
+ * the locations to an array of query vars:
+ * $input = [ 'event_display' => 'some-view', 'event_date' => '2018-01-03' ];
+ * $query_args = tribe_context()->map_to_read( $input, Tribe__Context::REQUEST_VAR );
+ * $url = add_query_arg( $query_args, home_url() );
+ *
+ * @since 4.9.11
+ *
+ * @param array $input An associative array of values in the shape `[ => ]`;
+ * where `location` is the name of the location registered in the Context
+ * locations.
+ * @param string|array|null $types A white-list of read location types to include in the mapped output;
+ * `null`
+ * means all types are allowed.
+ * @param bool $passthru Whether to pass unknown locations in the output or not; if `false` then
+ * any input key that's not a context location will not appear in the output;
+ * defaults to `false` to remove unknown locations from the output.
+ *
+ * @return array An associative array in the shape `[ => ]`. Since some read
+ * locations could have multiple sources the number of elements in this array will likely NOT be the
+ * same as the number of elements in the input array. When a read location as more than 1 source then
+ * the value will be duplicated, in the output array, to both sources.
+ */
+ public function map_to_read( array $input, $types = null, $passthru = false ) {
+ $mapped = [];
+ $processed = [];
+ $types = null !== $types ? (array) $types : null;
+
+ $locations = $this->get_locations();
+
+ // Take the current read locations
+ foreach ( $locations as $key => $location ) {
+ if ( ! isset( $location['read'], $input[ $key ] ) ) {
+ continue;
+ }
+
+ $processed[] = $key;
+
+ foreach ( $location['read'] as $type => $name ) {
+ if ( null !== $types && ! in_array( $type, $types, true ) ) {
+ continue;
+ }
+
+ foreach ( (array) $name as $destination ) {
+ $mapped[ $destination ] = $input[ $key ];
+ }
+ }
+ }
+
+ if ( $passthru ) {
+ $mapped = array_merge(
+ $mapped,
+ array_diff_key( $input, array_keys( $locations ), array_combine( $processed, $processed ) )
+ );
+ }
+
+ ksort( $mapped );
+
+ return $mapped;
+ }
+
+ /**
+ * Translates sub-locations to their respective location key.
+ *
+ * This method leverages the inherent knowledge of aliases stored in the Context locations to "translate" a
+ * sub-location to its location key.
+ * E.g. assume the `car` location is `read` from the [ 'carriage', 'vehicle', 'transport_mean' ] query var; calling
+ * `$context->populate_aliases( [ 'vehicle' => 'hyunday' ], 'read', Context::QUERY_VAR )` would yield
+ * `[ 'car' => 'hyunday' ]`.
+ *
+ * @since 4.9.12
+ *
+ * @param array $values An associative array of value to use as "masters" to populate the aliases.
+ * @param string $type The type of Context location to use, e.g. `Tribe__Context::QUERY_VAR`.
+ * @param string $direction The direction to use for the location, one of `read` or `write`.
+ *
+ * @return array The original array, merged with the populated values.
+ */
+ public function translate_sub_locations( array $values, $type, $direction = 'read' ) {
+ if ( ! in_array( $direction, [ 'read', 'write' ], true ) ) {
+ throw new \InvalidArgumentException(
+ "Direction must be one of `read` or `write`; `{$direction}` is not valid."
+ );
+ }
+
+ $filled = [];
+ $locations = $this->get_locations();
+ $matching_locations = array_filter( $locations, static function ( $location ) use ( $type, $direction ) {
+ return isset( $location[ $direction ][ $type ] );
+ } );
+
+ foreach ( $matching_locations as $key => $location ) {
+ $entry = (array)$location[ $direction ][ $type ];
+ $found = array_intersect( array_keys( $values ), array_merge( $entry, [ $key ] ) );
+ if ( $found ) {
+ $filled[ $key ] = $values[ reset( $found ) ];
+ }
+ }
+
+ return $filled;
+ }
+
+ /**
+ * Convenience method to get and check if a location has a truthy value or not.
+ *
+ * @since 4.9.18
+ *
+ * @param string $flag_key The location to check.
+ * @param bool $default The default value to return if the location is not set.
+ *
+ * @return bool Whether the location has a truthy value or not.
+ */
+ public function is( $flag_key, $default = false ) {
+ $val = $this->get( $flag_key, $default );
+
+ return ! empty( $val ) || tribe_is_truthy( $val );
+ }
+
+ /**
+ * Reads the value from one callback, passing it the value of another Context location.
+ *
+ * @since 4.9.18
+ *
+ * @param array $location_and_callback An array of two elements: the location key and the callback to call on the
+ * location value. The callback will receive the location value as argument.
+ *
+ * @return mixed The return value of the callback, called on the location value.
+ */
+ public function location_func( array $location_and_callback ) {
+ list( $location, $callback ) = $location_and_callback;
+
+ return $callback( $this->get( $location ) );
+ }
+
+ /**
+ * Checks whether the current request is a REST API one or not.
+ *
+ * @since 4.9.20
+ *
+ * @return bool Whether the current request is a REST API one or not.
+ */
+ public function doing_rest() {
+ return defined( 'REST_REQUEST' ) && REST_REQUEST;
+ }
+
+ /**
+ * Reads the value from one or more global WP_Query object methods.
+ *
+ * @since 4.9.20
+ *
+ * @param array $query_vars The list of query methods to call, in order.
+ * @param mixed $default The default value to return if no method was defined on the global `WP_Query` object.
+ *
+ * @return mixed The first valid value found or the default value.
+ */
+ public function query_method( $methods, $default ) {
+ global $wp_query;
+ $found = $default;
+
+ foreach ( $methods as $method ) {
+ $this_value = $wp_query instanceof WP_Query && method_exists( $wp_query, $method )
+ ? call_user_func( [ $wp_query, $method ] )
+ : static::NOT_FOUND;
+
+ if ( static::NOT_FOUND !== $this_value ) {
+ return $this_value;
+ }
+ }
+
+ return $found;
+ }
+
+ /**
+ * Whether the current request is for a PHP-rendered initial state or not.
+ *
+ * This method is a shortcut to make sure we're not doing an AJAX, REST or Cron request.
+ *
+ * @since 4.9.20
+ *
+ * @return bool Whether the current request is for a PHP-rendered initial state or not.
+ */
+ public function doing_php_initial_state() {
+ return ! $this->doing_rest() && ! $this->doing_ajax() && ! $this->doing_cron();
+ }
+
+ /**
+ * Returns the first key, if there are many, that will be used to read a location.
+ *
+ * The type ar
+ *
+ * @since 4.9.20
+ *
+ * @param string $location The location to get the read key for.
+ * @param string|null $type The type of read location to return the key for; default to `static::REQUEST_VAR`.
+ *
+ * @return string Either the first key for the type of read location, or the input location if not found.
+ */
+ public function get_read_key_for( $location, $type = null ) {
+ $type = $type ?: static::REQUEST_VAR;
+ $locations = $this->get_locations();
+ if ( isset( $locations[ $location ]['read'][ $type ] ) ) {
+ $keys = (array) $locations[ $location ]['read'][ $type ];
+ return reset( $keys );
+ }
+
+ return $location;
+ }
+
+ /**
+ * Safely set the value of a group of locations.
+ *
+ * This method can only augment the context, without altering it; it can only add new values.
+ *
+ * @since 4.10.2
+ *
+ * @param array|string $values The values to set, if not already set or the key of the value to set, requires
+ * the `$value` to be passed.
+ * @param mixed|null $value The value to set for the key, this parameter will be ignored if the `$values_or_key`
+ * parameter is not a string.
+ */
+ public function safe_set( $values_or_key, $value = null ) {
+ $values = func_num_args() === 2
+ ? [ $values_or_key => $value ]
+ : $values_or_key;
+
+ foreach ( $values as $key => $val ) {
+ if ( static::NOT_FOUND !== $this->get( $key, static::NOT_FOUND ) ) {
+ continue;
+ }
+ $this->request_cache[ $key ] = $val;
+ }
+ }
+
+ /**
+ * Whether the current request is one to edit a list of the specified post types or not.
+ *
+ * The admin edit screen for a post type is the one that lists all the posts of that typ,
+ * it has the URL `/wp-admin/edit.php?post_type=`.
+ *
+ * @since 5.0.13
+ * @since 5.0.13 Extracted the logic to the `TEC\Common\Context\Post_Request_Type` class.
+ *
+ * @param string|array $post_type The post type or post types to check.
+ *
+ * @return bool Whether the current request is one to edit a list of the specified post types or not.
+ */
+ public function is_editing_posts_list( $post_type ): bool {
+ return $this->post_state->is_editing_post_list( $post_type );
+ }
+
+ /**
+ * Whether the current request is one to quick edit a single post of the specified post type or not.
+ *
+ * @since 5.0.13
+ *
+ * @param string|array $post_type The post type or post types to check.
+ *
+ * @return bool Whether the current request is one to quick edit a single post of the specified post type or not.
+ */
+ public function is_inline_editing_post( $post_type ): bool {
+ return $this->post_state->is_inline_editing_post( $post_type );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Context/locations.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context/locations.php
new file mode 100644
index 000000000..6a433dd97
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context/locations.php
@@ -0,0 +1,163 @@
+ [
+ 'read' => [
+ Tribe__Context::FUNC => static function () {
+ return get_the_ID();
+ }
+ ],
+ ],
+ 'permalink_structure' => [
+ 'read' => [
+ Tribe__Context::OPTION => [ 'permalink_structure' ],
+ ],
+ ],
+ 'plain_permalink' => [
+ 'read' => [
+ Tribe__Context::LOCATION_FUNC => [
+ 'permalink_structure',
+ static function( $struct ){
+ return empty( $struct );
+ },
+ ],
+ ],
+ ],
+ 'posts_per_page' => [
+ 'read' => [
+ Tribe__Context::REQUEST_VAR => 'posts_per_page',
+ Tribe__Context::OPTION => 'posts_per_page',
+ Tribe__Context::TRIBE_OPTION => [ 'posts_per_page', 'postsPerPage' ],
+ ],
+ 'write' => [
+ Tribe__Context::REQUEST_VAR => 'posts_per_page',
+ ],
+ ],
+ 'is_main_query' => [
+ 'read' => [
+ Tribe__Context::FUNC => static function () {
+ global $wp_query;
+
+ if ( empty( $wp_query ) ) {
+ return false;
+ }
+
+ if ( ! $wp_query instanceof WP_Query ) {
+ return false;
+ }
+
+ return $wp_query->is_main_query();
+ },
+ ],
+ 'write' => [
+ Tribe__Context::FUNC => static function () {
+ global $wp_query, $wp_the_query;
+ $wp_the_query = $wp_query;
+ },
+ ],
+ ],
+ 'paged' => [
+ 'read' => [
+ Tribe__Context::REQUEST_VAR => [ 'paged', 'page' ],
+ Tribe__Context::QUERY_VAR => [ 'paged', 'page' ],
+ ],
+ 'write' => [
+ Tribe__Context::REQUEST_VAR => 'paged',
+ Tribe__Context::QUERY_VAR => 'paged',
+ ],
+ ],
+ 'page' => [
+ 'read' => [
+ Tribe__Context::REQUEST_VAR => [ 'page', 'paged' ],
+ Tribe__Context::QUERY_VAR => [ 'page', 'paged' ],
+ ],
+ 'write' => [
+ Tribe__Context::REQUEST_VAR => 'page',
+ Tribe__Context::QUERY_VAR => 'page',
+ ],
+ ],
+ 'name' => [
+ 'read' => [
+ Tribe__Context::REQUEST_VAR => [ 'name', 'post_name' ],
+ Tribe__Context::WP_PARSED => [ 'name', 'post_name' ],
+ Tribe__Context::QUERY_VAR => [ 'name', 'post_name' ],
+ ],
+ 'write' => [
+ Tribe__Context::REQUEST_VAR => [ 'name', 'post_name' ],
+ Tribe__Context::QUERY_VAR => [ 'name', 'post_name' ],
+ ],
+ ],
+ 'post_type' => [
+ 'read' => [
+ Tribe__Context::FUNC => static function() {
+ $post_type_objs = get_post_types(
+ [
+ 'public' => true,
+ '_builtin' => false,
+ ],
+ 'objects'
+ );
+
+ foreach ( $post_type_objs as $post_type ) {
+ if ( empty( $post_type->query_var ) ) {
+ continue;
+ }
+
+ $url_value = tribe_get_request_var( $post_type->query_var, false );
+ if ( empty( $url_value ) ) {
+ continue;
+ }
+
+ return $post_type->name;
+ }
+
+ return Tribe__Context::NOT_FOUND;
+ },
+ Tribe__Context::QUERY_PROP => 'post_type',
+ Tribe__Context::QUERY_VAR => 'post_type',
+ Tribe__Context::REQUEST_VAR => 'post_type',
+ ],
+ ],
+ 'single' => [
+ 'read' => [ Tribe__Context::QUERY_METHOD => 'is_single' ]
+ ],
+ 'taxonomy' => [
+ 'read' => [
+ Tribe__Context::QUERY_PROP => [ 'taxonomy' ],
+ Tribe__Context::QUERY_VAR => [ 'taxonomy' ],
+ Tribe__Context::REQUEST_VAR => [ 'taxonomy' ],
+ ],
+ ],
+ 'post_tag' => [
+ 'read' => [
+ Tribe__Context::QUERY_PROP => [ 'post_tag', 'tag' ],
+ Tribe__Context::QUERY_VAR => [ 'post_tag', 'tag' ],
+ Tribe__Context::REQUEST_VAR => [ 'post_tag', 'tag' ],
+ ],
+ ],
+ 'bulk_edit' => [
+ 'read' => [
+ Tribe__Context::REQUEST_VAR => [ 'bulk_edit' ],
+ ],
+ ],
+ 'inline_save' => [
+ 'read' => [
+ Tribe__Context::FUNC => [
+ static function () {
+ return tribe_get_request_var( 'action', false ) === 'inline-save'
+ ? true
+ : Tribe__Context::NOT_FOUND;
+ }
+ ],
+ ],
+ ],
+];
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Cost_Utils.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cost_Utils.php
new file mode 100644
index 000000000..42a112a19
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cost_Utils.php
@@ -0,0 +1,542 @@
+get_cost_regex(), trim( $cost ) );
+ }
+
+ /**
+ * Returns the regular expression that shold be used to identify a valid
+ * cost string.
+ *
+ * @return string
+ */
+ public function get_cost_regex() {
+ $separators = '[\\' . implode( '\\', $this->get_separators() ) . ']?';
+ $cost_regex = '(' . $separators . '([\d]+)' . $separators . '([\d]*))';
+
+ /**
+ * Filters the regular expression that will be used to identify a valid cost
+ * string.
+ *
+ * @param string $cost_regex
+ *
+ * @deprecated 4.3 Use `tribe_cost_regex` instead
+ */
+ $cost_regex = apply_filters(
+ 'tribe_events_cost_regex', $cost_regex
+ );
+
+ /**
+ * Filters the regular expression that will be used to identify a valid cost
+ * string.
+ *
+ * @param string $cost_regex
+ */
+ $cost_regex = apply_filters( 'tribe_cost_regex', $cost_regex );
+
+ return $cost_regex;
+ }
+
+ /**
+ * Fetch the possible separators
+ *
+ * @return array
+ */
+ public function get_separators() {
+ $separators = [ ',', '.' ];
+
+ /**
+ * Filters the cost string possible separators, those must be only 1 char.
+ *
+ * @param array $separators Defaults to comma (",") and period (".")
+ */
+ return apply_filters( 'tribe_events_cost_separators', $separators );
+ }
+
+ /**
+ * If the cost is "0", call it "Free"
+ *
+ * @param int|float|string $cost Cost to analyze
+ *
+ * @return int|float|string
+ */
+ public function maybe_replace_cost_with_free( $cost ) {
+
+ $cost_with_period = $this->convert_decimal_separator( $cost );
+
+ if (
+ is_numeric( $cost_with_period )
+ && '0.00' === number_format( $cost_with_period, 2, '.', ',' )
+ ) {
+ return esc_html__( 'Free', 'tribe-common' );
+ }
+
+ return $cost;
+ }
+
+ /**
+ * Formats a cost with a currency symbol
+ *
+ * @param int|float|string $cost Cost to format
+ *
+ * return string
+ * @param int|WP_Post $event An event post ID or post object.
+ * @param string $currency_symbol
+ * @param string $currency_position Either "prefix" or "posfix"
+ *
+ * @return float|int|string
+ */
+ public function maybe_format_with_currency( $cost, $event = null, $currency_symbol = null, $currency_position = null ) {
+ // check if the currency symbol is desired, and it's just a number in the field
+ // be sure to account for european formats in decimals, and thousands separators
+ if ( is_numeric( str_replace( $this->get_separators(), '', $cost ) ) ) {
+ $reverse_position = null;
+ // currency_position often gets passed as null or an empty string.
+ if ( ! empty( $currency_position ) ) {
+ $reverse_position = 'prefix' === $currency_position ? false : true;
+ }
+
+ $cost = tribe_format_currency( $cost, $event, $currency_symbol, $reverse_position );
+ }
+
+ return $cost;
+ }
+
+ /**
+ * @param string $original_string_cost A string cost with or without currency symbol,
+ * e.g. `10 - 20`, `Free` or `2$ - 4$`.
+ * @param array|string $merging_cost A single string cost representation to merge or an array of
+ * string cost representations to merge, e.g. ['Free', 10, 20,
+ * 'Donation'] or `Donation`.
+ * @param bool $with_currency_symbol Whether the output should prepend the currency symbol to the
+ * numeric costs or not.
+ * @param array $sorted_mins An array of non numeric price minimums sorted smaller to larger,
+ * e.g. `['Really free', 'Somewhat free', 'Free with 3 friends']`.
+ * @param array $sorted_maxs An array of non numeric price maximums sorted smaller to larger,
+ * e.g. `['Donation min $10', 'Donation min $20', 'Donation min
+ * $100']`.
+ *
+ * @return string|array The merged cost range.
+ */
+ public function merge_cost_ranges( $original_string_cost, $merging_cost, $with_currency_symbol, $sorted_mins = [], $sorted_maxs = [] ) {
+ if ( empty( $merging_cost ) || $original_string_cost === $merging_cost ) {
+ return $original_string_cost;
+ }
+
+ $_merging_cost = array_map(
+ [ $this, 'convert_decimal_separator' ], (array) $merging_cost
+ );
+ $_merging_cost = array_map( [ $this, 'numerize_numbers' ], $_merging_cost );
+ $numeric_merging_cost_costs = array_filter( $_merging_cost, 'is_numeric' );
+
+ $matches = [];
+ preg_match_all(
+ '!\d+(?:([' . preg_quote( $this->_supported_decimal_separators ) . '])\d+)?!', $original_string_cost,
+ $matches
+ );
+ $this->_current_original_cost_separator = empty( $matches[1][0] ) ? '.' : $matches[1][0];
+ $matches[0] = empty( $matches[0] )
+ ? $matches[0]
+ : array_map(
+ [
+ $this,
+ 'convert_decimal_separator',
+ ],
+ $matches[0]
+ );
+
+ $numeric_orignal_costs = empty( $matches[0] ) ? $matches[0] : array_map(
+ 'floatval', $matches[0]
+ );
+
+ $all_numeric_costs = array_filter( array_merge( $numeric_merging_cost_costs, $numeric_orignal_costs ) );
+ $cost_min = $cost_max = false;
+
+ $merging_mins = array_intersect( $sorted_mins, (array) $merging_cost );
+ $merging_has_min = array_search( reset( $merging_mins ), $sorted_mins );
+ $original_has_min = array_search( $original_string_cost, $sorted_mins );
+ $merging_has_min = false === $merging_has_min ? 999 : $merging_has_min;
+ $original_has_min = false === $original_has_min ? 999 : $original_has_min;
+ $string_min_key = min( $merging_has_min, $original_has_min );
+ if ( array_key_exists( $string_min_key, $sorted_mins ) ) {
+ $cost_min = $sorted_mins[ $string_min_key ];
+ } else {
+ $cost_min = empty( $all_numeric_costs ) ? '' : min( $all_numeric_costs );
+ }
+
+ $merging_maxs = array_intersect( $sorted_maxs, (array) $merging_cost );
+ $merging_has_max = array_search( end( $merging_maxs ), $sorted_maxs );
+ $original_has_max = array_search( $original_string_cost, $sorted_maxs );
+ $merging_has_max = false === $merging_has_max ? - 1 : $merging_has_max;
+ $original_has_max = false === $original_has_max ? - 1 : $original_has_max;
+ $string_max_key = max( $merging_has_max, $original_has_max );
+ if ( array_key_exists( $string_max_key, $sorted_maxs ) ) {
+ $cost_max = $sorted_maxs[ $string_max_key ];
+ } else {
+ $cost_max = empty( $all_numeric_costs ) ? '' : max( $all_numeric_costs );
+ }
+
+ $cost = array_filter( [ $cost_min, $cost_max ] );
+
+ if ( $with_currency_symbol ) {
+ $formatted_cost = [];
+ foreach ( $cost as $c ) {
+ $formatted_cost[] = is_numeric( $c ) ? tribe_format_currency( $c ) : $c;
+ }
+ $cost = $formatted_cost;
+ }
+
+ return empty( $cost ) ? $original_string_cost : array_map(
+ [ $this, 'restore_original_decimal_separator' ],
+ $cost
+ );
+ }
+
+ /**
+ * Returns a maximum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched
+ * via query.
+ *
+ * @param $costs mixed Cost(s) to review for max value
+ *
+ * @return float
+ */
+ public function get_maximum_cost( $costs = null ) {
+ return $this->get_cost_by_func( $costs, 'max' );
+ }
+
+ /**
+ * Returns a particular cost within an array of costs
+ *
+ * @param $costs mixed Cost(s) to review for max value
+ * @param $function string Function to use to determine which cost to return from range. Valid values: max, min
+ *
+ * @return float
+ */
+ protected function get_cost_by_func( $costs = null, $function = 'max' ) {
+ if ( null === $costs ) {
+ $costs = $this->get_all_costs();
+ } else {
+ $costs = (array) $costs;
+ }
+
+ $costs = $this->parse_cost_range( $costs );
+
+ // if there's only one item, we're looking at a single event. If the cost is non-numeric, let's
+ // return the non-numeric cost so that value is preserved
+ if ( 1 === count( $costs ) && ! is_numeric( current( $costs ) ) ) {
+ return current( $costs );
+ }
+
+ // make sure we are only trying to get numeric min/max values
+ $costs = array_filter( $costs, 'is_numeric' );
+
+ if ( empty( $costs ) ) {
+ return 0;
+ }
+
+ switch ( $function ) {
+ case 'min':
+ $cost = $costs[ min( array_keys( $costs ) ) ];
+ break;
+ case 'max':
+ default:
+ $cost = $costs[ max( array_keys( $costs ) ) ];
+ break;
+ }
+
+ // If there isn't anything on the cost just return 0
+ if ( empty( $cost ) ) {
+ return 0;
+ }
+
+ return $cost;
+ }
+
+ /**
+ * Parses a cost into an array of ranges.
+ *
+ * If a range isn't provided, the resulting array will hold a single
+ * value.
+ *
+ * @param string|array $costs A cost string or an array of cost strings.
+ * @param null $max_decimals The maximum number of decimal values that should be returned in the range.
+ * @param bool $sort Whether the returned values should be sorted.
+ *
+ * @return array An associative array of parsed costs in [ => ] format.
+ */
+ public function parse_cost_range( $costs, $max_decimals = null, $sort = true ) {
+ if ( ! is_array( $costs ) && ! is_string( $costs ) ) {
+ return [];
+ }
+
+ // make sure costs is an array
+ $costs = (array) $costs;
+
+ // If there aren't any costs, return a blank array
+ if ( 0 === count( $costs ) ) {
+ return [];
+ }
+
+ // Build the regular expression
+ $price_regex = $this->get_cost_regex();
+ $max = 0;
+
+ foreach ( $costs as &$cost ) {
+ // Get the required parts
+ if ( preg_match_all( '/' . $price_regex . '/', $cost, $matches ) ) {
+ $cost = reset( $matches );
+ } else {
+ $cost = [ $cost ];
+ continue;
+ }
+
+ // Get the max number of decimals for the range
+ if ( count( $matches ) === 4 ) {
+ $decimals = max( array_map( 'strlen', end( $matches ) ) );
+ $max = max( $max, $decimals );
+ }
+ }
+
+ // If we passed max decimals
+ if ( ! is_null( $max_decimals ) ) {
+ $max = max( $max_decimals, $max );
+ }
+
+ $output_costs = [];
+ $costs = call_user_func_array( 'array_merge', array_values( $costs ) );
+
+ foreach ( $costs as $cost ) {
+ $numeric_cost = str_replace( $this->get_separators(), '.', $cost );
+
+ if ( is_numeric( $numeric_cost ) ) {
+ // Creates a Well Balanced Index that will perform good on a Key Sorting method
+ $index = str_replace( [ '.', ',' ], '', number_format( $numeric_cost, $max ) );
+ } else {
+ // Makes sure that we have "index-safe" string
+ $index = sanitize_title( $numeric_cost );
+ }
+
+ // Keep the Costs in a organizable array by keys with the "numeric" value
+ $output_costs[ $index ] = $cost;
+ }
+
+ // Filter keeping the Keys
+ if ( $sort ) {
+ ksort( $output_costs );
+ }
+
+ return (array) $output_costs;
+ }
+
+ /**
+ * Returns a minimum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched
+ * via query.
+ *
+ * @param $costs mixed Cost(s) to review for min value
+ *
+ * @return float
+ */
+ public function get_minimum_cost( $costs = null ) {
+ return $this->get_cost_by_func( $costs, 'min' );
+ }
+
+ /**
+ * Converts the original decimal separator to ".".
+ *
+ * @param string|int $value
+ *
+ * @return string
+ */
+ protected function convert_decimal_separator( $value ) {
+ return preg_replace( '/[' . preg_quote( $this->_supported_decimal_separators ) . ']/', '.', $value );
+ }
+
+ /**
+ * Restores the decimal separator to its original symbol.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ protected function restore_original_decimal_separator( $value ) {
+ return str_replace( '.', $this->_current_original_cost_separator, $value );
+ }
+
+ /**
+ * Extracts int and floats from a numeric "dirty" string like strings that might contain other symbols.
+ *
+ * E.g. "$10" will yield "10"; "23.55$" will yield "23.55".
+ *
+ * @param string|int $value
+ *
+ * @return int|float
+ */
+ protected function numerize_numbers( $value ) {
+ $matches = [];
+
+ $pattern = '/(\\d{1,}([' . $this->_supported_decimal_separators . ']\\d{1,}))/';
+
+ return preg_match( $pattern, $value, $matches ) ? $matches[1] : $value;
+ }
+
+ /**
+ * Parses the currency symbol part of a cost string.
+ *
+ * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs.
+ *
+ * @return false|string Either the inferred currency symbol or `false` if the currency symbol is missing or not consistent.
+ */
+ public function parse_currency_symbol( $cost ) {
+ if ( empty( $cost ) ) {
+ return false;
+ }
+
+ $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost );
+ $costs = $this->parse_cost_range( $original_costs, null, false );
+
+ if ( empty( $costs ) ) {
+ return false;
+ }
+
+ $currency_symbols = [];
+ $i = 0;
+ foreach ( $costs as $string => $value ) {
+ if ( is_numeric( $string ) ) {
+ $currency_symbols[] = trim( str_replace( $value, '', $original_costs[ $i ] ) );
+ if ( end( $currency_symbols ) !== reset( $currency_symbols ) ) {
+ return false;
+ }
+ }
+
+ $i ++;
+ }
+
+ return ! empty( $currency_symbols ) ? reset( $currency_symbols ) : false;
+ }
+
+ /**
+ * Parses the currency symbol position part of a cost string.
+ *
+ * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs.
+ *
+ * @return false|string Either the inferred currency symbol position or `false` if not present or not consistent.
+ */
+ public function parse_currency_position( $cost ) {
+ if ( empty( $cost ) ) {
+ return false;
+ }
+
+ $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost );
+ $currency_symbol = $this->parse_currency_symbol( $original_costs );
+
+ if ( empty( $currency_symbol ) ) {
+ return false;
+ }
+
+ $currency_positions = [];
+ foreach ( $original_costs as $original_cost ) {
+ $currency_symbol_position = strpos( trim( $original_cost ), $currency_symbol );
+ if ( false === $currency_symbol_position ) {
+ continue;
+ }
+
+ $currency_positions[] = 0 === $currency_symbol_position ? 'prefix' : 'postfix';
+ if ( end( $currency_positions ) !== reset( $currency_positions ) ) {
+ return false;
+ }
+ }
+
+ return ! empty( $currency_positions ) ? reset( $currency_positions ) : false;
+ }
+
+ /**
+ * Parses the cost value and current locale to infer decimal and thousands separators.
+ *
+ * The cost values stored in the meta table might not use the same decimal and thousands separator as the current
+ * locale.
+ * To work around this we parse the value assuming the decimal separator will be the last non-numeric symbol,
+ * if any.
+ *
+ * @since 4.9.12
+ *
+ * @param string|int|float $value The cost value to parse.
+ *
+ * @return array An array containing the parsed decimal and thousands separator symbols.
+ */
+ public function parse_separators( $value ) {
+ global $wp_locale;
+ $locale_decimal_point = $wp_locale->number_format['decimal_point'];
+ $locale_thousands_sep = $wp_locale->number_format['thousands_sep'];
+ $decimal_sep = $locale_decimal_point;
+ $thousands_sep = $locale_thousands_sep;
+
+ preg_match_all( '/[\\.,]+/', $value, $matches );
+
+ if ( ! empty( $matches[0] ) ) {
+ $matched_separators = $matches[0];
+ if ( count( array_unique( $matched_separators ) ) > 1 ) {
+ // We have both, the decimal separator will be the last non-numeric symbol.
+ $decimal_sep = end( $matched_separators );
+ $thousands_sep = reset( $matched_separators );
+ } else {
+ /*
+ * We only have one, we can assume it's the decimal separator if it comes before a number of numeric
+ * symbols that is not exactly 3. If there are exactly 3 number after the symbols we fall back on the
+ * locale; we did our best and cannot guess any further.
+ */
+ $frags = explode( end( $matched_separators ), $value );
+ if ( strlen( end( $frags ) ) !== 3 ) {
+ $decimal_sep = end( $matched_separators );
+ $thousands_sep = $decimal_sep === $locale_decimal_point ?
+ $locale_thousands_sep
+ : $locale_decimal_point;
+ }
+ }
+ }
+
+ return [ $decimal_sep, $thousands_sep ];
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Credits.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Credits.php
new file mode 100644
index 000000000..c87bc1f51
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Credits.php
@@ -0,0 +1,116 @@
+hook();
+ }
+
+ /**
+ * Hook the functionality of this class into the world
+ */
+ public function hook() {
+ add_filter( 'tribe_events_after_html', [ $this, 'html_comment_credit' ] );
+ add_filter( 'admin_footer_text', [ $this, 'rating_nudge' ], 1, 2 );
+ }
+
+ /**
+ * Add credit in HTML page source
+ *
+ * @return void
+ **/
+ public function html_comment_credit( $after_html ) {
+
+ if ( ! class_exists( 'Tribe__Events__Main' ) ) {
+ return $after_html;
+ }
+
+ $html_credit = "\n\n";
+ $after_html .= apply_filters( 'tribe_html_credit', $html_credit );
+ return $after_html;
+ }
+
+ /**
+ * Add ratings nudge in admin footer
+ *
+ * @param $footer_text
+ *
+ * @return string
+ */
+ public function rating_nudge( $footer_text ) {
+ $admin_helpers = Tribe__Admin__Helpers::instance();
+
+ add_filter( 'tribe_tickets_post_types', [ $this, 'tmp_return_tribe_events' ], 99 );
+
+ $review_text_tec = esc_html__( 'Rate %1$sThe Events Calendar%2$s %3$s', 'tribe-common' );
+ $review_url_tec = 'https://wordpress.org/support/plugin/the-events-calendar/reviews/?filter=5';
+
+ $review_text_et = esc_html__( 'If you like %1$sEvent Tickets%2$s please leave us a %3$s. It takes a minute and it helps a lot.', 'tribe-common' );
+ $review_url_et = 'https://wordpress.org/support/plugin/event-tickets/reviews/?filter=5';
+
+ // Only display custom text on Tribe Admin Pages.
+ if ( $admin_helpers->is_screen() || $admin_helpers->is_post_type_screen() ) {
+
+ if ( class_exists( 'Tribe__Events__Main' ) ) {
+ // If we have TEC and ET, split the impressions 50/50.
+ if ( class_exists( 'Tribe__Tickets__Main' ) && wp_rand( 0,1 ) ) {
+ $review_text = $review_text_et;
+ $review_url = $review_url_et;
+ } else {
+ $review_text = $review_text_tec;
+ $review_url = $review_url_tec;
+ }
+ } else {
+ $review_text = $review_text_et;
+ $review_url = $review_url_et;
+ }
+
+ $footer_text = sprintf(
+ $review_text,
+ '',
+ '',
+ '★★★★★'
+ );
+ }
+
+ remove_filter( 'tribe_tickets_post_types', [ $this, 'tmp_return_tribe_events' ], 99 );
+
+ /**
+ * Filters the admin footer text.
+ *
+ * @since 4.15.0
+ *
+ * @param $footer_text The admin footer text.
+ */
+ return apply_filters( 'tec_admin_footer_text', $footer_text );
+ }
+
+ /**
+ * temporary function to filter event types down to only tribe-specific types
+ *
+ * This will limit the request for ratings to only those post type pages
+ */
+ public function tmp_return_tribe_events( $unused_post_types ) {
+ return [ 'tribe_events' ];
+ }
+
+ /**
+ * @var $instance
+ */
+ private static $instance = null;
+
+ /**
+ * @return self
+ */
+ public static function instance() {
+ if ( empty( self::$instance ) ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer.php
new file mode 100644
index 000000000..a153ded51
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer.php
@@ -0,0 +1,1073 @@
+is_active() ) {
+ return;
+ }
+
+ /**
+ * Filters the Panel ID, which is also the `wp_option` name for the Customizer settings
+ *
+ * @since 4.4
+ *
+ * @param string $ID
+ * @param self $customizer
+ */
+ $this->ID = apply_filters( 'tribe_customizer_panel_id', 'tribe_customizer', $this );
+
+ // Hook the Registering methods
+ add_action( 'customize_register', [ $this, 'register' ], 15 );
+
+ add_action( 'wp_print_footer_scripts', [ $this, 'print_css_template' ], 15 );
+ add_action( 'customize_controls_print_footer_scripts', [ $this, 'customize_controls_print_footer_scripts' ], 15 );
+
+ // front end styles from customizer
+ add_action( 'tribe_events_pro_widget_render', [ $this, 'inline_style' ], 101 );
+ add_action( 'wp_print_footer_scripts', [ $this, 'shortcode_inline_style' ], 10 );
+ add_action( 'wp_print_footer_scripts', [ $this, 'widget_inline_style' ], 10 );
+
+ /**
+ * Allows filtering the action that will be used to trigger the printing of inline scripts.
+ *
+ * By default inline scripts will be printed on the `wp_enqueue_scripts` action, but other
+ * plugins or later iterations might require inline styles to be printed on other actions.
+ *
+ * @since 4.12.15
+ *
+ * @param string $inline_script_action_handle The handle of the action that will be used to try
+ * and attempt to print inline scripts.
+ */
+ $print_styles_action = apply_filters( 'tribe_customizer_print_styles_action', 'wp_enqueue_scripts' );
+
+ add_action( $print_styles_action, [ $this, 'inline_style' ], 15 );
+
+ add_filter( "default_option_{$this->ID}", [ $this, 'maybe_fallback_get_option' ] );
+ }
+
+ /**
+ * Backwards compatibility for the old Customizer Option Save
+ *
+ * @since 4.4
+ *
+ * @param mixed $sections
+ *
+ * @return mixed
+ */
+ public function maybe_fallback_get_option( $sections ) {
+ // Return if there is something there
+ if ( ! empty( $sections ) ) {
+ return $sections;
+ }
+
+ return get_option( 'tribe_events_pro_customizer', [] );
+ }
+
+ /**
+ * Loads a Section to the Customizer on The Events Calendar's Panel
+ *
+ * @since 4.4
+ *
+ * @param object $section An Object that extends the Abstract `Tribe__Customizer__Section`
+ *
+ * @return bool
+ */
+ public function load_section( $section ) {
+ // You can only add a section if it extends the abstract Section
+ if ( ! is_object( $section ) || ! in_array( 'Tribe__Customizer__Section', class_parents( $section ) ) ) {
+ return false;
+ }
+
+ // Add the Section
+ // Enforces the usage of `$instance->ID`
+ $this->sections_class[ $section->ID ] = $section;
+
+ return true;
+ }
+
+ /**
+ * Fetches all Section Classes
+ *
+ * @since 4.4
+ *
+ * @return array
+ */
+ public function get_loaded_sections() {
+ /**
+ * Allow developers to filter Classes from Customizer Sections
+ *
+ * @deprecated
+ * @since 4.0
+ *
+ * @param array $sections_class
+ * @param self $customizer
+ */
+ $this->sections_class = apply_filters( 'tribe_events_pro_customizer_sections_class', $this->sections_class, $this );
+
+ /**
+ * Allow developers to filter Classes from Customizer Sections
+ *
+ * @since 4.4
+ *
+ * @param array $sections_class
+ * @param self $customizer
+ */
+ $this->sections_class = apply_filters( 'tribe_customizer_sections_class', $this->sections_class, $this );
+
+ return $this->sections_class;
+ }
+
+ /**
+ * Returns the section requested by ID.
+ *
+ * @since 4.13.3
+ *
+ * @param string $id The ID of the desired section.
+ *
+ * @return boolean|Tribe__Customizer__Section The requested section or boolean false if not found.
+ */
+ public function get_section( $id ) {
+ $sections = $this->get_loaded_sections();
+
+ if ( empty( $sections[ $id ] ) ) {
+ return false;
+ }
+
+ return $sections[ $id ];
+ }
+
+ /**
+ * A easy way to check if customize is active
+ *
+ * @since 4.2.2
+ *
+ * @return boolean
+ */
+ public function is_active() {
+ /**
+ * Allows Developers to completely deactivate Events Calendar Customizer
+ *
+ * @deprecated
+ *
+ * @param boolean $is_active
+ */
+ $is_active = apply_filters( 'tribe_events_pro_customizer_is_active', true );
+
+ /**
+ * Allows Developers to completely deactivate Events Calendar Customizer
+ *
+ * @param boolean $is_active
+ */
+ return apply_filters( 'tribe_customizer_is_active', true );
+ }
+
+ /**
+ * A method to easily search on an array
+ *
+ * @since 4.0
+ *
+ * @param array $variable Variable to be searched
+ * @param array $indexes The index that the method will try to retrieve
+ * @param mixed $default If the variable doesn't exist, what is the default
+ *
+ * @return mixed Return the variable based on the index
+ */
+ public static function search_var( $variable = null, $indexes = [], $default = null ) {
+ if ( is_object( $variable ) ) {
+ $variable = (array) $variable;
+ }
+
+ if ( ! is_array( $variable ) ) {
+ return $variable;
+ }
+
+ foreach ( (array) $indexes as $index ) {
+ if ( ! is_array( $variable ) || ! isset( $variable[ $index ] ) ) {
+ $variable = $default;
+ break;
+ }
+
+ $variable = $variable[ $index ];
+ }
+
+ return $variable;
+ }
+
+ /**
+ * Get an option from the database, using index search you can retrieve the full panel, a section or even a setting.
+ *
+ * @since 4.4
+ *
+ * @param array $search Index search, array( 'section_name', 'setting_name' ).
+ * @param mixed $default The default, if the requested variable doesn't exits.
+ *
+ * @return mixed The requested option or the default.
+ */
+ public function get_option( $search = null, $default = null ) {
+ $sections = get_option( $this->ID, $default );
+
+ foreach ( $this->get_loaded_sections() as $section ) {
+ /**
+ * Allow filtering the defaults for each settings to be filtered before the Ghost options to be set
+ *
+ * @deprecated
+ * @since 4.0
+ *
+ * @param array $defaults
+ */
+ $defaults[ $section->ID ] = apply_filters( "tribe_events_pro_customizer_section_{$section->ID}_defaults", [] );
+
+ /**
+ * Allow filtering the defaults for each settings to be filtered before the Ghost options to be set
+ *
+ * @since 4.4
+ *
+ * @param array $defaults
+ */
+ $settings = isset( $sections[ $section->ID ] ) ? $sections[ $section->ID ] : [];
+ $defaults[ $section->ID ] = apply_filters( "tribe_customizer_section_{$section->ID}_defaults", $settings );
+ $sections[ $section->ID ] = wp_parse_args( $settings, $defaults[ $section->ID ] );
+ }
+
+ /**
+ * Allows Ghost Options to be inserted
+ *
+ * @deprecated
+ * @since 4.0
+ *
+ * @param array $sections
+ * @param array $search
+ */
+ $sections = apply_filters( 'tribe_events_pro_customizer_pre_get_option', $sections, $search );
+
+ /**
+ * Allows Ghost Options to be inserted
+ *
+ * @since 4.4
+ *
+ * @param array $sections
+ * @param array $search
+ */
+ $sections = apply_filters( 'tribe_customizer_pre_get_option', $sections, $search );
+
+ // Search on the Array
+ if ( ! is_null( $search ) ) {
+ $option = self::search_var( $sections, $search, $default );
+ } else {
+ $option = $sections;
+ }
+
+ /**
+ * Apply Filters After finding the variable
+ *
+ * @since 4.4
+ *
+ * @param mixed $option
+ * @param array $search
+ * @param array $sections
+ */
+ $option = apply_filters( 'tribe_customizer_get_option', $option, $search, $sections );
+
+ return $option;
+ }
+
+ /**
+ * Check if the option exists, this method is used allow only sections that were saved to be applied.
+ *
+ * @param strings Using the following structure: self::has_option( 'section_name', 'setting_name' );
+ *
+ * @return boolean Whether the option exists in the database
+ */
+ public function has_option() {
+ $search = func_get_args();
+ $option = self::get_option();
+ $real_option = get_option( $this->ID, [] );
+
+ // Get section and Settings based on keys
+ $section = reset( $search );
+ $setting = end( $search );
+
+ if ( empty( $real_option ) || empty( $real_option[ $section ] ) ) {
+ return false;
+ }
+
+ // Search on the Array
+ if ( ! is_null( $search ) ) {
+ $option = self::search_var( $option, $search, null );
+ }
+
+ return ! empty( $option );
+ }
+
+ /**
+ * Add an action for some backwards compatibility.
+ *
+ * @since 4.14.2
+ *
+ * @return void
+ */
+ public function customize_controls_print_footer_scripts() {
+ /**
+ * Allows plugins to hook in and add any scripts they need at the right time.
+ *
+ * @param Tribe__Customizer $customizer The current instance of Tribe__Customizer.
+ */
+ do_action( 'tribe_enqueue_customizer_scripts', $this );
+ }
+
+ /**
+ * Print the CSS for the customizer on `wp_print_footer_scripts`
+ *
+ * @since 4.12.6 Moved the template building code to the `get_styles_scripts` method.
+ *
+ * @return void
+ */
+ public function print_css_template() {
+
+ //Only load in Customizer
+ if ( ! is_customize_preview() ) {
+ return false;
+ }
+
+ echo $this->get_styles_scripts();
+ }
+
+ /**
+ * Print the CSS for the customizer for shortcodes.
+ *
+ * @since 4.12.6
+ */
+ public function shortcode_inline_style() {
+ /**
+ * Whether customizer styles should print for shortcodes or not.
+ *
+ * @since 4.12.6
+ *
+ * @param boolean $should_print Whether the inline styles should be printed on screen.
+ */
+ $should_print = apply_filters( 'tribe_customizer_should_print_shortcode_customizer_styles', false );
+
+ if ( empty( $should_print ) ) {
+ return;
+ }
+
+ $this->inline_style();
+ }
+
+ /**
+ * Print the CSS for the customizer for widgets.
+ *
+ * @since 4.12.14
+ */
+ public function widget_inline_style() {
+ /**
+ * Whether customizer styles should print for widgets or not.
+ *
+ * @since 4.12.14
+ *
+ * @param boolean $should_print Whether the inline styles should be printed on screen.
+ */
+ $should_print = apply_filters( 'tribe_customizer_should_print_widget_customizer_styles', false );
+
+ if ( empty( $should_print ) ) {
+ return;
+ }
+
+ $this->inline_style();
+ }
+
+ /**
+ * Print the CSS for the customizer using wp_add_inline_style
+ *
+ * @since 4.12.15 Added the `$force` parameter to force the print of the style inline.
+ *
+ * @param bool $force Whether to ignore the context to try and print the style inline, or not.
+ */
+ public function inline_style( $force = false ) {
+ // Only load once on front-end.
+ if ( ! $force && ( is_customize_preview() || is_admin() || $this->inline_style ) ) {
+ return false;
+ }
+
+ /**
+ * Use this filter to add more CSS, using Underscore Template style
+ *
+ * @since 4.4
+ *
+ * @link http://underscorejs.org/#template
+ *
+ * @param string $template
+ */
+ $css_template = trim( apply_filters( 'tribe_customizer_css_template', '' ) );
+
+ // If we don't have anything on the customizer don't print empty styles
+ if ( empty( $css_template ) ) {
+ return false;
+ }
+
+ $sheets = [
+ 'tribe-common-full-style',
+ ];
+
+ /**
+ * Allow plugins to add themselves to this list.
+ *
+ * @since 4.12.1
+ *
+ * @param array $sheets An array of sheets to search for.
+ * @param string $css_template String containing the inline css to add.
+ */
+ $sheets = apply_filters( 'tribe_customizer_inline_stylesheets', $sheets, $css_template );
+
+ if ( empty( $sheets ) ) {
+ return false;
+ }
+
+ // Add customizer styles inline with the latest stylesheet that is enqueued.
+ foreach ( array_reverse( $sheets ) as $sheet ) {
+ if ( wp_style_is( $sheet ) ) {
+ $inline_style = wp_strip_all_tags( $this->parse_css_template( $css_template ) );
+
+ /**
+ * Fires before a style is, possibly, printed inline depending on the stylesheet.
+ *
+ * @since 4.12.15
+ *
+ * @param string $sheet The handle of the stylesheet the style will be printed inline for.
+ * @param string $inline_style The inline style contents, as they will be printed on the page.
+ */
+ do_action( 'tribe_customizer_before_inline_style', $sheet, $inline_style );
+
+ // Just print styles if doing 'wp_print_footer_scripts' action.
+ $just_print = (bool) doing_action( 'wp_print_footer_scripts' );
+
+ if ( $just_print ) {
+ printf(
+ "\n",
+ esc_attr( $sheet ),
+ $inline_style
+ );
+ } else {
+ wp_add_inline_style( $sheet, $inline_style );
+ }
+
+ $this->inline_style = true;
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Replaces the Settings using the Underscore templating strings
+ *
+ * @param string $template The template variable, that we will look to replace the variables
+ * @return string A Valid css after replacing the variables
+ */
+ private function parse_css_template( $template ) {
+ $css = $template;
+ $sections = $this->get_option();
+
+ $search = [];
+ $replace = [];
+
+ foreach ( $sections as $section => $settings ) {
+ if ( ! is_array( $settings ) ) {
+ continue;
+ }
+ foreach ( $settings as $setting => $value ) {
+ $index = [ $section, $setting ];
+
+ // Add search based on Underscore template
+ $search[] = '<%= ' . implode( '.', $index ) . ' %>';
+
+ // Get the Replace value
+ $replace[] = $value;
+ }
+ }
+
+ // Finally Str replace
+ return str_replace( $search, $replace, $css );
+ }
+
+ /**
+ * Method to start setting up the Customizer Section and Fields
+ *
+ * @since 4.0
+ *
+ * @param WP_Customize_Manager $customizer WordPress Customizer variable
+ * @return void
+ */
+ public function register( WP_Customize_Manager $customizer ) {
+ // Set the Customizer on a class variable
+ $this->manager = $customizer;
+
+ /**
+ * Allow users to filter the Panel
+ *
+ * @since 4.4
+ *
+ * @param WP_Customize_Panel $panel
+ * @param Tribe__Customizer $customizer
+ */
+ $this->panel = apply_filters( 'tribe_customizer_panel', $this->register_panel(), $this );
+
+ /**
+ * Filter the Sections within our Panel before they are added to the Customize Manager
+ *
+ * @since 4.4
+ *
+ * @param array $sections
+ * @param Tribe__Customizer $customizer
+ */
+ $this->sections = apply_filters( 'tribe_customizer_pre_sections', $this->sections, $this );
+
+ foreach ( $this->sections as $id => $section ) {
+ $this->sections[ $id ] = $this->register_section( $id, $section );
+
+ /**
+ * Allows people to Register and de-register the method to register more Fields
+ *
+ * @since 4.4
+ * @since 4.12.15 Add Customizer instance as a parameter.
+ *
+ * @param array $section
+ * @param WP_Customize_Manager $manager
+ * @param Tribe__Customizer $customizer The current customizer instance.
+ */
+ do_action( "tribe_customizer_register_{$id}_settings", $this->sections[ $id ], $this->manager, $this );
+ }
+
+ /**
+ * Filter the Sections within our Panel, now using the actual WP_Customize_Section
+ *
+ * @since 4.4
+ *
+ * @param array $sections
+ * @param Tribe__Customizer $customizer
+ */
+ $this->sections = apply_filters( 'tribe_customizer_sections', $this->sections, $this );
+
+ // After everything is done, try to add Selective refresh
+ $this->maybe_selective_refresh();
+ }
+
+ /**
+ * Register the base Panel for Events Calendar Sections to be attached to
+ *
+ * @since 4.0
+ *
+ * @return WP_Customize_Panel
+ */
+ private function register_panel() {
+ $panel = $this->manager->get_panel( $this->ID );
+
+ // If the Panel already exists we leave returning it's instance
+ if ( ! empty( $panel ) ) {
+ return $panel;
+ }
+
+ $panel_args = [
+ 'title' => esc_html__( 'The Events Calendar', 'tribe-common' ),
+ 'description' => esc_html__( 'Use the following panel of your customizer to change the styling of your Calendar and Event pages.', 'tribe-common' ),
+
+ // After `static_front_page`
+ 'priority' => 125,
+ ];
+
+ /**
+ * Filter the Panel Arguments for WP Customize
+ *
+ * @since 4.4
+ *
+ * @param array $args
+ * @param string $ID
+ * @param Tribe__Customizer $customizer
+ */
+ $panel_args = apply_filters( 'tribe_customizer_panel_args', $panel_args, $this->ID, $this );
+
+ // Actually Register the Panel
+ $this->manager->add_panel( $this->ID, $panel_args );
+
+ // Return the Panel instance
+ return $this->manager->get_panel( $this->ID );
+ }
+
+ /**
+ * Returns a URL to the TEC Customizer panel.
+ *
+ * @since 4.14.0
+ *
+ * @return string The URL to the TEC Customizer panel.
+ */
+ public function get_panel_url() {
+ $query['autofocus[panel]'] = 'tribe_customizer';
+ return add_query_arg( $query, admin_url( 'customize.php' ) );
+ }
+
+ /**
+ * Returns an HTML link directly to the (opened) TEC Customizer panel
+ *
+ * @since 4.14.0
+ *
+ * @param string $link_text The (pre)translated text for the link.
+ *
+ * @return string The HTML anchor element, linking to the TEC Customizer panel.
+ * An empty string is returned if missing a parameter.
+ */
+ public function get_panel_link( $link_text ) {
+ if ( empty( $link_text ) || ! is_string( $link_text ) ) {
+ return '';
+ }
+
+ $panel_url = $this->get_panel_url();
+
+ return sprintf(
+ '%2$s',
+ esc_url( $panel_url ),
+ esc_html( $link_text )
+ );
+ }
+
+ /**
+ * Use a "alias" method to register sections to allow users to filter args and the ID
+ *
+ * @since 4.0
+ *
+ * @param string $id The Unique section ID
+ * @param array $args Arguments to register the section
+ *
+ * @link https://codex.wordpress.org/Class_Reference/WP_Customize_Manager/add_section
+ *
+ * @return WP_Customize_Section
+ */
+ public function register_section( $id, $args ) {
+ /**
+ * Filter the Section ID
+ *
+ * @since 4.4
+ *
+ * @param string $section_id
+ * @param Tribe__Customizer $customizer
+ */
+ $section_id = apply_filters( 'tribe_customizer_section_id', $id, $this );
+
+ // Tries to fetch the section
+ $section = $this->manager->get_section( $section_id );
+
+ // If the Panel already exists we leave returning it's instance
+ if ( ! empty( $section ) ) {
+ return $section;
+ }
+
+ /**
+ * Filter the Section arguments, so that developers can filter arguments based on $section_id
+ *
+ * @since 4.4
+ *
+ * @param array $args
+ * @param string $section_id
+ * @param Tribe__Customizer $customizer
+ */
+ $section_args = apply_filters( 'tribe_customizer_section_args', $args, $section_id, $this );
+
+ // Don't allow sections outside of our panel
+ $section_args['panel'] = $this->panel->id;
+
+ // Actually Register the Section
+ $this->manager->add_section( $section_id, $section_args );
+
+ // Return the Section instance
+ return $this->manager->get_section( $section_id );
+ }
+
+ /**
+ * Returns a URL to the a specific TEC Customizer section.
+ *
+ * @since 4.14.0
+ *
+ * @param string $section The slug for the desired section.
+ *
+ * @return string The URL to the TEC Customizer section.
+ */
+ public function get_section_url( $section ) {
+ if ( empty( $section ) ) {
+ return '';
+ }
+
+ $query['autofocus[section]'] = $section;
+ return add_query_arg( $query, admin_url( 'customize.php' ) );
+ }
+
+ /**
+ * Gets the HTML link to a section in the TEC Customizer.
+ *
+ * @since 4.14.0
+ *
+ * @param string $section The section "slug" to link to.
+ * @param string $link_text The text for the link.
+ *
+ * @return string The HTML anchor element, linking to the TEC Customizer section.
+ * An empty string is returned if missing a parameter.
+ */
+ public function get_section_link( $section, $link_text = '' ) {
+ if ( empty( $section ) || empty( $link_text ) || ! is_string($link_text ) ) {
+ return '';
+ }
+
+
+ $panel_url = $this->get_section_url( $section );
+ if ( empty( $panel_url ) ) {
+ return '';
+ }
+
+ return sprintf(
+ '%2$s',
+ esc_url( $panel_url ),
+ esc_html( $link_text )
+ );
+ }
+
+ /**
+ * Build the Setting name using the HTML format for Arrays
+ *
+ * @since 4.0
+ *
+ * @param string $slug The actual Setting name
+ * @param string|WP_Customize_Section $section The section the setting lives in.
+ *
+ * @return string HTML name Attribute name of the setting.
+ */
+ public function get_setting_name( $slug, $section = null ) {
+ $name = ! empty( $this->panel->id ) ? $this->panel->id : '';
+
+ // If there is a section set append it
+ if ( $section instanceof WP_Customize_Section ) {
+ $name .= '[' . $section->id . ']';
+ } elseif ( is_string( $section ) ) {
+ $name .= '[' . $section . ']';
+ }
+
+ // Set the actual setting slug
+ $name .= '[' . esc_attr( $slug ) . ']';
+
+ return $name;
+ }
+
+ /**
+ * Adds a setting field name to the Array of Possible Selective refresh fields
+ *
+ * @since 4.2
+ *
+ * @param string $name The actual Setting name
+ *
+ * @return array The list of existing Settings, the new one included
+ */
+ public function add_setting_name( $name ) {
+ $this->settings[] = $name;
+ return $this->settings;
+ }
+
+ /**
+ * Gets the URL to a specific control/setting in the TEC Customizer.
+ *
+ * @since 4.14.0
+ *
+ * @param string $section The section "slug" to link into.
+ * @param string $setting The setting "slug" to link to.
+ *
+ * @return string The URL to the setting.
+ * An empty string is returned if a parameter is missing or the setting control cannot be found.
+ */
+ public function get_setting_url( $section, $setting ) {
+ // Bail if something is missing.
+ if ( empty( $setting ) || empty( $section ) ) {
+ return '';
+ }
+
+ $control = $this->get_setting_name( $setting, $section );
+
+ if ( empty( $control ) ) {
+ return '';
+ }
+
+ $query['autofocus[control]'] = $control;
+
+ return add_query_arg( $query, admin_url( 'customize.php' ) );
+ }
+
+ /**
+ * Gets the link to the a specific control/setting in the TEC Customizer.
+ *
+ * @since 4.14.0
+ *
+ * @param string $section The section "slug" to link into.
+ * @param string $setting The setting "slug" to link to.
+ * @param string $link_text The translated text for the link.
+ *
+ * @return string The HTML anchor element, linking to the TEC Customizer setting.
+ * An empty string is returned if missing a parameter or the setting control cannot be found.
+ */
+ public function get_setting_link( $section, $setting, $link_text ) {
+ // Bail if something is missing.
+ if ( empty( $setting ) || empty( $section ) || empty( $link_text ) ) {
+ return '';
+ }
+
+ $control_url = $this->get_setting_url( $section, $setting );
+
+ if ( empty( $control_url ) ) {
+ return '';
+ }
+
+ return sprintf(
+ '%2$s',
+ esc_url( $control_url ),
+ esc_html( $link_text )
+ );
+ }
+
+ /**
+ * Using the Previously created CSS element, we not just re-create it every setting change
+ *
+ * @since 4.2
+ *
+ * @return void
+ */
+ public function maybe_selective_refresh() {
+ // Only try to apply selective refresh if it's active
+ if ( ! isset( $this->manager->selective_refresh ) ) {
+ return;
+ }
+
+ foreach ( $this->settings as $name ) {
+ $setting = $this->manager->get_setting( $name );
+
+ // Skip if we don't have that setting then skip it
+ if ( is_null( $setting ) ) {
+ continue;
+ }
+
+ // Skip if we already have that
+ if ( ! is_null( $this->manager->selective_refresh->get_partial( $name ) ) ) {
+ continue;
+ }
+
+ // Remove the Setting
+ // We need this because settings are protected on the WP_Customize_Manager
+ $this->manager->remove_setting( $name );
+
+ // Change the Transport
+ $setting->transport = 'postMessage';
+
+ // Re-add the setting
+ // We need this because settings are protected on the WP_Customize_Manager
+ $this->manager->add_setting( $setting );
+
+ // Add the Partial
+ $this->manager->selective_refresh->add_partial(
+ $name,
+ [
+ 'selector' => '#' . esc_attr( $this->ID . '_css' ),
+ 'render_callback' => [ $this, 'print_css_template' ],
+ ]
+ );
+ }
+ }
+
+ /**
+ * Builds and returns the Customizer CSS template contents.
+ *
+ * The method DOES NOT check if the current context is the one where the Customizer template should
+ * be printed or not; that care is left to the code calling this method.
+ *
+ * @since 4.12.6 Extracted this method from the `print_css_template` one.
+ *
+ * @return string The CSS template contents.
+ */
+ public function get_styles_scripts() {
+ /**
+ * Use this filter to add more CSS, using Underscore Template style.
+ *
+ * @since 4.4
+ *
+ * @param string $template The Customizer template.
+ *
+ * @link http://underscorejs.org/#template
+ */
+ $css_template = trim( apply_filters( 'tribe_customizer_css_template', '' ) );
+
+ // If we don't have anything on the Customizer, then don't print empty styles.
+ if ( empty( $css_template ) ) {
+ return '';
+ }
+
+ // Prepare the customizer scripts.
+ $result = '';
+
+ // Prepare the customizer styles.
+ $result .= '';
+
+ return $result;
+ }
+
+ /**
+ * Inserts link to TEC Customizer section for FSE themes in admin (left) menu.
+ *
+ * @since 4.14.8
+ */
+ public function add_fse_customizer_link() {
+ _deprecated_function( __METHOD__, '4.14.18', 'No replacement. Customizer menu item is preserved as long as we activate it.');
+ // Exit early if the current theme is not a FSE theme.
+ if ( ! tec_is_full_site_editor() ) {
+ return;
+ }
+
+ // Add a link to the TEC panel in the Customizer.
+ add_submenu_page(
+ 'themes.php',
+ _x( 'Customize The Events Calendar', 'Page title for the TEC Customizer section.', 'tribe-common' ),
+ _x( 'Customize The Events Calendar', 'Menu item text for the TEC Customizer section link.', 'tribe-common' ),
+ 'edit_theme_options',
+ esc_url( add_query_arg( 'autofocus[panel]', 'tribe_customizer' , admin_url( 'customize.php' ) ) )
+ );
+ }
+
+ /**
+ * Inserts link to TEC Customizer section for FSE themes in Events > Settings > Display.
+ *
+ * @since 4.14.8
+ *
+ * @param array $settings The existing settings array.
+ *
+ * @return array $settings The modified settings array.
+ */
+ public function add_fse_customizer_link_to_display_tab( $settings ) {
+ _deprecated_function( __METHOD__, '4.14.18', 'No replacement. Customizer link is preserved as long as we activate it.');
+ // Exit early if the current theme is not a FSE theme.
+ if ( ! tec_is_full_site_editor() ) {
+ return $settings;
+ }
+
+ $new_settings = [
+ 'tribe-customizer-section-title' => [
+ 'type' => 'html',
+ 'html' => '
';
+
+ if ( true === $this->tooltip_first ) {
+ $return .= $this->do_tool_tip();
+ // and empty it to avoid it from being printed again
+ $this->tooltip = '';
+ }
+
+ return apply_filters( 'tribe_field_div_start', $return, $this );
+ }
+
+ /**
+ * returns the field's div end
+ *
+ * @return string the field div end
+ */
+ public function do_field_div_end() {
+ $return = $this->do_tool_tip();
+ $return .= '
';
+
+ return apply_filters( 'tribe_field_div_end', $return, $this );
+ }
+
+ /**
+ * returns the field's tooltip/description
+ *
+ * @return string the field tooltip
+ */
+ public function do_tool_tip() {
+ $return = '';
+ if ( $this->tooltip ) {
+ $return = '
' . $this->tooltip . '
';
+ }
+
+ return apply_filters( 'tribe_field_tooltip', $return, $this->tooltip, $this );
+ }
+
+ /**
+ * returns the screen reader label
+ *
+ * @return string the screen reader label
+ */
+ public function do_screen_reader_label() {
+ $return = '';
+ if ( $this->tooltip ) {
+ $return = '';
+ }
+
+ return apply_filters( 'tribe_field_screen_reader_label', $return, $this->tooltip, $this );
+ }
+
+ /**
+ * returns the field's value
+ *
+ * @return string the field value
+ */
+ public function do_field_value() {
+ $return = '';
+ if ( $this->value ) {
+ $return = ' value="' . $this->value . '"';
+ }
+
+ return apply_filters( 'tribe_field_value', $return, $this->value, $this );
+ }
+
+ /**
+ * returns the field's name
+ *
+ * @param bool $multi
+ *
+ * @return string the field name
+ */
+ public function do_field_name( $multi = false ) {
+ $return = '';
+ if ( $this->name ) {
+ if ( $multi ) {
+ $return = ' name="' . $this->name . '[]"';
+ } else {
+ $return = ' name="' . $this->name . '"';
+ }
+ }
+
+ return apply_filters( 'tribe_field_name', $return, $this->name, $this );
+ }
+
+ /**
+ * returns the field's placeholder
+ *
+ * @return string the field value
+ */
+ public function do_field_placeholder() {
+ $return = '';
+ if ( $this->placeholder ) {
+ $return = ' placeholder="' . $this->placeholder . '"';
+ }
+
+ return apply_filters( 'tribe_field_placeholder', $return, $this->placeholder, $this );
+ }
+
+ /**
+ * Return a string of attributes for the field
+ *
+ * @return string
+ **/
+ public function do_field_attributes() {
+ $return = '';
+ if ( ! empty( $this->attributes ) ) {
+ foreach ( $this->attributes as $key => $value ) {
+ $return .= ' ' . $key . '="' . $value . '"';
+ }
+ }
+
+ return apply_filters( 'tribe_field_attributes', $return, $this->name, $this );
+ }
+
+ /**
+ * Return a string of attributes for the fieldset
+ *
+ * @return string
+ **/
+ public function do_fieldset_attributes() {
+ $return = '';
+ if ( ! empty( $this->fieldset_attributes ) ) {
+ foreach ( $this->fieldset_attributes as $key => $value ) {
+ $return .= ' ' . $key . '="' . $value . '"';
+ }
+ }
+
+ return apply_filters( 'tribe_fieldset_attributes', $return, $this->name, $this );
+ }
+
+ /**
+ * generate a heading field
+ *
+ * @return string the field
+ */
+ public function heading() {
+ $field = '
' . $this->label . '
';
+
+ return $field;
+ }
+
+ /**
+ * generate an html field
+ *
+ * @return string the field
+ */
+ public function html() {
+ $field = $this->do_field_label();
+ $field .= $this->html;
+
+ return $field;
+ }
+
+ /**
+ * generate a simple text field
+ *
+ * @return string the field
+ */
+ public function text() {
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+ $field .= 'do_field_name();
+ $field .= $this->do_field_value();
+ $field .= $this->do_field_placeholder();
+ $field .= $this->do_field_attributes();
+ $field .= '/>';
+ $field .= $this->do_screen_reader_label();
+ $field .= $this->do_field_div_end();
+ $field .= $this->do_field_end();
+
+ return $field;
+ }
+
+ /**
+ * generate a textarea field
+ *
+ * @return string the field
+ */
+ public function textarea() {
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+ $field .= '';
+ $field .= $this->do_screen_reader_label();
+ $field .= $this->do_field_div_end();
+ $field .= $this->do_field_end();
+
+ return $field;
+ }
+
+ /**
+ * generate a wp_editor field
+ *
+ * @return string the field
+ */
+ public function wysiwyg() {
+ $mce = new Wysiwyg( $this->name, $this->value, $this->settings );
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+ $field .= $mce->get_html();
+ $field .= $this->do_screen_reader_label();
+ $field .= $this->do_field_div_end();
+ $field .= $this->do_field_end();
+
+ return $field;
+ }
+
+ /**
+ * generate a radio button field
+ *
+ * @return string the field
+ */
+ public function radio() {
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+ if ( is_array( $this->options ) ) {
+ foreach ( $this->options as $option_id => $title ) {
+ $field_id = sprintf(
+ '%1$s-%2$s',
+ sanitize_html_class( trim( $this->id ) ),
+ sanitize_html_class( trim( $option_id ) )
+ );
+
+ $field .= '';
+ }
+ } else {
+ $field .= '' . esc_html__( 'No radio options specified', 'tribe-common' ) . '';
+ }
+ $field .= $this->do_field_div_end();
+ $field .= $this->do_field_end();
+
+ return $field;
+ }
+
+ /**
+ * generate a checkbox_list field
+ *
+ * @return string the field
+ */
+ public function checkbox_list() {
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+
+ if ( ! is_array( $this->value ) ) {
+ if ( ! empty( $this->value ) ) {
+ $this->value = [ $this->value ];
+ } else {
+ $this->value = [];
+ }
+ }
+
+ if ( is_array( $this->options ) ) {
+ foreach ( $this->options as $option_id => $title ) {
+ $field .= '';
+ }
+ } else {
+ $field .= '' . esc_html__( 'No checkbox options specified', 'tribe-common' ) . '';
+ }
+ $field .= $this->do_field_div_end();
+ $field .= $this->do_field_end();
+
+ return $field;
+ }
+
+ /**
+ * generate a boolean checkbox field
+ *
+ * @return string the field
+ */
+ public function checkbox_bool() {
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+ $field .= 'do_field_name();
+ $field .= ' value="1" ' . checked( $this->value, true, false );
+ $field .= $this->do_field_attributes();
+ $field .= '/>';
+ $field .= $this->do_screen_reader_label();
+ $field .= $this->do_field_div_end();
+ $field .= $this->do_field_end();
+
+ return $field;
+ }
+
+ /**
+ * generate a dropdown field
+ *
+ * @return string the field
+ */
+ public function dropdown() {
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+ if ( is_array( $this->options ) && ! empty( $this->options ) ) {
+ $field .= '';
+ $field .= $this->do_screen_reader_label();
+ } elseif ( $this->if_empty ) {
+ $field .= '' . (string) $this->if_empty . '';
+ } else {
+ $field .= '' . esc_html__( 'No select options specified', 'tribe-common' ) . '';
+ }
+ $field .= $this->do_field_div_end();
+ $field .= $this->do_field_end();
+
+ return $field;
+ }
+
+ /**
+ * generate a chosen dropdown field - the same as the
+ * regular dropdown but wrapped so it can have the
+ * right css class applied to it
+ *
+ * @deprecated
+ *
+ * @return string the field
+ */
+ public function dropdown_chosen() {
+ $field = $this->dropdown();
+
+ return $field;
+ }
+
+ /**
+ * generate a select2 dropdown field - the same as the
+ * regular dropdown but wrapped so it can have the
+ * right css class applied to it
+ *
+ * @deprecated
+ *
+ * @return string the field
+ */
+ public function dropdown_select2() {
+ $field = $this->dropdown();
+
+ return $field;
+ }
+
+ /**
+ * generate a license key field
+ *
+ * @return string the field
+ */
+ public function license_key() {
+ $field = $this->do_field_start();
+ $field .= $this->do_field_label();
+ $field .= $this->do_field_div_start();
+ $field .= 'do_field_name();
+ $field .= $this->do_field_value();
+ $field .= $this->do_field_attributes();
+ $field .= '/>';
+ $field .= '
',
+ $message_row_html
+ );
+
+ $this->plugin_notice = [
+ 'slug' => $this->plugin_file,
+ 'message_row_html' => $message_row_html,
+ ];
+
+ add_filter( 'tribe_plugin_notices', [ $this, 'add_notice_to_plugin_notices' ] );
+
+ }
+
+ public function add_notice_to_plugin_notices( $notices ) {
+ if ( ! $this->plugin_notice || $this->is_network_licensed() ) {
+ return $notices;
+ }
+
+ $notices[ $this->plugin_notice['slug'] ] = $this->plugin_notice;
+
+ return $notices;
+ }
+
+ /**
+ * Returns plugin/license key data based on the provided query arguments.
+ *
+ * Calling this method will also take care of setting up admin notices for any
+ * keys that are invalid or have expired, etc.
+ *
+ * @see Tribe__PUE__Checker::request_info()
+ *
+ * @param $query_args
+ *
+ * @return Tribe__PUE__Plugin_Info|null
+ */
+ public function license_key_status( $query_args ) {
+ $pue_notices = Tribe__Main::instance()->pue_notices();
+ $plugin_info = $this->request_info( $query_args );
+ $plugin_name = empty( $this->plugin_name ) ? $this->get_plugin_name() : $this->plugin_name;
+
+ if ( empty( $plugin_name ) ) {
+ return $plugin_info;
+ }
+
+ $install_key = $this->get_key();
+
+ // Check for expired keys
+ if ( ! empty( $plugin_info->api_expired ) ) {
+ $pue_notices->add_notice( Tribe__PUE__Notices::EXPIRED_KEY, $plugin_name );
+ } elseif ( ! empty( $plugin_info->api_upgrade ) ) {
+ // Check for keys that are out of installs (*must* happen before the api_invalid test)
+ $pue_notices->add_notice( Tribe__PUE__Notices::UPGRADE_KEY, $plugin_name );
+ } elseif (
+ // Check for invalid keys last of all (upgrades/empty keys will be flagged as invalid)
+ ! empty( $plugin_info->api_invalid )
+ && (
+ 'component' === $this->context
+ || (
+ 'service' === $this->context
+ && $install_key
+ )
+ )
+ ) {
+ $pue_notices->add_notice( Tribe__PUE__Notices::INVALID_KEY, $plugin_name );
+ } else {
+ // If none of the above were satisfied we can assume the key is valid
+ $pue_notices->clear_notices( $plugin_name );
+ }
+
+ return $plugin_info;
+ }
+
+ /**
+ * Sets up and manages those license key notifications which don't depend on communicating with a remote
+ * PUE server, etc.
+ */
+ public function general_notifications() {
+ $plugin_name = empty( $this->plugin_name ) ? $this->get_plugin_name() : $this->plugin_name;
+
+ // Register our plugin name for use in messages (thus if we're deactivated, any previously
+ // added persistent messaging can be cleared)
+ Tribe__Main::instance()->pue_notices()->register_name( $plugin_name );
+
+ // Detect and setup notices for missing keys
+ $install_key = $this->get_key();
+
+ if ( empty( $install_key ) && 'service' !== $this->context ) {
+ Tribe__Main::instance()->pue_notices()->add_notice( Tribe__PUE__Notices::INVALID_KEY, $plugin_name );
+ }
+ }
+
+ /**
+ * Retrieve plugin info from the configured API endpoint.
+ *
+ * In general, this method should not be called directly and it is preferable to call
+ * the license_key_status() method instead. That method returns the same result, but
+ * also analyses each response to set up appropriate license key notifications in the
+ * admin environment.
+ *
+ * @uses wp_remote_get()
+ * @see Tribe__PUE__Checker::license_key_status()
+ *
+ * @param array $query_args Additional query arguments to append to the request. Optional.
+ *
+ * @return string $plugin_info
+ */
+ public function request_info( $query_args = [] ) {
+ $query_args = apply_filters( 'tribe_puc_request_info_query_args-' . $this->get_slug(), $query_args );
+
+ // Cache the API call so it only needs to be made once per plugin per page load.
+ static $plugin_info_cache;
+
+ // Sort parameter keys
+ $hash_data = $query_args;
+
+ ksort( $hash_data );
+
+ // Flatten hashed data
+ $hash_data = json_encode( $hash_data );
+
+ // Generate unique hash
+ $key = hash( 'sha256', $hash_data );
+
+ if ( isset( $plugin_info_cache[ $key ] ) ) {
+ return $plugin_info_cache[ $key ];
+ }
+
+ //Various options for the wp_remote_get() call. Plugins can filter these, too.
+ $options = [
+ 'body' => $query_args,
+ 'timeout' => 15, //seconds
+ 'headers' => [
+ 'Accept' => 'application/json',
+ ],
+ ];
+ $options = apply_filters( 'tribe_puc_request_info_options-' . $this->get_slug(), $options );
+
+ $url = sprintf( '%s/api/plugins/v2/license/validate', $this->get_pue_update_url() );
+
+ $result = wp_remote_post(
+ $url,
+ $options
+ );
+
+ // Try to parse the response
+ $plugin_info = null;
+ if ( ! is_wp_error( $result ) && isset( $result['response']['code'] ) && ( 200 === (int) $result['response']['code'] ) && ! empty( $result['body'] ) ) {
+ $plugin_info = Tribe__PUE__Plugin_Info::from_json( $result['body'] );
+ }
+ $plugin_info = apply_filters( 'tribe_puc_request_info_result-' . $this->get_slug(), $plugin_info, $result );
+
+ $plugin_info_cache[ $key ] = $plugin_info;
+
+ return $plugin_info;
+ }
+
+ /**
+ * Returns the domain contained in the network's siteurl option (not the full URL).
+ *
+ * @return string
+ */
+ public function get_network_domain() {
+ $site_url = wp_parse_url( get_site_option( 'siteurl' ) );
+ if ( ! $site_url || ! isset( $site_url['host'] ) ) {
+ return '';
+ } else {
+ return strtolower( $site_url['host'] );
+ }
+ }
+
+ /**
+ * Retrieve the latest update (if any) from the configured API endpoint.
+ *
+ * @uses Tribe__PUE__Checker::request_info()
+ *
+ * @return Tribe__PUE__Utility An instance of Tribe__PUE__Utility, or NULL when no updates are available.
+ */
+ public function request_update() {
+ // For the sake of simplicity, this function just calls request_info()
+ // and transforms the result accordingly.
+ $query_args = $this->get_validate_query();
+
+ if ( ! empty( $_POST['key'] ) ) {
+ $query_args['key'] = sanitize_text_field( $_POST['key'] );
+ } elseif ( ! empty( $_POST[ $this->pue_install_key ] ) ) {
+ $query_args['key'] = sanitize_text_field( $_POST[ $this->pue_install_key ] );
+ }
+
+ $this->plugin_info = $plugin_info = $this->license_key_status( $query_args );
+
+ if ( null === $plugin_info ) {
+ return null;
+ }
+
+ // admin display for if the update check reveals that there is a new version but the API key isn't valid.
+ if ( isset( $plugin_info->api_invalid ) ) {
+ $plugin_info = Tribe__PUE__Utility::from_plugin_info( $plugin_info );
+ $plugin_info->license_error = $this->get_api_message( $plugin_info );
+
+ return $plugin_info;
+ }
+
+ if ( ! empty( $plugin_info->new_install_key ) ) {
+ $this->update_key( $plugin_info->new_install_key );
+ }
+
+ // Need to correct the download url so it contains the custom user data (e.g. api and any other parameters).
+ $download_query = $this->get_download_query();
+
+ if ( ! empty( $download_query ) ) {
+ $plugin_info->download_url = esc_url_raw( add_query_arg( $download_query, $plugin_info->download_url ) );
+ }
+
+ // Add plugin dirname/file (this will be expected by WordPress when it builds the plugin list table)
+ $plugin_info->plugin = $this->get_plugin_file();
+
+ return Tribe__PUE__Utility::from_plugin_info( $plugin_info );
+ }
+
+ /**
+ * Display a changelog when the api key is missing.
+ */
+ public function display_changelog() {
+ //contents of changelog display page when api-key is invalid or missing. It will ONLY show the changelog (hook into existing thickbox?)
+ }
+
+ /**
+ * Get the currently installed version of the plugin.
+ *
+ * @return string Version number.
+ */
+ public function get_installed_version() {
+ if ( function_exists( 'get_plugins' ) ) {
+ $all_plugins = get_plugins();
+ if ( array_key_exists( $this->get_plugin_file(), $all_plugins ) && array_key_exists( 'Version', $all_plugins[ $this->get_plugin_file() ] ) ) {
+ return $all_plugins[ $this->get_plugin_file() ]['Version'];
+ }
+ }
+ }
+
+ /**
+ * Get plugin update state
+ *
+ * @param boolean $force_recheck
+ *
+ * @return object
+ */
+ public function get_state( $force_recheck = false ) {
+
+ $state = null;
+
+ if ( ! $force_recheck ) {
+ $state = get_site_option( $this->pue_option_name, false, false );
+ }
+
+ if ( empty( $state ) ) {
+ $state = new stdClass;
+ $state->lastCheck = 0;
+ $state->checkedVersion = '';
+ $state->update = null;
+ }
+
+ return $state;
+
+ }
+
+ /**
+ * Update plugin update state
+ *
+ * @param object $value
+ */
+ public function update_state( $value ) {
+
+ update_site_option( $this->pue_option_name, $value );
+
+ }
+
+ /**
+ * Check for plugin updates.
+ *
+ * The results are stored in the DB option specified in $pue_option_name.
+ *
+ * @param array $updates
+ * @param boolean $force_recheck
+ *
+ * @return array
+ */
+ public function check_for_updates( $updates = [], $force_recheck = false ) {
+ $state = $this->get_state( $force_recheck );
+
+ $state->lastCheck = time();
+ $state->checkedVersion = $this->get_installed_version();
+
+ // Save before checking in case something goes wrong
+ $this->update_state( $state );
+
+ $state->update = $this->request_update();
+
+ // If a null update was returned, skip to the end of the function.
+ if ( null !== $state->update ) {
+ // Is there an update to insert?
+ if ( version_compare( $state->update->version, $this->get_installed_version(), '>' ) ) {
+ if ( empty( $updates ) ) {
+ $updates = (object) [ 'response' => [] ];
+ }
+
+ $updates->response[ $this->get_plugin_file() ] = $state->update->to_wp_format();
+
+ // If the key has expired we should register an appropriate admin notice
+ if ( $this->plugin_info->api_expired ) {
+ Tribe__Main::instance()->pue_notices()->add_notice( Tribe__PUE__Notices::EXPIRED_KEY, $this->plugin_name );
+ }
+ }
+ }
+
+ $this->update_state( $state );
+
+ return $updates;
+ }
+
+ /**
+ * Clears out the site external site option and re-checks the license key
+ *
+ * @param string $value
+ * @param string $field_id
+ * @param string $validated_field
+ *
+ * @return string
+ */
+ public function check_for_api_key_error( $value, $field_id, $validated_field ) {
+
+ // Only hook into our option
+ if ( $this->pue_install_key !== $field_id ) {
+ return $value;
+ }
+
+ if ( 'service' !== $this->context ) {
+ $this->check_for_updates( [], true );
+ }
+
+ $network_option = false;
+
+ if ( ! empty( $validated_field->field['network_option'] ) ) {
+ $network_option = (boolean) $validated_field->field['network_option'];
+ }
+
+ $key_type = 'local';
+
+ if ( $network_option ) {
+ $key_type = 'network';
+ }
+
+ $current_key = $this->get_key( $key_type );
+
+ // if we are saving this PUE key, we need to make sure we update the license key notices
+ // appropriately. Otherwise, we could have an invalid license key in place but the notices
+ // aren't being thrown globally
+
+ $query_args = $this->get_validate_query();
+
+ $query_args['key'] = sanitize_text_field( $value );
+
+ $this->license_key_status( $query_args );
+
+ return $value;
+
+ }
+
+ /**
+ * Intercept plugins_api() calls that request information about our plugin and
+ * use the configured API endpoint to satisfy them.
+ *
+ * @see plugins_api()
+ *
+ * @param mixed $result
+ * @param string $action
+ * @param array|object $args
+ *
+ * @return mixed
+ */
+ public function inject_info( $result, $action = null, $args = null ) {
+ $relevant = ( 'plugin_information' === $action ) && isset( $args->slug ) && ( $args->slug === $this->slug );
+ if ( ! $relevant ) {
+ return $result;
+ }
+
+ $query_args = $this->get_validate_query();
+
+ $plugin_info = $this->license_key_status( $query_args );
+
+ if ( $plugin_info ) {
+ return $plugin_info->to_wp_format();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Register a callback for filtering query arguments.
+ *
+ * The callback function should take one argument - an associative array of query arguments.
+ * It should return a modified array of query arguments.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callback $callback
+ *
+ */
+ public function add_query_arg_filter( $callback ) {
+ add_filter( 'tribe_puc_request_info_query_args-' . $this->get_slug(), $callback );
+ }
+
+ /**
+ * Register a callback for filtering arguments passed to wp_remote_get().
+ *
+ * The callback function should take one argument - an associative array of arguments -
+ * and return a modified array or arguments. See the WP documentation on wp_remote_get()
+ * for details on what arguments are available and how they work.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callback $callback
+ *
+ */
+ public function add_http_request_arg_filter( $callback ) {
+ add_filter( 'tribe_puc_request_info_options-' . $this->get_slug(), $callback );
+ }
+
+ /**
+ * Register a callback for filtering the plugin info retrieved from the external API.
+ *
+ * The callback function should take two arguments. If the plugin info was retrieved
+ * successfully, the first argument passed will be an instance of Tribe__PUE__Plugin_Info. Otherwise,
+ * it will be NULL. The second argument will be the corresponding return value of
+ * wp_remote_get (see WP docs for details).
+ *
+ * The callback function should return a new or modified instance of Tribe__PUE__Plugin_Info or NULL.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callback $callback
+ *
+ */
+ public function add_result_filter( $callback ) {
+ add_filter( 'tribe_puc_request_info_result-' . $this->get_slug(), $callback, 10, 2 );
+ }
+
+ /**
+ * Insert an array after a specified key within another array.
+ *
+ * @param $key
+ * @param $source_array
+ * @param $insert_array
+ *
+ * @return array
+ *
+ */
+ public static function array_insert_after_key( $key, $source_array, $insert_array ) {
+ if ( array_key_exists( $key, $source_array ) ) {
+ $position = array_search( $key, array_keys( $source_array ), true ) + 1;
+ $source_array = array_slice( $source_array, 0, $position, true ) + $insert_array + array_slice( $source_array, $position, null, true );
+ } else {
+ // If no key is found, then add it to the end of the array.
+ $source_array += $insert_array;
+ }
+
+ return $source_array;
+ }
+
+ /**
+ * Add this plugin key to the list of keys
+ *
+ * @param array $keys
+ *
+ * @return array $keys
+ *
+ */
+ public function return_install_key( $keys = [] ) {
+ $key = $this->get_key();
+
+ if ( ! empty( $key ) ) {
+ $keys[ $this->get_slug() ] = $key;
+ }
+
+ return $keys;
+ }
+
+ /**
+ * Prevent the default inline update-available messages from appearing, as we
+ * have implemented our own.
+ *
+ * @see resources/js/pue-notices.js
+ */
+ public function remove_default_inline_update_msg() {
+ remove_action( "after_plugin_row_{$this->plugin_file}", 'wp_plugin_update_row' );
+ }
+
+ /**
+ * Returns the domain of the single site installation
+ *
+ * Will try to read it from the $_SERVER['SERVER_NAME'] variable
+ * and fall back on the one contained in the siteurl option.
+ *
+ * @return string
+ */
+ protected function get_site_domain() {
+ if ( isset( $_SERVER['SERVER_NAME'] ) ) {
+ return $_SERVER['SERVER_NAME'];
+ }
+ $site_url = wp_parse_url( get_option( 'siteurl' ) );
+ if ( ! $site_url || ! isset( $site_url['host'] ) ) {
+ return '';
+ } else {
+ return strtolower( $site_url['host'] );
+ }
+ }
+
+ /**
+ * Check whether the current plugin is active for the network or not.
+ *
+ * @return boolean Whether the plugin is network activated
+ */
+ protected function is_plugin_active_for_network() {
+
+ if ( ! is_multisite() ) {
+ return false;
+ }
+
+ $map = [
+ 'event-aggregator/event-aggregator.php' => 'the-events-calendar/the-events-calendar.php',
+ ];
+
+ $plugin_file = $this->get_plugin_file();
+
+ if ( isset( $map[ $this->plugin_file ] ) ) {
+ $plugin_file = $map[ $this->plugin_file ];
+ }
+
+ if ( function_exists( 'is_plugin_active_for_network' ) ) {
+ // If is_plugin_active_for_network() is available, let's use it!
+ return is_plugin_active_for_network( $plugin_file );
+ } else {
+ // When this method is called sufficiently early in the request,
+ // is_plugin_active_for_network() may not be available (#115826)
+ $plugins = get_site_option( 'active_sitewide_plugins' );
+ return isset( $plugins[ $plugin_file ] );
+ }
+ }
+
+ /**
+ * Returns the localized string for a plugin or component license state.
+ *
+ * @return string The localized state string.
+ */
+ protected function get_network_license_state_string() {
+ $states = [
+ 'licensed' => esc_html__( 'A valid license has been entered by your network administrator.', 'tribe-common' ),
+ 'not-licensed' => esc_html__( 'No license entered. Consult your network administrator.', 'tribe-common' ),
+ 'expired' => esc_html__( 'Expired license. Consult your network administrator.', 'tribe-common' ),
+ ];
+
+ $response = $this->validate_key( $this->get_key( 'network' ), true );
+
+ if ( isset( $response['status'] ) && 1 === (int) $response['status'] ) {
+ $state = 'licensed';
+ } elseif ( isset( $response['api_expired'] ) && true === (boolean) $response['api_expired'] ) {
+ $state = 'expired';
+ } else {
+ $state = 'not-licensed';
+ }
+
+ return $states[ $state ];
+ }
+
+ /**
+ * Whether the user should be shown the fully editable subsite license field or not.
+ *
+ * This check will happen in the context of the plugin administration area; checks on the user
+ * capability to edit the plugin settings have been made before.
+ *
+ * @return bool
+ */
+ public function should_show_subsite_editable_license() {
+ if ( ! is_multisite() ) {
+ return true;
+ }
+
+ if ( is_network_admin() ) {
+ return false;
+ }
+
+ if ( $this->is_plugin_active_for_network() && ! is_super_admin() ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether the user should be shown the override control to override the network license key or not.
+ *
+ * This check will happen in the context of the plugin administration area; checks on the user
+ * capability to edit the plugin settings have been made before.
+ *
+ * @return bool
+ */
+ public function should_show_overrideable_license() {
+ if ( is_network_admin() ) {
+ return false;
+ }
+
+ if ( is_super_admin() ) {
+ return false;
+ }
+
+ if ( ! $this->is_plugin_active_for_network() ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether the user should be shown the fully editable network license field or not.
+ *
+ * This check will happen in the context of the network plugin administration area; checks on the user
+ * capability to edit the network plugin settings have been made before.
+ *
+ * @return bool
+ */
+ public function should_show_network_editable_license() {
+ return is_network_admin() && is_super_admin();
+ }
+
+ /**
+ * Determines if the value on the DB is the correct format.
+ *
+ * @since 4.15.0
+ *
+ * @return bool
+ */
+ public function is_valid_key_format() {
+ $license_opt = (string) get_option( $this->get_license_option_key() );
+ if ( empty( $license_opt ) ) {
+ return false;
+ }
+
+ if ( ! preg_match( "/([0-9a-z]+)/i", $license_opt, $matches ) ) {
+ return false;
+ }
+
+ // Pull the matching string into a variable
+ $license = $matches[1];
+
+ if ( 40 !== strlen( $license ) ) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Notices.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Notices.php
new file mode 100644
index 000000000..4f9c8702d
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Notices.php
@@ -0,0 +1,533 @@
+ 'Event Tickets Plus',
+ 'pue_install_key_events_community' => 'The Events Calendar: Community Events',
+ 'pue_install_key_events_community_tickets' => 'The Events Calendar: Community Events Tickets',
+ 'pue_install_key_image_widget_plus' => 'Image Widget Plus',
+ 'pue_install_key_tribe_eventbrite' => 'The Events Calendar: Eventbrite Tickets',
+ 'pue_install_key_tribe_filterbar' => 'The Events Calendar: Filter Bar',
+ 'pue_install_key_event_aggregator' => 'Event Aggregator',
+ 'pue_install_key_events_calendar_pro' => 'The Events Calendar PRO',
+ ];
+
+ /**
+ * Sets up license key related admin notices.
+ */
+ public function __construct() {
+ $this->populate();
+ add_action( 'current_screen', [ $this, 'setup_notices' ] );
+ add_action( 'tribe_pue_notices_save_notices', [ $this, 'maybe_undismiss_notices' ] );
+ }
+
+ /**
+ * Registers a plugin name that should be used in license key notifications.
+ *
+ * If, on a given request, the name is not registered then the plugin name will not
+ * feature in any notifications. The benefit is that if a plugin is suddenly removed,
+ * it's name can be automatically dropped from any pre-registered persistent
+ * notifications.
+ *
+ * @param string $plugin_name
+ */
+ public function register_name( $plugin_name ) {
+ $this->registered[] = $plugin_name;
+ }
+
+ /**
+ * Restores plugins added on previous requests to the relevant notification
+ * groups.
+ */
+ protected function populate() {
+ $this->saved_notices = (array) get_option( self::STORE_KEY, [] );
+
+ if ( empty( $this->saved_notices ) ) {
+ return;
+ }
+
+ $this->notices = array_merge_recursive( $this->notices, $this->saved_notices );
+
+ // Cleanup
+ foreach ( $this->notices as $key => &$plugin_lists ) {
+ // Purge any elements that are not arrays
+ if ( ! is_array( $plugin_lists ) ) {
+ unset( $this->notices[ $key ] );
+ continue;
+ }
+ }
+ }
+
+ /**
+ * Saves any license key notices already added.
+ */
+ public function save_notices() {
+ update_option( self::STORE_KEY, $this->notices );
+
+ /**
+ * Fires after PUE license key notices have been saved.
+ *
+ * @param array $current_notices
+ * @param array $previously_saved_notices
+ */
+ do_action( 'tribe_pue_notices_save_notices', $this->notices, $this->saved_notices );
+ }
+
+ /**
+ * Undismisses license key notifications where appropriate.
+ *
+ * The idea is that if an invalid key is detected for one or more plugins, we show a notification
+ * until a user dismisses it. That user will not then see the notification again unless or until
+ * an additional plugin name is added to the invalid key list.
+ *
+ * Example:
+ *
+ * - Notification listing "Eventbrite" and "Pro" keys as invalid shows
+ * - User X dismisses the notification
+ * - The "Pro" license is fixed/corrected - notification remains in a "dismissed" status for User X
+ * - "Filter Bar" is added to the list of invalid keys
+ * - The invalid key notification is undismissed, to make all users (including User X) aware of
+ * the problem re Filter Bar
+ */
+ public function maybe_undismiss_notices() {
+ foreach ( $this->notices as $notice_type => $plugin_list ) {
+ if ( is_array( $this->saved_notices ) && ! empty( $this->saved_notices[ $notice_type ] ) ) {
+ $new_plugins = array_diff_key( $this->notices[ $notice_type ], $this->saved_notices[ $notice_type ] );
+ } else {
+ $new_plugins = $this->notices[ $notice_type ];
+ }
+
+ if ( ! empty( $new_plugins ) ) {
+ Tribe__Admin__Notices::instance()->undismiss_for_all( 'pue_key-' . $notice_type );
+ }
+ }
+ }
+
+ /**
+ * Used to include a plugin in a notification.
+ *
+ * For example, this could be used to add "My Plugin" to the expired license key
+ * notification by passing Tribe__PUE__Notices::EXPIRED_KEY as the second param.
+ *
+ * Plugins can only be added to one notification group at a time, so if a plugin
+ * was already added to the MISSING_KEY group and is subsequently added to the
+ * INVALID_KEY group, the previous entry (under MISSING_KEY) will be cleared.
+ *
+ * @param string $notice_type
+ * @param string $plugin_name
+ */
+ public function add_notice( $notice_type, $plugin_name ) {
+ $this->clear_notices( $plugin_name, true );
+ $this->notices[ $notice_type ][ $plugin_name ] = true;
+ $this->save_notices();
+ }
+
+ /**
+ * Returns whether or not a given plugin name has a specific notice
+ *
+ * @param string $plugin_name
+ * @param string|null $notice_type
+ *
+ * @return boolean
+ */
+ public function has_notice( $plugin_name, $notice_type = null ) {
+ // If we match a pue key we use that value
+ if ( isset( $this->plugin_names[ $plugin_name ] ) ) {
+ $plugin_name = $this->plugin_names[ $plugin_name ];
+ }
+
+ if ( $notice_type ) {
+ return ! empty( $this->notices[ $notice_type ][ $plugin_name ] );
+ }
+
+ foreach ( $this->notices as $notice_type => $plugins ) {
+ if ( ! empty( $plugins[ $plugin_name ] ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Removes any notifications for the specified plugin.
+ *
+ * Useful when a valid license key is detected for a plugin, where previously
+ * it might have been included under a warning notification.
+ *
+ * If the optional second param is set to true then this change will not
+ * immediately be committed to storage (useful if we know this will happen in
+ * any case later on in the same request).
+ *
+ * @param string $plugin_name
+ * @param bool $defer_saving_change = false
+ */
+ public function clear_notices( $plugin_name, $defer_saving_change = false ) {
+ foreach ( $this->notices as $notice_type => &$list_of_plugins ) {
+ unset( $list_of_plugins[ $plugin_name ] );
+ }
+
+ if ( ! $defer_saving_change ) {
+ $this->save_notices();
+ }
+ }
+
+ /**
+ * Tests to see if there are any extant notifications and renders them if so.
+ *
+ * This must run prior to Tribe__Admin__Notices::hook() (which currently runs during
+ * "current_screen" priority 20).
+ */
+ public function setup_notices() {
+ // Don't allow this to run multiple times
+ remove_action( 'current_screen', [ $this, 'setup_notices' ] );
+
+ // No need to display license key notices to users without appropriate capabilities
+ if ( ! current_user_can( 'install_plugins' ) ) {
+ return;
+ }
+
+ foreach ( $this->notices as $notice_type => $plugin_names ) {
+ if ( empty( $plugin_names ) ) {
+ continue;
+ }
+
+ $callback = [ $this, 'render_' . $notice_type ];
+
+ if ( is_callable( $callback ) ) {
+ tribe_notice( 'pue_key-' . $notice_type, $callback, 'dismiss=1&type=warning' );
+ }
+ }
+ }
+
+ /**
+ * Select all products with empty license keys
+ * and format their names
+ *
+ * This information will be used to remove products
+ * with no license keys from $this->notices['invalid_key']
+ *
+ * @since 4.8
+ *
+ * @return array
+ */
+ public function select_empty_keys() {
+ /** @var $wpdb */
+ global $wpdb;
+
+ $sql = "
+ SELECT option_name
+ FROM {$wpdb->options}
+ WHERE option_name LIKE 'pue_install_key_%'
+ AND option_value=''
+ ";
+
+ $empty_keys = $wpdb->get_results( $sql, ARRAY_N );
+
+ $formatted_empty_keys = [];
+ foreach ( $empty_keys as $empty_key ) {
+ $empty_key = Tribe__Utils__Array::get( $empty_key, [ 0 ] );
+ $formatted_empty_keys[] = Tribe__Utils__Array::get( $this->plugin_names, $empty_key );
+ }
+
+ return $formatted_empty_keys;
+ }
+
+ /**
+ * Generate a notice listing any plugins for which license keys have been entered but
+ * are invalid (in the sense of not matching PUE server records or having been revoked
+ * rather than having expired which is handled separately).
+ *
+ * In the context of the plugin admin screen, will not render if the key-has-expired
+ * notice is also scheduled to display.
+ */
+ public function render_invalid_key() {
+ global $pagenow;
+
+ $empty_keys = $this->select_empty_keys();
+
+ if ( empty( $empty_keys ) ) {
+ return;
+ }
+
+ // Remove the invalid_key notice for products with an empty license key
+ foreach ( $empty_keys as $empty_key ) {
+ if ( array_key_exists( $empty_key, $this->notices['invalid_key'] ) ) {
+ unset( $this->notices['invalid_key'][ $empty_key ] );
+ }
+ }
+
+ if ( 'plugins.php' === $pagenow && ! empty( $this->notices[ self::EXPIRED_KEY ] ) ) {
+ return;
+ }
+
+ $plugin_names = $this->get_formatted_plugin_names( self::INVALID_KEY );
+
+ /**
+ * Filters the list of plugins that should trigger an invalid key notice in PUE.
+ *
+ * @since 5.0.0
+ *
+ * @param array $plugin_names Array of plugin names that should trigger the invalid key notice.
+ */
+ $plugin_names = apply_filters( 'tec_pue_invalid_key_notice_plugins', $plugin_names );
+
+ if ( empty( $plugin_names ) ) {
+ return;
+ }
+
+ $prompt = sprintf(
+ _n(
+ "It looks like you're using %1\$s, but the license key is invalid. Please download the latest version %2\$sfrom your account%3\$s.",
+ "It looks like you're using %1\$s, but the license keys are invalid. Please download the latest versions %2\$sfrom your account%3\$s.",
+ count( $this->notices[ self::INVALID_KEY ] ),
+ 'tribe-common'
+ ),
+ $plugin_names,
+ '',
+ ''
+ );
+
+ /**
+ * Filters the actions that can be taken if an invalid key is present
+ *
+ * @param string $actions Actions
+ * @param array $plugin_names Plugin names the message applies to
+ */
+ $action_steps = apply_filters( 'tribe_notice_invalid_key_actions', $this->find_your_key_text(), $plugin_names );
+
+ if ( $action_steps ) {
+ $action_steps = "
{$action_steps}" );
+ }
+
+ /**
+ * Generate a notice listing any plugins for which license keys have expired.
+ *
+ * This notice should only appear at the top of the plugin admin screen and "trumps"
+ * the missing/invalid key notice on that screen only.
+ */
+ public function render_expired_key() {
+ global $pagenow;
+
+ if ( 'plugins.php' !== $pagenow ) {
+ return;
+ }
+
+ $plugin_names = $this->get_formatted_plugin_names( self::EXPIRED_KEY );
+
+ /**
+ * Filters the list of plugins that should trigger an expired key notice in PUE.
+ *
+ * @since 5.0.0
+ *
+ * @param array $plugin_names Array of plugin names that should trigger the expired key notice.
+ */
+ $plugin_names = apply_filters( 'tec_pue_expired_key_notice_plugins', $plugin_names );
+
+ if ( empty( $plugin_names ) ) {
+ return;
+ }
+
+ $prompt = sprintf( _n(
+ 'There is an update available for %1$s but your license has expired. %2$sVisit the Events Calendar website to renew your license.%3$s',
+ 'Updates are available for %1$s but your license keys have expired. %2$sVisit the Events Calendar website to renew your licenses.%3$s',
+ count( $this->notices[ self::EXPIRED_KEY ] ),
+ 'tribe-common'
+ ),
+ $plugin_names,
+ '',
+ ''
+ );
+
+ $renew_action =
+ '' .
+ __( 'Renew Your License Now', 'tribe-common' ) .
+ '' .
+ __( ' (opens in a new window)', 'tribe-common' ) .
+ '';
+
+ $this->render_notice( 'pue_key-' . self::EXPIRED_KEY, "
$prompt
$renew_action
" );
+ }
+
+ /**
+ * Generate a notice listing any plugins which have valid license keys, but those keys
+ * have met or exceeded the permitted number of installations they can be applied to.
+ */
+ public function render_upgrade_key() {
+ $plugin_names = $this->get_formatted_plugin_names( self::UPGRADE_KEY );
+
+ /**
+ * Filters the list of plugins that should trigger an upgrade key notice in PUE.
+ *
+ * @since 5.0.0
+ *
+ * @param array $plugin_names Array of plugin names that should trigger the upgrade key notice.
+ */
+ $plugin_names = apply_filters( 'tec_pue_upgrade_key_notice_plugins', $plugin_names );
+
+ if ( empty( $plugin_names ) ) {
+ return;
+ }
+
+ $prompt = sprintf( _n(
+ 'You have a license key for %1$s but the key is out of installs. %2$sVisit the Events Calendar website%3$s to manage your installs, upgrade your license, or purchase a new one.',
+ 'You have license keys for %1$s but your keys are out of installs. %2$sVisit the Events Calendar website%3$s to manage your installs, upgrade your licenses, or purchase new ones.', count( $this->notices[ self::UPGRADE_KEY ] ),
+ 'tribe-common'
+ ),
+ $plugin_names,
+ '',
+ ''
+ );
+
+ $this->render_notice( 'pue_key-' . self::UPGRADE_KEY, "
$prompt
" );
+ }
+
+ /**
+ * Renders the notice itself (the provided HTML will be wrapped in a suitable container div).
+ *
+ * @param string $slug
+ * @param string $inner_html
+ */
+ protected function render_notice( $slug, $inner_html ) {
+
+ // Enqueue the notice CSS.
+ tribe( 'assets' )->enqueue( [ 'tribe-common-admin' ] );
+
+ $mascot = esc_url( Tribe__Main::instance()->plugin_url . 'src/resources/images/mascot.png' );
+
+ $html =
+ '
+
+
+
+
' . $inner_html . '
+
';
+
+ Tribe__Admin__Notices::instance()->render( $slug, $html, false );
+ }
+
+ /**
+ * @return string
+ */
+ protected function find_your_key_text() {
+ return sprintf(
+ __( 'You can always check the status of your licenses by logging in to %1$syour account on theeventscalendar.com%2$s.', 'tribe-common' ),
+ '',
+ ''
+ );
+ }
+
+ /**
+ * Transforms a list of plugins into human readable string.
+ *
+ * Examples of output:
+ *
+ * # One name
+ * "Ticket Pro"
+ *
+ * # Two names
+ * "Ticket Pro and Calendar Legend"
+ *
+ * # Three names
+ * "Ticket Pro, Calendar Legend and Date Stars"
+ *
+ *
+ * @since 4.9.12
+ *
+ * @param array|string $plugins Array of plugin classes.
+ *
+ * @return string|false
+ */
+ public function get_formatted_plugin_names_from_classes( $plugins ) {
+ $plugin_list = [];
+
+ foreach ( (array) $plugins as $class_name ) {
+ $pue = tribe( Tribe__Dependency::class )->get_pue_from_class( $class_name );
+
+ if ( ! $pue ) {
+ continue;
+ }
+
+ if ( ! isset( $this->plugin_names[ $pue->pue_install_key ] ) ) {
+ continue;
+ }
+
+ $plugin_list[] = $this->plugin_names[ $pue->pue_install_key ];
+ }
+
+ $num_plugins = count( $plugin_list );
+
+ if ( 0 === $num_plugins ) {
+ return false;
+ }
+
+ if ( 1 === $num_plugins ) {
+ $html = current( $plugin_list );
+ } elseif ( 1 < $num_plugins ) {
+ $all_but_last = join( ', ', array_slice( $plugin_list, 0, count( $plugin_list ) - 1 ) );
+ $last = current( array_slice( $plugin_list, count( $plugin_list ) - 1, 1 ) );
+ $html = sprintf( _x( '%1$s and %2$s', 'formatted plugin list', 'tribe-common' ), $all_but_last, $last );
+ }
+
+ return '' . $html . '';
+ }
+
+ /**
+ * Transforms the array referenced by group into a human readable,
+ * comma delimited list.
+ *
+ * Examples of output:
+ *
+ * # One name
+ * "Ticket Pro"
+ *
+ * # Two names
+ * "Ticket Pro and Calendar Legend"
+ *
+ * # Three names
+ * "Ticket Pro, Calendar Legend and Date Stars"
+ *
+ * # Fallback
+ * "Unknown Plugin(s)"
+ *
+ * @param string $group
+ *
+ * @return string
+ */
+ protected function get_formatted_plugin_names( $group ) {
+ if ( ! count( $this->notices[ $group ] ) ) {
+ return '';
+ }
+
+ $plugin_list = array_intersect( $this->registered, array_keys( $this->notices[ $group ] ) );
+ $num_plugins = count( $plugin_list );
+
+ if ( 0 === $num_plugins ) {
+ return '';
+ } elseif ( 1 === $num_plugins ) {
+ $html = current( $plugin_list );
+ } elseif ( 1 < $num_plugins ) {
+ $all_but_last = join( ', ', array_slice( $plugin_list, 0, count( $plugin_list ) - 1 ) );
+ $last = current( array_slice( $plugin_list, count( $plugin_list ) - 1, 1 ) );
+ $html = sprintf( _x( '%1$s and %2$s', 'formatted plugin list', 'tribe-common' ), $all_but_last, $last );
+ }
+
+ return '' . $html . '';
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Package_Handler.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Package_Handler.php
new file mode 100644
index 000000000..960d471f5
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Package_Handler.php
@@ -0,0 +1,157 @@
+filesystem = $wp_filesystem;
+ }
+
+ /**
+ * @return Tribe__PUE__Package_Handler
+ */
+ public static function instance() {
+ if ( empty( self::$instance ) ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Filters the package download step to store the downloaded file with a shorter file name.
+ *
+ * @param bool $reply Whether to bail without returning the package.
+ * Default false.
+ * @param string $package The package file name or URL.
+ * @param WP_Upgrader $upgrader The WP_Upgrader instance.
+ */
+ public function filter_upgrader_pre_download( $reply, $package, WP_Upgrader $upgrader ) {
+ if ( $this->is_mt_package( $package ) ) {
+ $this->upgrader = $upgrader;
+
+ return $this->download( $package );
+ }
+
+ return $reply;
+ }
+
+ /**
+ * Whether the current package is an MT plugin package or not.
+ *
+ * @param string $package The package file name or URL.
+ *
+ * @return bool
+ */
+ protected function is_mt_package( $package ) {
+ if (
+ empty( $package )
+ || ! preg_match( '!^(http|https|ftp)://!i', $package )
+ ) {
+ return false;
+ }
+
+ $query_vars = parse_url( $package, PHP_URL_QUERY );
+
+ if ( empty( $query_vars ) ) {
+ return false;
+ }
+
+ wp_parse_str( $query_vars, $parsed );
+
+ return isset( $parsed['pu_get_download'] ) && $parsed['pu_get_download'] == 1;
+ }
+
+ /**
+ * A mimic of the `WP_Upgrader::download_package` method that adds a step to store the temp file with a shorter
+ * file name.
+ *
+ * @see WP_Upgrader::download_package()
+ *
+ * @param string $package The URI of the package. If this is the full path to an
+ * existing local file, it will be returned untouched.
+ *
+ * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object.
+ */
+ protected function download( $package ) {
+ if ( empty( $this->filesystem ) ) {
+ // try to connect
+ $this->upgrader->fs_connect( [ WP_CONTENT_DIR, WP_PLUGIN_DIR ] );
+
+ global $wp_filesystem;
+
+ // still empty?
+ if ( empty( $wp_filesystem ) ) {
+ // bail
+ return false;
+ }
+
+ $this->filesystem = $wp_filesystem;
+ }
+
+ $this->upgrader->skin->feedback( 'downloading_package', $package );
+
+ $download_file = download_url( $package );
+
+ if ( is_wp_error( $download_file ) ) {
+ return new WP_Error( 'download_failed', $this->upgrader->strings['download_failed'],
+ $download_file->get_error_message() );
+ }
+
+ $file = $this->get_short_filename( $download_file );
+
+ $moved = $this->filesystem->move( $download_file, $file );
+
+ if ( empty( $moved ) ) {
+ // we tried, we failed, we bail and let WP do its job
+ return false;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the absolute path to a shorter filename version of the original download temp file.
+ *
+ * The path will point to the same temp dir (WP handled) but shortening the filename to a
+ * 6 chars hash to cope with OSes limiting the max number of chars in a file path.
+ * The original filename would be a sanitized version of the URL including query args.
+ *
+ * @param string $download_file The absolute path to the original download file.
+ *
+ * @return string The absolute path to a shorter name version of the downloaded file.
+ */
+ protected function get_short_filename( $download_file ) {
+ $extension = pathinfo( $download_file, PATHINFO_EXTENSION );
+ $filename = substr( md5( $download_file ), 0, 5 );
+ $file = dirname( $download_file ) . '/' . $filename . '.' . $extension;
+
+ return $file;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Plugin_Info.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Plugin_Info.php
new file mode 100644
index 000000000..a3dc71592
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Plugin_Info.php
@@ -0,0 +1,141 @@
+results ) ) {
+ $apiResponse = current( $apiResponse->results );
+ }
+
+ if ( empty( $apiResponse ) || ! is_object( $apiResponse ) ) {
+ return null;
+ }
+
+ //Very, very basic validation.
+ $valid = ( isset( $apiResponse->name ) && ! empty( $apiResponse->name ) && isset( $apiResponse->version ) && ! empty( $apiResponse->version ) ) || ( isset( $apiResponse->api_invalid ) || isset( $apiResponse->no_api ) );
+ if ( ! $valid ) {
+ return null;
+ }
+
+ $info = new Tribe__PUE__Plugin_Info();
+
+ foreach ( get_object_vars( $apiResponse ) as $key => $value ) {
+ $key = str_replace( 'plugin_', '', $key ); // let's strip out the "plugin_" prefix we've added in plugin-updater-classes.
+ $info->$key = $value;
+ }
+
+ return $info;
+ }
+
+ /**
+ * Transform plugin info into the format used by the native WordPress.org API
+ *
+ * @return object
+ */
+ public function to_wp_format() {
+ $info = new StdClass;
+
+ // The custom update API is built so that many fields have the same name and format
+ // as those returned by the native WordPress.org API. These can be assigned directly.
+ $sameFormat = [
+ 'name',
+ 'slug',
+ 'version',
+ 'requires',
+ 'tested',
+ 'rating',
+ 'upgrade_notice',
+ 'num_ratings',
+ 'downloaded',
+ 'homepage',
+ 'last_updated',
+ 'api_expired',
+ 'api_upgrade',
+ 'api_invalid',
+ ];
+
+ foreach ( $sameFormat as $field ) {
+ if ( isset( $this->$field ) ) {
+ $info->$field = $this->$field;
+ } else {
+ $info->$field = null;
+ }
+ }
+
+ //Other fields need to be renamed and/or transformed.
+ $info->download_link = $this->download_url;
+
+ if ( ! empty( $this->author_homepage ) ) {
+ $info->author = sprintf( '%s', esc_url( $this->author_homepage ), $this->author );
+ } else {
+ $info->author = $this->author;
+ }
+
+ if ( is_object( $this->sections ) ) {
+ $info->sections = get_object_vars( $this->sections );
+ } elseif ( is_array( $this->sections ) ) {
+ $info->sections = $this->sections;
+ } else {
+ $info->sections = [ 'description' => '' ];
+ }
+
+ return $info;
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Update_Prevention.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Update_Prevention.php
new file mode 100644
index 000000000..c5164b44f
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Update_Prevention.php
@@ -0,0 +1,199 @@
+ version ] or empty if it didn't find it.
+ */
+ public function get_dependencies( $content ) {
+ $regex = "/'(?[^']*)'(?:[^']*)'(?[^']*)',/";
+
+ if ( ! preg_match_all( $regex, $content, $matches ) ) {
+ return [];
+ }
+
+ $dependencies = array_combine( $matches['plugin'], $matches['version'] );
+
+ return $dependencies;
+ }
+
+ /**
+ * Checks for the list of constants associate with plugin to make sure we are dealing
+ * with a plugin owned by The Events Calendar.
+ *
+ * @since 4.9.12
+ *
+ * @param string $plugin Plugin file partial path, folder and main php file.
+ *
+ * @return bool
+ */
+ public function is_tribe_plugin( $plugin ) {
+ $path_constants_list = [
+ // The Events Calendar
+ 'TRIBE_EVENTS_FILE',
+
+ // Events Pro
+ 'EVENTS_CALENDAR_PRO_FILE',
+
+ // Filter bar
+ 'TRIBE_EVENTS_FILTERBAR_FILE',
+
+ // Eventbrite Tickets
+ 'EVENTBRITE_PLUGIN_FILE',
+ ];
+
+ foreach ( $path_constants_list as $constant_name ) {
+ if ( ! defined( $constant_name ) ) {
+ continue;
+ }
+
+ if ( false === strpos( constant( $constant_name ), $plugin ) ) {
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Filters the source file location for the upgrade package for the PUE Update_Prevention engine.
+ *
+ * @since 4.9.12
+ *
+ * @param string $source File source location.
+ * @param string $remote_source Remote file source location.
+ * @param WP_Upgrader $upgrader WP_Upgrader instance.
+ * @param array $extra Extra arguments passed to hooked filters.
+ */
+ public function filter_upgrader_source_selection( $source, $remote_source, $upgrader, $extras ) {
+ if ( ! isset( $extras['plugin'] ) ) {
+ return $source;
+ }
+
+ $plugin = $extras['plugin'];
+
+ // Bail if we are not dealing with a plugin we own.
+ if ( ! $this->is_tribe_plugin( $plugin ) ) {
+ return $source;
+ }
+
+ $register_path = $source . '/src/Tribe/Plugin_Register.php';
+
+ // Bail when the Plugin Register file doesn't exist.
+ if ( ! file_exists( $register_path ) ) {
+ return $source;
+ }
+
+ $register_contents = file_get_contents( $register_path );
+
+ $dependencies = $this->get_dependencies( $register_contents );
+ $incompatible_plugins = [];
+
+ foreach ( $dependencies as $class_name => $required_version ) {
+ // Skip inactive plugin checks.
+ if ( ! class_exists( $class_name ) ) {
+ continue;
+ }
+
+ $constant_name = $class_name . '::VERSION';
+
+ // Skip if we can't find the version constant.
+ if ( ! defined( $constant_name ) ) {
+ continue;
+ }
+
+ $current_version = constant( $constant_name );
+
+ // Skip when the version is equal or higher than the required.
+ if ( version_compare( $current_version, $required_version, '>=' ) ) {
+ continue;
+ }
+
+ $pue = tribe( Dependency::class )->get_pue_from_class( $class_name );
+ $has_pue_notice = $pue ? tribe( 'pue.notices' )->has_notice( $pue->pue_install_key ) : false;
+
+ // Only throw warning for customers with notices of invalid/expired licenses.
+ if ( ! $has_pue_notice ) {
+ continue;
+ }
+
+ // Flag that we should prevent the Update
+ $incompatible_plugins[ $class_name ] = $required_version;
+ }
+
+ // Bail when there are no incompatible plugins.
+ if ( empty( $incompatible_plugins ) ) {
+ return $source;
+ }
+
+ /**
+ * Filter the if we should prevent the update.
+ *
+ * @since 4.9.12
+ *
+ * @param bool $should_revent Flag false to skip the prevention.
+ * @param array $incompatible_plugins Which plugins were incompatible with new version of the plugin.
+ * @param string $source File source location.
+ * @param string $remote_source Remote file source location.
+ * @param WP_Upgrader $upgrader WP_Upgrader instance.
+ * @param array $extra Extra arguments passed to hooked filters.
+ */
+ $should_prevent_update = apply_filters(
+ 'tribe_pue_should_prevent_update_without_license',
+ true,
+ $incompatible_plugins,
+ $source,
+ $remote_source,
+ $upgrader,
+ $extras
+ );
+
+ // Bail if the filter above returns anything but true.
+ if ( true !== $should_prevent_update ) {
+ return $source;
+ }
+
+ $full_plugin_path = $remote_source . '/' . $plugin;
+ $plugin_data = get_plugin_data( $full_plugin_path );
+
+ $plugins_classes = array_keys( $incompatible_plugins );
+ $plugins_list_html = tribe( 'pue.notices' )->get_formatted_plugin_names_from_classes( $plugins_classes );
+
+ $link_read_more = '' . esc_html__( 'Read more', 'tribe-common' ) . '.';
+
+ $message = sprintf(
+ esc_html__( 'Your update failed due to an incompatibility between the version (%1$s) of the %2$s you tried to update to and the version of %3$s that you are using. %4$s', 'tribe-common' ),
+ esc_html( $plugin_data['Version'] ),
+ esc_html( $plugin_data['Name'] ),
+ $plugins_list_html,
+ $link_read_more
+ );
+
+ $error = new WP_Error(
+ 'tribe-updater-failed-prevention',
+ $message,
+ []
+ );
+
+ return $error;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Utility.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Utility.php
new file mode 100644
index 000000000..07b6bbf5d
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Utility.php
@@ -0,0 +1,126 @@
+$field ) ) {
+ continue;
+ }
+
+ $update->$field = $info->$field;
+ }
+
+ return $update;
+ }
+
+ /**
+ * Transform the update into the format used by WordPress native plugin API.
+ *
+ * @return object
+ */
+ public function to_wp_format() {
+ $update = new StdClass;
+
+ $update->id = $this->id;
+ $update->plugin = $this->plugin;
+ $update->slug = $this->slug;
+ $update->new_version = $this->version;
+ $update->url = $this->homepage;
+ $update->package = $this->download_url;
+ if ( ! empty( $this->upgrade_notice ) ) {
+ $update->upgrade_notice = $this->upgrade_notice;
+ }
+
+ // Support custom $update properties coming straight from PUE
+ if ( ! empty( $this->custom_update ) ) {
+ $custom_update = get_object_vars( $this->custom_update );
+
+ foreach ( $custom_update as $field => $custom_value ) {
+ if ( is_object( $custom_value ) ) {
+ $custom_value = get_object_vars( $custom_value );
+ }
+
+ $update->$field = $custom_value;
+ }
+ }
+
+ return $update;
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugin_Meta_Links.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugin_Meta_Links.php
new file mode 100644
index 000000000..fd674cbfb
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugin_Meta_Links.php
@@ -0,0 +1,147 @@
+ Plugins list
+ */
+class Tribe__Plugin_Meta_Links {
+ /**
+ * Class instance
+ *
+ * @var Tribe__Plugin_Meta_Links The singleton instance.
+ */
+ private static $instance;
+
+ /**
+ * The various meta links that will be added
+ *
+ * @var array {
+ * Each plugin that will be filtered.
+ *
+ * @type array $plugin_basename {
+ * Meta links added to this plugin.
+ *
+ * @type array {
+ * Each individual link.
+ *
+ * @type string $html The full HTML for this link.
+ * @type bool $remove Whether we are adding or removing this link.
+ * }
+ * }
+ * }
+ */
+ private $meta_links = [];
+
+ /**
+ * Returns the singleton instance of this class.
+ *
+ * @return Tribe__Plugin_Meta_Links instance.
+ */
+ public static function instance() {
+ return null === self::$instance ? new self() : self::$instance;
+ }
+
+ private function __construct() {
+ add_action( 'plugin_row_meta', [ $this, 'filter_meta_links' ], 10, 2 );
+ }
+
+ /**
+ * Adds an link to the meta list
+ *
+ * @param string $plugin Path to plugin file.
+ * @param string $text Inner text for HTML element.
+ * @param string $href URL for the link.
+ * @param array $attributes Key => value attributes for element.
+ */
+ public function add_link( $plugin, $title, $href, $attributes = [] ) {
+ $attributes['href'] = $href;
+
+ // Build the element.
+ $html = ' $val ) {
+ $html .= ' ' . $att . '="' . esc_attr( $val ) . '"';
+ }
+
+ $html .= '>' . esc_html( $title ) . '';
+
+ $this->set( $plugin, $html, false );
+ }
+
+ /**
+ * Adds or removes the specified HTML link
+ *
+ * @param string $plugin Path to plugin file.
+ * @param string $html Full HTML for this link.
+ * @param bool $remove Whether to add this HTML/link or match and remove it.
+ */
+ public function set( $plugin, $html, $remove = false ) {
+ $basename = plugin_basename( $plugin );
+
+ // Get any current links for this plugin.
+ $cur_links = Tribe__Utils__Array::get( $this->meta_links, $basename, [] );
+
+ $cur_links[] = [
+ 'html' => $html,
+ 'remove' => $remove,
+ ];
+
+ $this->meta_links = Tribe__Utils__Array::set( $this->meta_links, $basename, $cur_links );
+ }
+
+ /**
+ * Filters meta links on the plugins list page
+ *
+ * @param array $links The current plugin's links.
+ * @param string $basename The plugin currently being filtered.
+ *
+ * @return array Filtered action links array.
+ */
+ public function filter_meta_links( $links, $basename ) {
+ // Gets any links that are set for this plugin, defaults to an empty array.
+ $set_links = Tribe__Utils__Array::get( $this->meta_links, $basename, [] );
+
+ foreach ( $set_links as $link ) {
+
+ if ( true === $link['remove'] ) {
+ // Remove a link.
+ $pos = array_search( $link['html'], $links );
+
+ if ( false !== $pos ) {
+ unset( $links[ $pos ] );
+ }
+ } else {
+ // Add a link.
+ $links[] = $link['html'];
+ }
+ }
+
+ return $links;
+ }
+
+ /**
+ * Prevent cloning the singleton with 'clone' operator
+ *
+ * @return void
+ */
+ final public function __clone() {
+ _doing_it_wrong(
+ __FUNCTION__,
+ 'Can not use this method on singletons.',
+ '4.3'
+ );
+ }
+
+ /**
+ * Prevent unserializing the singleton instance
+ *
+ * @return void
+ */
+ final public function __wakeup() {
+ _doing_it_wrong(
+ __FUNCTION__,
+ 'Can not use this method on singletons.',
+ '4.3'
+ );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins.php
new file mode 100644
index 000000000..5d1482fc1
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins.php
@@ -0,0 +1,184 @@
+ Common name for the plugin, used in places such as WP Admin messages
+ * 'class' => Main plugin class
+ * 'thickbox_url' => Download or purchase URL for plugin from within /wp-admin/ thickbox
+ * ]
+ */
+ private $tribe_plugins = [
+ [
+ 'short_name' => 'Event Tickets',
+ 'class' => 'Tribe__Tickets__Main',
+ 'thickbox_url' => 'plugin-install.php?tab=plugin-information&plugin=event-tickets&TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Event Tickets Plus',
+ 'class' => 'Tribe__Tickets_Plus__Main',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-event-tickets-plus/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'The Events Calendar',
+ 'class' => 'Tribe__Events__Main',
+ 'thickbox_url' => 'plugin-install.php?tab=plugin-information&plugin=the-events-calendar&TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Events Calendar Pro',
+ 'class' => 'Tribe__Events__Pro__Main',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-events-calendar-pro/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Community Events',
+ 'class' => 'Tribe__Events__Community__Main',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-community-events/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Community Tickets',
+ 'class' => 'Tribe__Events__Community__Tickets__Main',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/community-tickets/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Filter Bar',
+ 'class' => 'Tribe__Events__Filterbar__View',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-events-filterbar/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Facebook Events',
+ 'class' => 'Tribe__Events__Facebook__Importer',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/facebook-events/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'iCal Importer',
+ 'class' => 'Tribe__Events__Ical_Importer__Main',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/ical-importer/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Eventbrite Tickets',
+ 'class' => 'Tribe__Events__Tickets__Eventbrite__Main',
+ 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-eventbrite-tickets/?TB_iframe=true',
+ ],
+ [
+ 'short_name' => 'Advanced Post Manager',
+ 'class' => 'Tribe_APM',
+ 'thickbox_url' => 'plugin-install.php?tab=plugin-information&plugin=advanced-post-manager&TB_iframe=true',
+ ],
+ ];
+
+ /**
+ * Searches the plugin list for key/value pair and return the full details for that plugin
+ *
+ * @param string $search_key The array key this value will appear in
+ * @param string $search_val The value itself
+ *
+ * @return array|null
+ */
+ public function get_plugin_by_key( $search_key, $search_val ) {
+ foreach ( $this->get_list() as $plugin ) {
+ if ( isset( $plugin[ $search_key ] ) && $plugin[ $search_key ] === $search_val ) {
+ return $plugin;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieves plugins details by plugin name
+ *
+ * @param string $name Common name for the plugin, not necessarily the lengthy name in the WP Admin Plugins list
+ *
+ * @return array|null
+ */
+ public function get_plugin_by_name( $name ) {
+ return $this->get_plugin_by_key( 'short_name', $name );
+ }
+
+ /**
+ * Retrieves plugins details by class name
+ *
+ * @param string $main_class Main/base class for this plugin
+ *
+ * @return array|null
+ */
+ public function get_plugin_by_class( $main_class ) {
+ return $this->get_plugin_by_key( 'class', $main_class );
+ }
+
+ /**
+ * Retrieves the entire list
+ *
+ * @return array
+ */
+ public function get_list() {
+ /**
+ * Gives an opportunity to filter the list of tribe plugins
+ *
+ * @since 4.7.18
+ *
+ * @param array Contains a list of all tribe plugins
+ */
+ return apply_filters( 'tribe_plugins_get_list', $this->tribe_plugins );
+ }
+
+ /**
+ * Checks if given plugin is active. Usually a The Events Calendar plugin.
+ *
+ * @param string $plugin_name The name of the plugin. Each plugin defines their name upon hooking on the filter.
+ *
+ * @since 4.12.1
+ *
+ * @return bool True if plugin is active. False if plugin is not active.
+ */
+ public static function is_active( $plugin_name ) {
+ if ( ! did_action( "plugins_loaded" ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'Using this function before "plugins_loaded" action has fired can return unreliable results.', 'tribe-common' ),
+ '4.12.6'
+ );
+ }
+
+ /**
+ * Filters the array that each Tribe plugin overrides to
+ * set itself as active when this function is called.
+ *
+ * @example [ 'the-events-calendar' => true, 'event-tickets' => true ]
+ *
+ * @since 4.12.1
+ *
+ * @return array Plugin slugs as keys and bool as value for whether it's active or not.
+ */
+ $plugins = apply_filters( 'tribe_active_plugins', [] );
+
+ return isset( $plugins[ $plugin_name ] ) && tribe_is_truthy( $plugins[ $plugin_name ] );
+ }
+
+ /**
+ * Retrieves a list of active plugins.
+ *
+ * @since 5.1.3
+ *
+ * @return array List of active plugins.
+ */
+ public function get_active_plugins() {
+ $plugins = $this->get_list();
+
+ $plugins = array_filter( $plugins, function ( $plugin ) {
+ return self::is_active( $plugin['short_name'] );
+ } );
+
+ return $plugins;
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins_API.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins_API.php
new file mode 100644
index 000000000..471986ec9
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins_API.php
@@ -0,0 +1,289 @@
+ [
+ 'title' => __( 'The Events Calendar', 'tribe-common' ),
+ 'slug' => 'the-events-calendar',
+ 'link' => 'https://evnt.is/1ai-',
+ 'plugin-dir' => 'the-events-calendar',
+ 'main-file' => 'the-events-calendar.php',
+ 'description' => __( 'Our flagship free calendar', 'tribe-common' ),
+ 'description-help' => __( 'The #1 calendar for WordPress', 'tribe-common' ),
+ 'features' => [
+ __( 'Customizable', 'tribe-common' ),
+ __( 'Import & export events', 'tribe-common' ),
+ __( 'Timezone support', 'tribe-common' ),
+ __( 'Multiple views', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/calendar.jpg',
+ 'logo' => 'images/logo/the-events-calendar.svg',
+ 'is_installed' => class_exists( 'Tribe__Events__Main' ),
+ 'free' => true,
+ 'active_installs' => 800000,
+ ],
+ 'event-aggregator' => [
+ 'title' => __( 'Event Aggregator', 'tribe-common' ),
+ 'slug' => 'event-aggregator',
+ 'link' => 'https://evnt.is/1aj0',
+ 'plugin-dir' => '',
+ 'main-file' => '',
+ 'description' => __( 'Automated imports for your calendar', 'tribe-common' ),
+ 'description-help' => __( 'Import events from Meetup, Eventbrite, iCal, Google Calendar, and more.', 'tribe-common' ),
+ 'features' => [
+ __( 'Schedule automated imports', 'tribe-common' ),
+ __( 'Customizable', 'tribe-common' ),
+ __( 'Works with Google Calendar, Meetup, and more', 'tribe-common' ),
+ __( 'Refine by date, location, or keyword', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/aggregator.jpg',
+ 'logo' => 'images/logo/event-aggregator.svg',
+ 'is_installed' => class_exists( 'Tribe__Events__Aggregator' ) && Tribe__Events__Aggregator::is_service_active(),
+ 'free' => false,
+ 'active_installs' => 20000,
+ ],
+ 'event-automator' => [
+ 'title' => __( 'Event Automator', 'tribe-common' ),
+ 'slug' => 'event-automator',
+ 'link' => 'https://evnt.is/1bc7',
+ 'plugin-dir' => '',
+ 'main-file' => '',
+ 'description' => __( 'Automate your event workflow with Zapier—no code required!', 'tribe-common' ),
+ 'description-help' => __( 'Connect The Events Calendar and Event Tickets with thousands of the most popular apps.', 'tribe-common' ),
+ 'features' => [
+ __( 'Lead Management', 'tribe-common' ),
+ __( 'Communication Outreach', 'tribe-common' ),
+ __( 'Internal Processes', 'tribe-common' ),
+ __( 'Data Management', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/automator.svg',
+ 'logo' => 'images/logo/event-automator.svg',
+ 'is_installed' => class_exists( 'TEC\Event_Automator\Plugin' ),
+ 'free' => false,
+ 'active_installs' => 0,
+ ],
+ 'events-calendar-pro' => [
+ 'title' => __( 'Events Calendar Pro', 'tribe-common' ),
+ 'slug' => 'events-calendar-pro',
+ 'link' => 'https://evnt.is/1ai-',
+ 'plugin-dir' => 'events-calendar-pro',
+ 'main-file' => 'events-calendar-pro.php',
+ 'description' => __( 'Power up your calendar with Pro', 'tribe-common' ),
+ 'description-help' => __( 'The #1 calendar for WordPress', 'tribe-common' ),
+ 'features' => [
+ __( 'Premium support', 'tribe-common' ),
+ __( 'Recurring events & series', 'tribe-common' ),
+ __( 'Additional views', 'tribe-common' ),
+ __( 'Shortcodes', 'tribe-common' ),
+ __( 'Duplicate events', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/pro.jpg',
+ 'logo' => 'images/logo/events-calendar-pro.svg',
+ 'is_installed' => class_exists( 'Tribe__Events__Pro__Main' ),
+ 'free' => false,
+ 'active_installs' => 100000,
+ ],
+ 'event-tickets' => [
+ 'title' => __( 'Event Tickets', 'tribe-common' ),
+ 'slug' => 'event-tickets',
+ 'link' => 'https://evnt.is/1aj1',
+ 'plugin-dir' => 'event-tickets',
+ 'main-file' => 'event-tickets.php',
+ 'description' => __( 'Manage ticketing and RSVPs', 'tribe-common' ),
+ 'description-help' => __( 'Collect RSVPs and sell tickets', 'tribe-common' ),
+ 'features' => [
+ __( 'Add tickets and RSVP to any post', 'tribe-common' ),
+ __( 'Paypal integration', 'tribe-common' ),
+ __( 'Attendee reports', 'tribe-common' ),
+ __( 'Customizable ticket template', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/tickets.jpg',
+ 'logo' => 'images/logo/event-tickets.svg',
+ 'is_installed' => class_exists( 'Tribe__Tickets__Main' ),
+ 'free' => true,
+ 'active_installs' => 20000,
+ ],
+ 'event-tickets-plus' => [
+ 'title' => __( 'Event Tickets Plus', 'tribe-common' ),
+ 'slug' => 'event-tickets-plus',
+ 'link' => 'http://evnt.is/1aj1',
+ 'plugin-dir' => 'event-tickets-plus',
+ 'main-file' => 'event-tickets-plus.php',
+ 'description' => __( 'Monetize your events', 'tribe-common' ),
+ 'description-help' => __( 'Collect RSVPs and sell tickets', 'tribe-common' ),
+ 'features' => [
+ __( 'Custom registration fields', 'tribe-common' ),
+ __( 'WooCommerce compatibility', 'tribe-common' ),
+ __( 'Ticket scanning with mobile app', 'tribe-common' ),
+ __( 'Custom attendee registration fields', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/tickets-plus.jpg',
+ 'logo' => 'images/logo/event-tickets-plus.svg',
+ 'is_installed' => class_exists( 'Tribe__Tickets_Plus__Main' ),
+ 'free' => false,
+ 'active_installs' => 10000,
+ ],
+ 'promoter' => [
+ 'title' => __( 'Promoter', 'tribe-common' ),
+ 'slug' => 'promoter',
+ 'link' => 'https://evnt.is/1acy',
+ 'plugin-dir' => '',
+ 'main-file' => '',
+ 'description' => __( 'An email marketing solution for events and the people running them', 'tribe-common' ),
+ 'description-help' => __( 'Email marketing to promote your events', 'tribe-common' ),
+ 'features' => [
+ __( 'Automate email touchpoints', 'tribe-common' ),
+ __( 'Customize email templates', 'tribe-common' ),
+ __( 'Streamline your email process', 'tribe-common' ),
+ __( 'Segment your attendee lists', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/promoter.jpg',
+ 'logo' => 'images/logo/promoter.svg',
+ 'is_installed' => tribe( 'promoter.pue' )->has_license_key(),
+ 'free' => false,
+ 'active_installs' => 1000,
+ ],
+ 'tribe-filterbar' => [
+ 'title' => __( 'Filter Bar', 'tribe-common' ),
+ 'slug' => 'tribe-filterbar',
+ 'link' => 'https://evnt.is/19o6',
+ 'plugin-dir' => 'the-events-calendar-filterbar',
+ 'main-file' => 'the-events-calendar-filter-view.php',
+ 'description' => __( 'Help users find exactly the right event', 'tribe-common' ),
+ 'description-help' => __( 'Allow users to search for events by category, tag, venue, organizer, day of the week, time of day, and price.', 'tribe-common' ),
+ 'features' => [
+ __( 'Configurable set of filters', 'tribe-common' ),
+ __( 'Horizontal or vertical', 'tribe-common' ),
+ __( 'Filter category, price, and more', 'tribe-common' ),
+ __( 'Filter distance (for Events Calendar Pro)', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/filter-bar.jpg',
+ 'logo' => 'images/logo/filterbar.svg',
+ 'is_installed' => class_exists( 'Tribe__Events__Filterbar__View' ),
+ 'free' => false,
+ 'active_installs' => 20000,
+ ],
+ 'events-community' => [
+ 'title' => __( 'Community Events', 'tribe-common' ),
+ 'slug' => 'events-community',
+ 'link' => 'https://evnt.is/19o7',
+ 'plugin-dir' => 'the-events-calendar-community-events',
+ 'main-file' => 'tribe-community-events.php',
+ 'description' => __( 'Users submit events to your calendar', 'tribe-common' ),
+ 'description-help' => __( 'Enable 3rd party event submissions.', 'tribe-common' ),
+ 'features' => [
+ __( 'Publishing Control', 'tribe-common' ),
+ __( 'Event Submission Form', 'tribe-common' ),
+ __( 'Registered User Settings', 'tribe-common' ),
+ __( 'Email notifications', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/community.jpg',
+ 'logo' => 'images/logo/community-events.svg',
+ 'is_installed' => class_exists( 'Tribe__Events__Community__Main' ),
+ 'free' => false,
+ 'active_installs' => 20000,
+ ],
+ 'events-community-tickets' => [
+ 'title' => __( 'Community Tickets', 'tribe-common' ),
+ 'slug' => 'events-community-tickets',
+ 'link' => 'https://evnt.is/19o8',
+ 'plugin-dir' => 'the-events-calendar-community-events-tickets',
+ 'main-file' => 'events-community-tickets.php',
+ 'description' => __( 'Run your own events marketplace', 'tribe-common' ),
+ 'description-help' => __( 'Let users create and sell tickets for events they submit to your calendar.', 'tribe-common' ),
+ 'features' => [
+ __( 'Users submit events and sell tickets', 'tribe-common' ),
+ __( 'Split commission with users', 'tribe-common' ),
+ __( 'No admin access required', 'tribe-common' ), /* code review: fail this */
+ __( 'Sales reporting', 'tribe-common' ),
+ ],
+ 'requires' => _x( 'Event Tickets Plus and Community Events', 'Names of required plugins for Community Tickets', 'tribe-common' ),
+ 'image' => 'images/shop/community-tickets.jpg',
+ 'logo' => 'images/logo/community-tickets.svg',
+ 'is_installed' => class_exists( 'Tribe__Events__Community__Tickets__Main' ),
+ 'free' => false,
+ 'active_installs' => 10000,
+ ],
+ 'tribe-eventbrite' => [
+ 'title' => __( 'Eventbrite Tickets', 'tribe-common' ),
+ 'slug' => 'tribe-eventbrite',
+ 'link' => 'https://evnt.is/19o9',
+ 'plugin-dir' => 'the-events-calendar-eventbrite-tickets',
+ 'main-file' => 'tribe-eventbrite.php',
+ 'description' => __( 'Unite the power of TEC with the ticketing of Eventbrite', 'tribe-common' ),
+ 'description-help' => __( 'Create Eventbrite tickets and events right from your WordPress dashboard.', 'tribe-common' ),
+ 'features' => [
+ __( 'Manage tickets from WordPress', 'tribe-common' ),
+ __( 'Ticket availability automatically updates', 'tribe-common' ),
+ __( 'Integrated with your events on Eventbrite', 'tribe-common' ),
+ __( 'Automatically import your events', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/eventbrite.jpg',
+ 'logo' => 'images/logo/eventbrite-tickets.svg',
+ 'is_installed' => class_exists( 'Tribe__Events__Tickets__Eventbrite__Main' ),
+ 'free' => false,
+ 'active_installs' => 20000,
+ ],
+ 'image-widget-plus' => [
+ 'title' => __( 'Image Widget Plus', 'tribe-common' ),
+ 'slug' => 'image-widget-plus',
+ 'link' => 'https://evnt.is/19nv',
+ 'plugin-dir' => 'image-widget-plus',
+ 'main-file' => 'image-widget-plus.php',
+ 'description' => __( 'Beautiful display options for your favorite photos.', 'tribe-common' ),
+ 'features' => [
+ __( 'Multi-Image Support', 'tribe-common' ),
+ __( 'Lightbox', 'tribe-common' ),
+ __( 'Slideshow', 'tribe-common' ),
+ __( 'Random Images', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/image-widget-plus.jpg',
+ 'logo' => 'images/logo/image-widget-plus.svg',
+ 'is_installed' => class_exists( 'Tribe__Image__Plus__Main' ),
+ 'free' => false,
+ 'active_installs' => 2500,
+ ],
+ 'events-virtual' => [
+ 'title' => __( 'Virtual Events', 'tribe-common' ),
+ 'slug' => 'events-virtual',
+ 'link' => 'http://evnt.is/virtual-events',
+ 'plugin-dir' => 'events-virtual',
+ 'main-file' => 'events-virtual.php',
+ 'description' => __( 'Features to optimize your calendar for virtual events.', 'tribe-common' ),
+ 'description-help' => __( 'Highlight virtual events on you calendar and integrate with your favorite online meeting tools.', 'tribe-common' ),
+ 'features' => [
+ __( 'Zoom integration', 'tribe-common' ),
+ __( 'Virtual event labels', 'tribe-common' ),
+ __( 'Status control for canceled or postponed events', 'tribe-common' ),
+ __( 'Embed livestreams and videos', 'tribe-common' ),
+ ],
+ 'image' => 'images/shop/virtual-events.jpg',
+ 'logo' => 'images/logo/virtual-events.svg',
+ 'is_installed' => defined( 'EVENTS_VIRTUAL_FILE' ),
+ 'free' => false,
+ 'active_installs' => 2500,
+ ],
+ ];
+
+ return $products;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_History.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_History.php
new file mode 100644
index 000000000..fc21e25b8
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_History.php
@@ -0,0 +1,134 @@
+post_id = $post_id;
+ }
+
+ /**
+ * Records a new history entry for the current post.
+ *
+ * @param string $message
+ * @param array $data
+ */
+ public function add_entry( $message, array $data = [] ) {
+ $datetime = current_time( 'mysql' );
+ $checksum = uniqid( substr( hash( 'md5', $datetime . $message . serialize( $data ) ), 0, 8 ) . '_' );
+
+ $log_entry = wp_slash( json_encode( [
+ 'datetime' => $datetime,
+ 'message' => $message,
+ 'data' => $data,
+ 'checksum' => $checksum,
+ ] ) );
+
+ add_post_meta( $this->post_id, self::HISTORY_KEY, $log_entry );
+ }
+
+ /**
+ * Indicates if any history exists for the current post.
+ *
+ * @return bool
+ */
+ public function has_entries() {
+ $first_available_entry = get_post_meta( $this->post_id, self::HISTORY_KEY, true );
+ return ! empty( $first_available_entry );
+ }
+
+ /**
+ * Returns all historical records for the current post as an array
+ * of objects, each object taking the form:
+ *
+ * {
+ * "datetime": "yyyy-mm-dd hh:ii:ss",
+ * "message": "...",
+ * "data": []
+ * }
+ *
+ * @return array
+ */
+ public function get_entries() {
+ $entries = [];
+
+ foreach ( get_post_meta( $this->post_id, self::HISTORY_KEY ) as $log_entry ) {
+ $log_entry = json_decode( $log_entry );
+
+ if ( ! $log_entry ) {
+ continue;
+ }
+
+ $entries[] = $log_entry;
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Deletes all entries for the current post that match the provided datetime
+ * string and (optionally) also match the provided checksum.
+ *
+ * Returns the total number of deleted entries, which may be zero if none were matched;
+ * can also be more than one if multiple entries were logged at the same time and no
+ * checksum is provided.
+ *
+ * @param string $datetime
+ * @param string $checksum optional value to more precisely specify the entry to be deleted
+ *
+ * @return int
+ */
+ public function delete_entry( $datetime, $checksum = null ) {
+ $deleted = 0;
+
+ foreach ( $this->get_entries() as $entry ) {
+ if ( $entry->datetime !== $datetime ) {
+ continue;
+ }
+
+ if ( null !== $checksum && $entry->checksum !== $checksum ) {
+ continue;
+ }
+
+ if ( delete_post_meta( $this->post_id, self::HISTORY_KEY, json_encode( $entry ) ) ) {
+ $deleted++;
+ }
+ }
+
+ return $deleted;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_Transient.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_Transient.php
new file mode 100644
index 000000000..65655ffa7
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_Transient.php
@@ -0,0 +1,214 @@
+ID;
+ }
+
+ if ( has_filter( 'tribe_pre_post_meta_transient_' . $transient ) ) {
+ /**
+ * Attach an action before getting the new Transient
+ *
+ * @since 4.1
+ *
+ * @param int $post_id Post ID
+ * @param string $transient The Post Meta Key
+ */
+ $pre = apply_filters( 'tribe_pre_post_meta_transient_' . $transient, $post_id, $transient );
+ if ( false !== $pre ) {
+ return $pre;
+ }
+ }
+
+ if ( $_wp_using_ext_object_cache ) {
+ $value = wp_cache_get( "tribe_{$transient}-{$post_id}", "tribe_post_meta_transient-{$post_id}" );
+ } else {
+ $meta_timeout = '_transient_timeout_' . $transient;
+ $meta = '_transient_' . $transient;
+ $value = get_post_meta( $post_id, $meta, false );
+
+ // if there aren't any values, communicate that it did not fetch data from post transient
+ if ( ! is_array( $value ) || 0 === count( $value ) ) {
+ return false;
+ }
+
+ // grab the first value, because that's all we care about
+ $value = current( $value );
+
+ if ( $value && ! defined( 'WP_INSTALLING' ) ) {
+ if ( get_post_meta( $post_id, $meta_timeout, true ) < time() ) {
+ $this->delete( $post_id, $transient );
+
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Attach an action after getting the new Transient
+ *
+ * @since 4.1
+ *
+ * @param int $post_id Post ID
+ * @param string $transient The Post Meta Key
+ */
+ return has_filter( 'tribe_post_meta_transient_' . $transient )
+ ? apply_filters( 'tribe_post_meta_transient_' . $transient, $value, $post_id )
+ : $value;
+ }
+
+ /**
+ * Delete a post meta transient.
+ *
+ * @since 4.1
+ *
+ * @param int $post_id The Post ID, can also be a WP_Post.
+ * @param string $transient Post Meta to Delete.
+ * @param string $value Only delete if the value Matches.
+ *
+ * @return boolean If we were able to delete the transient.
+ */
+ public function delete( $post_id, $transient, $value = null ) {
+ global $_wp_using_ext_object_cache;
+
+ if ( is_numeric( $post_id ) ) {
+ $post_id = (int) $post_id;
+ } else {
+ $post = get_post( $post_id );
+ $post_id = $post->ID;
+ }
+
+ /**
+ * Use this to pre attach an action to deleting a Post Transient
+ *
+ * @since 4.1
+ *
+ * @param int $post_id Post ID
+ * @param string $transient The Post Meta Key
+ */
+ do_action( 'tribe_delete_post_meta_transient_' . $transient, $post_id, $transient );
+
+ if ( $_wp_using_ext_object_cache ) {
+ $result = wp_cache_delete( "tribe_{$transient}-{$post_id}", "tribe_post_meta_transient-{$post_id}" );
+ } else {
+ $meta_timeout = '_transient_timeout_' . $transient;
+ $meta = '_transient_' . $transient;
+ $result = delete_post_meta( $post_id, $meta, $value );
+ if ( $result ) {
+ delete_post_meta( $post_id, $meta_timeout, $value );
+ }
+ }
+
+ if ( $result ) {
+ /**
+ * Use this to attach an Action to when the Transient is deleted
+ *
+ * @since 4.1
+ *
+ * @param int $post_id Post ID
+ * @param string $transient The Post Meta Key
+ */
+ do_action( 'tribe_deleted_post_meta_transient', $transient, $post_id, $transient );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Sets a new value for the Transient.
+ *
+ * @since 4.1
+ *
+ * @param int $post_id The Post ID, can also be a WP_Post.
+ * @param string $transient Post Meta to set.
+ * @param string $value Only delete if the value Matches.
+ * @param int $expiration How long this transient will be valid, in seconds.
+ *
+ * @return int|false Meta ID on success, false on failure.
+ */
+ public function set( $post_id, $transient, $value, $expiration = 0 ) {
+ global $_wp_using_ext_object_cache;
+
+ if ( is_numeric( $post_id ) ) {
+ $post_id = (int) $post_id;
+ } else {
+ $post = get_post( $post_id );
+ $post_id = $post->ID;
+ }
+
+ $this->delete( $post_id, $transient );
+
+ /**
+ * Attach an action before setting the new Transient
+ *
+ * @since 4.1
+ *
+ * @param int $post_id Post ID
+ * @param string $transient The Post Meta Key
+ */
+ if ( has_filter( 'tribe_pre_set_post_meta_transient_' . $transient ) ) {
+ $value = apply_filters( 'tribe_pre_set_post_meta_transient_' . $transient, $value, $post_id, $transient );
+ }
+
+ if ( $_wp_using_ext_object_cache ) {
+ $result = wp_cache_set( "tribe_{$transient}-{$post_id}", $value, "tribe_post_meta_transient-{$post_id}", $expiration );
+ } else {
+ $meta_timeout = '_transient_timeout_' . $transient;
+ $meta = '_transient_' . $transient;
+ if ( $expiration ) {
+ add_post_meta( $post_id, $meta_timeout, time() + $expiration, true );
+ }
+ $result = add_post_meta( $post_id, $meta, $value, true );
+ }
+
+ if ( $result ) {
+ /**
+ * Attach an action after setting the new Transient
+ *
+ * @since 4.1
+ *
+ * @param int $post_id Post ID
+ * @param string $transient The Post Meta Key
+ */
+ do_action( 'tribe_set_post_meta_transient_' . $transient, $post_id, $transient );
+ }
+
+ return $result;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Handler.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Handler.php
new file mode 100644
index 000000000..ccad1f680
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Handler.php
@@ -0,0 +1,361 @@
+action = call_user_func( [ $class, 'action' ] );
+ $this->identifier = $this->prefix . '_' . $this->action;
+
+ add_action( 'wp_ajax_' . $this->identifier, [ $this, 'maybe_handle' ] );
+
+ /**
+ * Filters whether background processing should be triggered and handled on
+ * non-private AJAX requests as the ones triggered by a non logged in user.
+ * Defaults to `true` to exploit any possible chance to process.
+ *
+ * @since 4.9.5
+ *
+ * @param bool $allow_nopriv Whether background processing should be triggered and handled on
+ * non-private AJAX requests or not.
+ * @param static $this This handler instance.
+ */
+ $allow_nopriv = apply_filters( 'tribe_process_allow_nopriv_handling', true, $this );
+
+ if ( $allow_nopriv ) {
+ add_action( 'wp_ajax_nopriv_' . $this->identifier, [ $this, 'maybe_handle' ] );
+ }
+
+ $this->healthcheck_cron_hook_id = $this->identifier;
+ $this->feature_detection = tribe( 'feature-detection' );
+
+ /*
+ * This object might have been built while processing crons so
+ * we hook on the the object cron identifier to handle the task
+ * if the cron-triggered action ever fires.
+ */
+ add_action( $this->healthcheck_cron_hook_id, [ $this, 'maybe_handle' ] );
+ }
+
+ /**
+ * Returns the async process action name.
+ *
+ * Extending classes must override this method to return their unique action slug.
+ *
+ * @since 4.7.12
+ *
+ * @return string
+ *
+ * @throws RuntimeException If the extending class does not override this method.
+ */
+ public static function action() {
+ $class = static::class;
+ throw new RuntimeException( "Class {$class} should override the `action` method to define its own unique identifier." );
+ }
+
+ /**
+ * Handles the process request if valid and if authorized.
+ *
+ * @since 4.7.23
+ * @since 4.9.5 Pulled the `maybe_handle` implementation of the `WP_Async_Request` class.
+ *
+ * @param array|null $data_source A source of data if not provided in the request; used for
+ * cron-based fallback.
+ */
+ public function maybe_handle( $data_source = null ) {
+ $data_source = (array) $data_source;
+
+ if ( $this->feature_detection->supports_async_process() ) {
+ // Don't lock up other requests while processing.
+ session_write_close();
+
+ check_ajax_referer( $this->identifier, 'nonce' );
+
+ // Let's make sure to hydrate date from the request if not set.
+ if ( count( array_filter( $data_source ) ) < 1 ) {
+ $data_source = $_POST;
+ }
+
+ do_action(
+ 'tribe_log',
+ 'debug',
+ $this->identifier,
+ [
+ 'action' => 'async_handling',
+ 'data_source' => $data_source,
+ 'payload' => $_POST,
+ ]
+ );
+
+ $this->handle( $data_source );
+
+ wp_die();
+ }
+
+ /*
+ * If the environment does not support AJAX-based async processing then
+ * fallback to use the cron-based approach and just call the handle method
+ * removing it first from the action to avoid multiple calls.
+ */
+ wp_clear_scheduled_hook( $this->healthcheck_cron_hook_id, [ $data_source ] );
+
+ do_action(
+ 'tribe_log',
+ 'debug',
+ $this->identifier,
+ array_merge( [ 'action' => 'cron_handling' ], $data_source ) );
+
+ $this->handle( $data_source );
+ }
+
+ /**
+ * Overrides the base `dispatch` method to allow for constants and/or environment vars to run
+ * async requests in sync mode.
+ *
+ * @since 4.7.12
+ * @since 4.9.5 Pulled `dispatch` method logic from the `WP_Async_Request` class.
+ *
+ * @return mixed
+ */
+ public function dispatch() {
+ if (
+ ( defined( 'TRIBE_NO_ASYNC' ) && true === TRIBE_NO_ASYNC )
+ || true === (bool) getenv( 'TRIBE_NO_ASYNC' )
+ ) {
+ do_action( 'tribe_log', 'debug', $this->identifier, [ 'action' => 'sync_handle', 'data' => $this->data ] );
+
+ return $this->sync_handle( $this->data );
+ }
+
+ if ( $this->feature_detection->supports_async_process() ) {
+ $url = add_query_arg( $this->get_query_args(), $this->get_query_url() );
+ $args = $this->get_post_args();
+
+ do_action( 'tribe_log', 'debug', $this->identifier, [ 'action' => 'async_dispatch', 'data' => $this->data ] );
+
+ return wp_remote_post( esc_url_raw( $url ), $args );
+ }
+
+ /*
+ * If async AJAX-based processing is not available then we "dispatch"
+ * by scheduling a single cron event immediately (as soon as possible)
+ * for this handler cron identifier.
+ */
+ if ( ! wp_next_scheduled( $this->healthcheck_cron_hook_id, [ $this->data ] ) ) {
+ // Schedule the event to happen as soon as possible.
+ $scheduled = wp_schedule_single_event( time() - 1, $this->healthcheck_cron_hook_id, [ $this->data ] );
+
+ if ( false === $scheduled ) {
+ /** @var Tribe__Log__Logger $logger */
+ $logger = tribe( 'logger' );
+ $class = get_class( $this );
+ $src = call_user_func( [ $class, 'action' ] );
+ $logger->log( 'Could not schedule event for cron-based handling', Tribe__Log::ERROR, $src );
+
+ do_action(
+ 'tribe_log',
+ 'error',
+ $this->identifier,
+ [ 'action' => 'schedule_cron', 'data' => $this->data ]
+ );
+ }
+
+ do_action(
+ 'tribe_log',
+ 'debug',
+ $this->identifier,
+ [ 'action' => 'schedule_cron', 'data' => $this->data ]
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Handles the process immediately, not in an async manner.
+ *
+ * @since 4.7.12
+ *
+ * @param array|null $data_source If not provided the method will read the handler data from the
+ * request array.
+ *
+ * @return mixed|null The result of the synchronous handling.
+ */
+ abstract public function sync_handle( array $data_source = null );
+
+ /**
+ * Returns an array of arguments that will be used to send the POST request.
+ *
+ * @since 4.9.5 Pulled from the `WP_Async_Request` class.
+ *
+ * @return array An array of arguments for the POST request.
+ */
+ protected function get_query_args() {
+ if ( null !== $this->query_args ) {
+ return $this->query_args;
+ }
+
+ return [
+ 'action' => $this->identifier,
+ 'nonce' => wp_create_nonce( $this->identifier ),
+ ];
+ }
+
+ /**
+ * Returns the URL that wil be used to post the request.
+ *
+ * @since 4.9.5 Pulled from the `WP_Async_Request` class.
+ *
+ * @return string The URL that will be used to POST the dispatch request; defaults
+ * to the `admin-ajax.php` one.
+ */
+ protected function get_query_url() {
+ if ( null !== $this->query_url ) {
+ return $this->query_url;
+ }
+
+ return admin_url( 'admin-ajax.php' );
+ }
+
+ /**
+ * Returns the arguments that will be used to send the POST request.
+ *
+ * @since 4.9.5 Pulled from the `WP_Async_Request` class.
+ *
+ * @return array An array of arguments that will be used to send the POST request.
+ */
+ protected function get_post_args() {
+ if ( null !== $this->post_args ) {
+ return $this->post_args;
+ }
+
+ return [
+ 'timeout' => 0.01,
+ 'blocking' => false,
+ 'body' => $this->data,
+ 'cookies' => $_COOKIE,
+ 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
+ ];
+ }
+
+ /**
+ * Returns this handler cron hook identifier.
+ *
+ * The handler cron hook identifier is the one that the handler
+ * will use to schedule a single cron event when the `dispatch`
+ * method is called and the environment does not support async
+ * processing.
+ *
+ * @since 4.7.23
+ *
+ * @return string The complete cron hook name (identifier) for
+ * this handler.
+ */
+ public function get_healthcheck_cron_hook_id() {
+ return $this->healthcheck_cron_hook_id;
+ }
+
+ /**
+ * Sets the that will be used during the request.
+ *
+ * @since 4.9.5 Pulled from the `WP_Async_Request` class.
+ *
+ * @param array $data Data.
+ *
+ * @return $this This handler instance.
+ */
+ public function data( $data ) {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * Handles the request and performs an action.
+ *
+ * @since 4.9.5 Pulled from the `WP_Async_Request` class.
+ *
+ * @param array|null $data_source A source of data if not provided in the request; used for
+ * cron-based fallback.
+ *
+ * @return null|array Depending on the context of the call, cron or async, either the result
+ * of the handling (cron) or nothing (async).
+ */
+ abstract protected function handle( array $data_source = null );
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Post_Thumbnail_Setter.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Post_Thumbnail_Setter.php
new file mode 100644
index 000000000..f019d1dfa
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Post_Thumbnail_Setter.php
@@ -0,0 +1,176 @@
+set_post_id( $post_id );
+ * $post_thumbnail_setter->set_post_thumbnail( 'http://foo.com/random-image.jpg' );
+ * $post_thumbnail_setter->dispatch();
+ *
+ * @since 4.7.12
+ */
+class Tribe__Process__Post_Thumbnail_Setter extends Tribe__Process__Handler {
+ /**
+ * @var int The ID of the post the post thumbnail should be assigned to.
+ */
+ protected $post_id;
+
+ /**
+ * @var int|string Either the ID of an attachment that should be set as the post thumbnail
+ * or the full URL, or file path, to it.
+ */
+ protected $post_thumbnail;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function action() {
+ return 'post_thumbnail_setter';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dispatch() {
+ if ( ! isset( $this->post_id, $this->post_thumbnail ) ) {
+ // since this is a developer error we are not localizing this error string
+ throw new InvalidArgumentException( 'Post ID and featured image should be set before trying to dispatch.' );
+ }
+
+ $data = [
+ 'post_id' => $this->post_id,
+ 'post_thumbnail' => trim( $this->post_thumbnail ),
+ ];
+
+ $this->data( $data );
+
+ do_action( 'tribe_log', 'debug', $this->identifier, $data );
+
+ return parent::dispatch();
+ }
+
+ /**
+ * Sets the ID of the post the post thumbnail (aka "featured image") should be attached
+ * and set for.
+ *
+ * @since 4.7.12
+ *
+ * @param int $post_id The target post ID.
+ */
+ public function set_post_id( $post_id ) {
+ $this->post_id = $post_id;
+ }
+
+ /**
+ * Sets the post thumbnail ID or source the process should set.
+ *
+ * @since 4.7.12
+ *
+ * @param int|string $post_thumbnail Either an attachment ID or the full URL, or path, to
+ * the post thumbnail image.
+ */
+ public function set_post_thumbnail( $post_thumbnail ) {
+ $this->post_thumbnail = $post_thumbnail;
+ }
+
+ /**
+ * Handles the post thumbnail setting async process.
+ *
+ * The post thumbnail will be uploaded, if not uploaded already, using the `tribe_upload_image` function.
+ * This method is an alias of the publicly accessible `sync_handle` one.
+ *
+ * @since 4.7.12
+ *
+ * @param array|null $data_source An optional source of data.
+ *
+ * @see Tribe__Process__Post_Thumbnail_Setter::sync_handle()
+ *
+ * @see tribe_upload_image()
+ */
+ protected function handle( array $data_source = null ) {
+ $this->sync_handle( $data_source );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sync_handle( array $data_source = null ) {
+ do_action( 'tribe_log', 'debug', $this->identifier, [ 'status' => 'handling request' ] );
+
+ $data_source = isset( $data_source ) ? $data_source : $_POST;
+
+ if ( ! isset( $data_source['post_id'], $data_source['post_thumbnail'] ) ) {
+ do_action( 'tribe_log', 'error', $this->identifier, [ 'data' => $data_source, ] );
+
+ return 0;
+ }
+
+ $id = filter_var( $data_source['post_id'], FILTER_SANITIZE_NUMBER_INT );
+ $post_thumbnail = tec_sanitize_string( $data_source['post_thumbnail'] );
+
+ do_action( 'tribe_log', 'debug', $this->identifier, [
+ 'status' => 'fetching thumbnail',
+ 'post_thumbnail' => $post_thumbnail,
+ 'post_id' => $id,
+ ] );
+
+ $thumbnail_id = tribe_upload_image( $post_thumbnail );
+
+ if ( false === $thumbnail_id ) {
+ do_action(
+ 'tribe_log',
+ 'error',
+ $this->identifier,
+ [
+ 'action' => 'fetch',
+ 'post_thumbnail' => $post_thumbnail,
+ 'post_id' => $id,
+ 'status' => 'could not fetch',
+ ]
+ );
+
+ return 0;
+ }
+
+ $set = true;
+ if ( (int) get_post_thumbnail_id( $id ) !== (int) $thumbnail_id ) {
+ $set = set_post_thumbnail( $id, $thumbnail_id );
+ }
+
+ if ( false === $set ) {
+ do_action(
+ 'tribe_log',
+ 'error',
+ $this->identifier,
+ [
+ 'action' => 'set',
+ 'post_thumbnail' => $post_thumbnail,
+ 'attachment_id' => $thumbnail_id,
+ 'post_id' => $id,
+ 'status' => 'unable to set thumbnail',
+ ]
+ );
+
+ return $thumbnail_id;
+ }
+
+ do_action(
+ 'tribe_log',
+ 'debug',
+ $this->identifier,
+ [
+ 'action' => 'set',
+ 'post_thumbnail' => $post_thumbnail,
+ 'attachment_id' => $thumbnail_id,
+ 'post_id' => $id,
+ 'status' => 'completed - attachment created and linked to the post',
+ ]
+ );
+
+ return $thumbnail_id;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Queue.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Queue.php
new file mode 100644
index 000000000..b1dc37fcc
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Queue.php
@@ -0,0 +1,1129 @@
+action = call_user_func( [ $class, 'action' ] );
+ $this->feature_detection = tribe( 'feature-detection' );
+
+ parent::__construct();
+
+ $this->healthcheck_cron_hook_id = $this->identifier . '_cron';
+ $this->healthcheck_cron_interval_id = $this->identifier . '_cron_interval';
+
+ add_action( $this->healthcheck_cron_hook_id, [ $this, 'handle_cron_healthcheck' ] );
+ add_filter( 'cron_schedules', [ $this, 'schedule_cron_healthcheck' ] );
+
+ /*
+ * This object might have been built while processing crons so
+ * we hook on the the object cron identifier to handle the task
+ * if the cron-triggered action ever fires.
+ */
+ add_action( $this->identifier, [ $this, 'maybe_handle' ] );
+ }
+
+ /**
+ * Stops a queue that might be running.
+ *
+ * The queue process results are not rolled back (e.g. 200 posts to create, stopped
+ * after 50, those 50 posts will persist).
+ *
+ * @since 4.7.12
+ *
+ * @param string $queue_id The unique identifier of the queue that should be stopped.
+ *
+ * @see Tribe__Process__Queue::save() to get the queue unique id.
+ *
+ * @return bool Whether the queue was correctly stopped, and its information
+ * deleted, or not.
+ */
+ public static function stop_queue( $queue_id ) {
+ $meta = (array) get_transient( $queue_id . '_meta' );
+ delete_transient( $queue_id . '_meta' );
+
+ if ( ! empty( $meta['identifier'] ) ) {
+ delete_transient( $meta['identifier'] . '_process_lock' );
+ }
+
+ return delete_option( $queue_id );
+ }
+
+ /**
+ * Whether a queue process is stuck or not.
+ *
+ * A queue process that has not been doing anything for an amount
+ * of time is considered "stuck".
+ *
+ * @since 4.7.18
+ *
+ * @param string $queue_id The queue process unique identifier.
+ *
+ * @return bool
+ */
+ public static function is_stuck( $queue_id ) {
+ $queue_status = self::get_status_of( $queue_id );
+ $is_stuck = false;
+
+ /**
+ * Filters the maximum allowed time a queue process can go without updates
+ * before being considered stuck.
+ *
+ * @since 4.7.18
+ *
+ * @param int $time_limit A value in seconds, defaults to 5'.
+ */
+ $limit = (float) apply_filters( 'tribe_process_queue_time_limit', 300 );
+
+ if ( ! empty( $queue_status['last_update'] ) && is_numeric( $queue_status['last_update'] ) ) {
+ $is_stuck = time() - (int) $queue_status['last_update'] > $limit;
+ } else {
+ $queue_status['last_update'] = time();
+ set_transient( $queue_id . '_meta', $queue_status->to_array(), DAY_IN_SECONDS );
+ }
+
+ /**
+ * Filters whether a queue is considered "stuck" or not.
+ *
+ * @since 4.7.18
+ *
+ * @param bool $is_stuck
+ * @param string $queue_id
+ * @param Tribe__Data $queue_status
+ */
+ return apply_filters( 'tribe_process_queue_is_stuck', $is_stuck, $queue_id, $queue_status );
+ }
+
+ /**
+ * Returns a queue status and information.
+ *
+ * @since 4.7.12
+ *
+ * @param string $queue_id
+ *
+ * @return Tribe__Data An object containing information about the queue.
+ *
+ * @see Tribe__Process__Queue::save() to get the queue unique id.
+ */
+ public static function get_status_of( $queue_id ) {
+ $meta = (array) get_transient( $queue_id . '_meta' );
+ $data = [
+ 'identifier' => $queue_id,
+ 'done' => (int) Tribe__Utils__Array::get( $meta, 'done', 0 ),
+ 'total' => (int) Tribe__Utils__Array::get( $meta, 'total', 0 ),
+ 'fragments' => (int) Tribe__Utils__Array::get( $meta, 'fragments', 0 ),
+ 'last_update' => (int) Tribe__Utils__Array::get( $meta, 'last_update', false ),
+ ];
+
+ return new Tribe__Data( $data, 0 );
+ }
+
+ /**
+ * Deletes all queues for a specific action.
+ *
+ * @since 4.7.19
+ *
+ * @param string $action The action (prefix) of the queues to delete.
+ *
+ * @return int The number of delete queues.
+ */
+ public static function delete_all_queues( $action ) {
+ global $wpdb;
+
+ $action = $wpdb->esc_like( 'tribe_queue_' . $action ) . '%';
+
+ $queues = $wpdb->get_col( $wpdb->prepare( "
+ SELECT DISTINCT(option_name)
+ FROM {$wpdb->options}
+ WHERE option_name LIKE %s
+ ", $action ) );
+
+ if ( empty( $queues ) ) {
+ return 0;
+ }
+
+ $deleted = 0;
+
+ foreach ( $queues as $queue ) {
+ $deleted ++;
+ self::delete_queue( $queue );
+ }
+
+ return $deleted;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete( $key ) {
+ self::delete_queue( $key );
+
+ return $this;
+ }
+
+ /**
+ * Deletes a queue batch(es) and meta information.
+ *
+ * @since 4.7.18
+ *
+ * @param string $key
+ */
+ public static function delete_queue( $key ) {
+ global $wpdb;
+
+ $meta_key = $key . '_meta';
+
+ $key = $wpdb->esc_like( $key ) . '%';
+
+ $wpdb->query( $wpdb->prepare( "
+ DELETE
+ FROM {$wpdb->options}
+ WHERE option_name LIKE %s
+ ", $key ) );
+
+ delete_transient( $meta_key );
+ }
+
+ /**
+ * Upates the queue and meta data for the process.
+ *
+ * @since 4.7.12
+ * @since 4.9.5 Pulled method from the `WP_Background_Process` class.
+ *
+ * @param string $key The key of the data to save.
+ * @param array $data The data to save.
+ *
+ * @return $this This process instance.
+ */
+ public function update( $key, $data ) {
+ $meta_key = $this->get_meta_key( $key );
+ $meta = (array) get_transient( $meta_key );
+ $done = $this->original_batch_count - count( $data );
+
+ $update_data = array_merge( $meta, [
+ 'done' => $meta['done'] + $done,
+ 'last_update' => time(),
+ ] );
+
+ /**
+ * Filters the information that will be updated in the database for this queue type.
+ *
+ * @since 4.7.12
+ *
+ * @param array $update_data
+ * @param self $this
+ */
+ $update_data = apply_filters( "tribe_process_queue_{$this->identifier}_update_data", $update_data, $this );
+
+ set_transient( $meta_key, $update_data, DAY_IN_SECONDS );
+
+ if ( ! empty( $data ) ) {
+ update_option( $key, $data );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the name of the transient that will store the queue meta information
+ * for the specific key.
+ *
+ * @since 4.7.12
+ *
+ * @param string $key
+ *
+ * @return string
+ */
+ public function get_meta_key( $key ) {
+ $key = preg_replace( '/^(.*)_\\d+$/', '$1', $key );
+
+ return $key . '_meta';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save() {
+ $key = $this->generate_key();
+
+ $fragments_count = $this->save_split_data( $key, $this->data );
+
+ $save_data = [
+ 'identifier' => $this->identifier,
+ 'done' => 0,
+ 'total' => count( $this->data ),
+ 'fragments' => $fragments_count,
+ 'last_update' => time(),
+ ];
+
+ /**
+ * Filters the information that will be saved to the database for this queue type.
+ *
+ * @since 4.7.12
+ *
+ * @param array $save_data
+ * @param self $this
+ */
+ $save_data = apply_filters( "tribe_process_queue_{$this->identifier}_save_data", $save_data, $this );
+
+ set_transient( $this->get_meta_key( $key ), $save_data );
+
+ $this->did_save = true;
+ $this->id = $key;
+
+ return $this;
+ }
+
+ /**
+ * Generates the unique key for the queue optionally using the client provided
+ * id.
+ *
+ * @since 4.7.12
+ *
+ * @param int $length The lengthy of the key to generate, longer keys will
+ * add more entropy; default to 64.
+ *
+ * @return string The generated batch key.
+ */
+ protected function generate_key( $length = 64 ) {
+ if ( empty( $this->id_base ) ) {
+ $this->id_base = md5( microtime() . mt_rand() );
+ }
+
+ $prepend = $this->identifier . '_batch_';
+
+ $this->batch_key = substr( $prepend . $this->id_base, 0, $length );
+
+ return $this->batch_key;
+ }
+
+ /**
+ * Saves the queue data to the database taking max_packet_size into account.
+ *
+ * In some instances the serialized size of the data might be bigger than the
+ * database `max_packet_size`; trying to write all the data in one query would
+ * make the db "go away...".
+ * Here we try to read the database `max_packet_size` setting and use that information
+ * to avoid overloading the query.
+ *
+ * @param string $key
+ * @param array $data
+ *
+ * @return int The number of fragments the data was split and stored into.
+ */
+ protected function save_split_data( $key, array $data ) {
+ if ( empty( $data ) ) {
+ return 0;
+ }
+
+ $max_frag_size = $this->get_max_frag_size();
+ // we add a 15% to the size to take the serialization and query overhead into account when fragmenting
+ $serialized_size = strlen( utf8_decode( maybe_serialize( $data ) ) ) * 1.15;
+ $frags_count = (int) ceil( $serialized_size / $max_frag_size );
+ $per_frag = max( (int) floor( count( $data ) / $frags_count ), 1 );
+
+ $split_data = array_chunk( $data, $per_frag );
+
+ if ( empty( $split_data ) ) {
+ return 0;
+ }
+
+ foreach ( $split_data as $i => $iValue ) {
+ $postfix = 0 === $i ? '' : "_{$i}";
+ update_option( $key . $postfix, $split_data[ $i ], false );
+ }
+
+ return count( $split_data );
+ }
+
+ /**
+ * Returns the max frag size in bytes.
+ *
+ * The bottleneck here is the database `max_packet_size` so we try to read
+ * it from the database.
+ *
+ * @return int The max size, in bytes, of a data fragment.
+ */
+ protected function get_max_frag_size() {
+ if ( ! empty( $this->max_frag_size ) ) {
+ return $this->max_frag_size;
+ }
+
+ return tribe( 'db' )->get_max_allowed_packet_size();
+ }
+
+ /**
+ * Sets the maximum size, in bytes, of the queue fragments.
+ *
+ * This will prevent the class from trying to read the value from the database.
+ *
+ * @since 4.7.12
+ *
+ * @param int $max_frag_size
+ */
+ public function set_max_frag_size( $max_frag_size ) {
+ $this->max_frag_size = $max_frag_size;
+ }
+
+ /**
+ * Returns the queue unique identifier.
+ *
+ * Mind that an id will only be available after saving a queue.
+ *
+ * @since 4.7.12
+ *
+ * @return string
+ * @throws RuntimeException if trying to get the queue id before saving it.
+ */
+ public function get_id() {
+ if ( null === $this->id ) {
+ // not localized as this is a developer-land error
+ throw new RuntimeException( 'Can only get the id of queue after saving it.' );
+ }
+
+ return $this->id;
+ }
+
+ /**
+ * Sets the queue unique id.
+ *
+ * When using this method the client code takes charge of the queue id uniqueness;
+ * the class will not check it.
+ *
+ * @since 4.7.12
+ *
+ * @param string $queue_id
+ *
+ * @throws RuntimeException If trying to set the queue id after saving it.
+ */
+ public function set_id( $queue_id ) {
+ if ( $this->did_save ) {
+ throw new RuntimeException( 'The queue id can be set only before saving it.' );
+ }
+
+ $queue_id = preg_replace( '/^' . preg_quote( $this->identifier, '/' ) . '_batch_/', '', $queue_id );
+
+ $this->id_base = $queue_id;
+ }
+
+ /**
+ * Overrides the base `dispatch` method to allow for constants and/or environment vars to run
+ * async requests in sync mode.
+ *
+ * @since 4.7.12
+ * @since 4.9.5 Pulled method code from the `WP_Background_Process` class.
+ *
+ * @return mixed
+ */
+ public function dispatch() {
+ if (
+ ( defined( 'TRIBE_NO_ASYNC' ) && true === TRIBE_NO_ASYNC )
+ || true === (bool) getenv( 'TRIBE_NO_ASYNC' )
+ || (bool) tribe_get_request_var( 'tribe_queue_sync', false )
+ || tribe_is_truthy( tribe_get_option( 'tribe_queue_sync', false ) )
+ ) {
+ $result = $this->sync_process();
+ $this->complete();
+
+ return $result;
+ }
+
+ if ( $this->feature_detection->supports_async_process() ) {
+ // Schedule the cron health-check.
+ $this->schedule_event();
+
+ // Perform remote post.
+ return parent::dispatch();
+ }
+
+ /*
+ * If async AJAX-based processing is not available then we "dispatch"
+ * by scheduling a single cron event immediately (as soon as possible)
+ * for this handler cron identifier.
+ */
+ if ( ! wp_next_scheduled( $this->identifier ) ) {
+ // Schedule the event to happen as soon as possible.
+ $scheduled = wp_schedule_single_event( time() - 1, $this->identifier );
+
+ if ( false === $scheduled ) {
+ /** @var Tribe__Log__Logger $logger */
+ $logger = tribe( 'logger' );
+ $class = get_class( $this );
+ $src = call_user_func( [ $class, 'action' ] );
+ $logger->log( 'Could not schedule event for cron-based processing', Tribe__Log::ERROR, $src );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Handles the process immediately, not in an async manner.
+ *
+ * @since 4.7.12
+ *
+ * @return array An array containing the result of each item handling.
+ */
+ public function sync_process() {
+ $result = [];
+ $this->doing_sync = true;
+
+ foreach ( $this->data as $item ) {
+ $result[] = $this->task( $item );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the name of the option used by the queue to store its batch(es).
+ *
+ * Mind that this value will be set only when first saving the queue and it will not be set
+ * in following queue processing.
+ *
+ * @since 4.7.12
+ *
+ * @param int $n The number of a specific batch option name to get; defaults to `0` to get the
+ * option name of the first one.
+ *
+ * @return string
+ *
+ * @throws RuntimeException If trying to get the value before saving the queue or during following
+ * processing.
+ */
+ public function get_batch_key( $n = 0 ) {
+ if ( null === $this->batch_key || ! $this->did_save ) {
+ throw new RuntimeException( 'The batch key will only be set after the queue is first saved' );
+ }
+
+ return empty( $n ) ? $this->batch_key : $this->batch_key . '_' . (int) $n;
+ }
+
+ /**
+ * Returns the queue action identifier.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @return string The queue action identifier.
+ */
+ public function get_identifier() {
+ return $this->identifier;
+ }
+
+ /**
+ * Returns a batch of items to process from the queue.
+ *
+ * @since 4.7.12
+ * @since 4.9.5 Pulled method code from the `WP_Background_Process` class.
+ *
+ * @return stdClass The first batch of items from the queue.
+ */
+ protected function get_batch() {
+ global $wpdb;
+
+ $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
+
+ $query = $wpdb->get_row( $wpdb->prepare( "
+ SELECT *
+ FROM {$wpdb->options}
+ WHERE option_name LIKE %s
+ ORDER BY option_id ASC
+ LIMIT 1
+ ", $key ) );
+
+ $batch = new stdClass();
+ $batch->key = $query->option_name;
+ $batch->data = maybe_unserialize( $query->option_value );
+
+ $this->original_batch_count = ! empty( $batch->data ) ? count( $batch->data ) : 0;
+
+ return $batch;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function get_post_args() {
+ $post_args = parent::get_post_args();
+
+ /**
+ * While sending the data into the body makes sense for the async process it does
+ * not make sense when processing a queue since the data will be stored and read
+ * from the database; furthermore this could raise issues with the max POST size.
+ */
+ $post_args['body'] = [];
+
+ return $post_args;
+ }
+
+ /**
+ * Maybe handle the process request in async or sync mode depending on the
+ * supported mode.
+ *
+ * @param array|null $data_source An optional data source.
+ *
+ * @since 4.9.5
+ */
+ public function maybe_handle( $data_source = null ) {
+ // Don't lock up other requests while processing
+ session_write_close();
+
+ if ( $this->feature_detection->supports_async_process() ) {
+ return $this->maybe_handle_async();
+ }
+
+ return $this->maybe_handle_sync();
+ }
+
+ /**
+ * Push an item to the process queue.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @param mixed $data An item to process.
+ *
+ * @return $this This process instance.
+ */
+ public function push_to_queue( $data ) {
+ $this->data[] = $data;
+
+ return $this;
+ }
+
+ /**
+ * Maybe handle this process request in async mode.
+ *
+ * @since 4.9.5
+ */
+ protected function maybe_handle_async() {
+ if ( $this->is_process_running() ) {
+ // Background process already running.
+ wp_die();
+ }
+
+ if ( $this->is_queue_empty() ) {
+ // No data to process: we're done.
+ $this->complete();
+ wp_die();
+ }
+
+ check_ajax_referer( $this->identifier, 'nonce' );
+
+ $this->handle();
+
+ wp_die();
+ }
+
+ /**
+ * Handle the process request in sync mode.
+ *
+ * @since 4.9.5
+ */
+ protected function maybe_handle_sync() {
+ if ( $this->is_process_running() ) {
+ // Background process already running.
+ return;
+ }
+
+ if ( $this->is_queue_empty() ) {
+ // No data to process: we're done.
+ $this->complete();
+
+ return;
+ }
+
+ $this->handle();
+ }
+
+ /**
+ * Checks whether the queue is empty or not.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @return bool Whether the queue is empty or not.
+ */
+ protected function is_queue_empty() {
+ global $wpdb;
+
+ $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
+
+ $count = $wpdb->get_var( $wpdb->prepare( "
+ SELECT COUNT(*)
+ FROM {$wpdb->options}
+ WHERE option_name LIKE %s
+ ", $key ) );
+
+ return $count <= 0;
+ }
+
+ /**
+ * Checks whether the process is currently running or not.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ */
+ protected function is_process_running() {
+ if ( get_transient( $this->identifier . '_process_lock' ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Locks the process so that other instances cannot spawn and run.
+ *
+ * Lock the process so that multiple instances can't run simultaneously.
+ * Override if applicable, but the duration should be greater than that
+ * defined in the `time_exceeded()` method.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ */
+ protected function lock_process() {
+ // Set start time of current process.
+ $this->start_time = time();
+
+ $lock_duration = $this->queue_lock_time;
+
+ /**
+ * Filters the duration of the lock acquired by a process instance.
+ *
+ * The lock duration should be larger than the maximum time a process is allowed to run.
+ *
+ * @since 4.9.5
+ *
+ * @param int $lock_duration The lock duration in seconds; defaults to one minute.
+ * @param static $this This process instance.
+ */
+ $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration, $this );
+
+ set_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
+ }
+
+ /**
+ * Releases the process lock so that other instances can spawn and run.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @return $this This process instance.
+ */
+ protected function unlock_process() {
+ delete_transient( $this->identifier . '_process_lock' );
+
+ return $this;
+ }
+
+ /**
+ * Handles the process request.
+ *
+ * Pass each queue item to the task handler, while remaining
+ * within server memory and time limit constraints.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @param array|null $data_source Unused and kept for compatibility with parent; the queue
+ * data is stored and read from the database.
+ */
+ protected function handle( array $data_source = null ) {
+ $this->lock_process();
+
+ do {
+ $batch = $this->get_batch();
+
+ foreach ( $batch->data as $key => $value ) {
+ $task = $this->task( $value );
+
+ if ( false !== $task ) {
+ $batch->data[ $key ] = $task;
+ } else {
+ unset( $batch->data[ $key ] );
+ }
+
+ if ( $this->time_exceeded() || $this->memory_exceeded() ) {
+ // Batch limits reached.
+ break;
+ }
+ }
+
+ // Update or delete current batch.
+ if ( ! empty( $batch->data ) ) {
+ $this->update( $batch->key, $batch->data );
+ } else {
+ $this->delete( $batch->key );
+ }
+ } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
+
+ $this->unlock_process();
+
+ // Start next batch or complete process.
+ if ( ! $this->is_queue_empty() ) {
+ $this->dispatch();
+ } else {
+ $this->complete();
+ }
+
+ if ( doing_action( $this->identifier ) ) {
+ /*
+ * We're probably acting in the context of a cron request or
+ * in the context of an explicitly triggered action: let's not
+ * die.
+ */
+ return;
+ }
+
+ wp_die();
+ }
+
+ /**
+ * Checks whether the memory limit was exceeded.
+ *
+ * Ensures the batch process never exceeds 90%
+ * of the maximum WordPress memory.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @return bool
+ */
+ protected function memory_exceeded() {
+ $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
+ $current_memory = memory_get_usage( true );
+ $return = false;
+
+ if ( $current_memory >= $memory_limit ) {
+ $return = true;
+ }
+
+ /**
+ * Filters whether the process did exceed the allowed memory limit or not.
+ *
+ * @since 4.9.5
+ *
+ * @param bool $return Whether the process did exceed the allowed memory limit or not.
+ * @param static $this This process instance.
+ */
+ return apply_filters( $this->identifier . '_memory_exceeded', $return, $this );
+ }
+
+ /**
+ * Returns the memory limit for this process.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @return int The memory limit in bytes.
+ */
+ protected function get_memory_limit() {
+ if ( function_exists( 'ini_get' ) ) {
+ $memory_limit = ini_get( 'memory_limit' );
+ } else {
+ // Sensible default.
+ $memory_limit = '128M';
+ }
+
+ if ( ! $memory_limit || -1 === (int) $memory_limit ) {
+ // Unlimited, set to 32GB.
+ $memory_limit = '32000M';
+ }
+
+ return (int) $memory_limit * 1024 * 1024;
+ }
+
+ /**
+ * Checks whether the execution time was exceeded or not.
+ *
+ * Ensures the batch never exceeds a sensible time limit.
+ * A timeout limit of 30s is common on shared hosting.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @return bool Whether the execution time was exceeded or not.
+ */
+ protected function time_exceeded() {
+ /**
+ * Filters the maximum time the process can operate before continuing in another
+ * request.
+ * We pick a safe default of 20 seconds but this value can be adjusted to suit the system
+ * timeout settings.
+ *
+ * @since 4.9.5
+ *
+ * @param int $default_time_limit The time limit for the process.
+ * @param static $this This process instance.
+ */
+ $time_limit = apply_filters( $this->identifier . '_default_time_limit', 20, $this );
+
+ $finish = $this->start_time + $time_limit;
+ $return = false;
+
+ if ( time() >= $finish ) {
+ $return = true;
+ }
+
+ /**
+ * Filters whether a process instance should be marked as having exceeded the time limit or not.
+ *
+ * @since 4.9.5
+ *
+ * @param bool $return Whether the process did exceed the time limit or not.
+ * @param static $this This process instance.
+ */
+ return apply_filters( $this->identifier . '_time_exceeded', $return );
+ }
+
+ /**
+ * Completes the processing, cleaning up after it.
+ *
+ * Override if applicable, but ensure that the below actions are
+ * performed, or, call parent::complete().
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ */
+ protected function complete() {
+ // Unschedule the cron health-check.
+ $this->clear_scheduled_event();
+ }
+
+ /**
+ * Schedules a cron-based health-check to restart the queue if stuck.
+ *
+ * Filters the `cron_schedules` filter to add a check every 5 minutes.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @param mixed $schedules The cron schedules to check.
+ *
+ * @return mixed The updated cron schedules.
+ */
+ public function schedule_cron_healthcheck( $schedules ) {
+ /**
+ * Filters the number of minutes to schedule the cron health-check.
+ *
+ * @since 4.9.5
+ *
+ * @param int $interval The number of minutes to schedule the cron health-check; defaults to 5.
+ * @param static $this This process instance.
+ */
+ $interval = apply_filters( $this->identifier . '_cron_interval', $this->healthcheck_cron_interval, $this );
+
+ // Adds every 5 minutes to the existing schedules.
+ $schedules[ $this->identifier . '_cron_interval' ] = [
+ 'interval' => MINUTE_IN_SECONDS * $interval,
+ 'display' => sprintf( __( 'Every %d Minutes', 'tribe-common' ), $interval ),
+ ];
+
+ return $schedules;
+ }
+
+ /**
+ * Handles the cron health-check.
+ *
+ * Restart the background process if not already running
+ * and data exists in the queue.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ */
+ public function handle_cron_healthcheck() {
+ if ( $this->is_process_running() ) {
+ // Background process already running.
+ exit;
+ }
+
+ if ( $this->is_queue_empty() ) {
+ // No data to process.
+ $this->clear_scheduled_event();
+ exit;
+ }
+
+ $this->handle();
+
+ exit;
+ }
+
+ /**
+ * Schedules the cron health-check event.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ */
+ protected function schedule_event() {
+ if ( ! wp_next_scheduled( $this->healthcheck_cron_hook_id ) ) {
+ wp_schedule_event( time(), $this->healthcheck_cron_interval_id, $this->healthcheck_cron_hook_id );
+ }
+ }
+
+ /**
+ * Clears the scheduled health-check cron event.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ */
+ protected function clear_scheduled_event() {
+ $timestamp = wp_next_scheduled( $this->healthcheck_cron_hook_id );
+
+ if ( $timestamp ) {
+ wp_unschedule_event( $timestamp, $this->healthcheck_cron_hook_id );
+ }
+ }
+
+ /**
+ * Cancels the current process.
+ *
+ * Stops processing queue items and clean up.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ */
+ public function cancel_process() {
+ if ( ! $this->is_queue_empty() ) {
+ $batch = $this->get_batch();
+
+ $this->delete( $batch->key );
+
+ wp_clear_scheduled_hook( $this->healthcheck_cron_hook_id );
+ }
+
+ }
+
+ /**
+ * Executes the process task on a single item.
+ *
+ * Override this method to perform any actions required on each
+ * queue item. Return the modified item for further processing
+ * in the next pass through. Or, return false to remove the
+ * item from the queue.
+ *
+ * @since 4.9.5 Pulled from the `WP_Background_Process` class.
+ *
+ * @param mixed $item Queue item to iterate over.
+ *
+ * @return mixed
+ */
+ abstract protected function task( $item );
+
+ /**
+ * Concrete implementation of the base handler method.
+ *
+ * Just a proxy to the `sync_process` method.
+ *
+ * @since 4.9.5
+ *
+ * @param array|null $data_source If not provided the method will read the handler data from the
+ * request array.
+ *
+ * @return array|mixed|null The synchronous process result.
+ */
+ public function sync_handle( array $data_source = null ) {
+ // In the base implementation the data source is unused and read from the database.
+ return $this->sync_process();
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Tester.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Tester.php
new file mode 100644
index 000000000..017f7e937
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Tester.php
@@ -0,0 +1,98 @@
+get_query_args(), $this->get_query_url() );
+ $args = $this->get_post_args();
+
+ return wp_remote_post( esc_url_raw( $url ), $args );
+ }
+
+ /**
+ * An override of the method implemented by the base Tribe Handler
+ * class to make sure the processing is done in async mode.
+ *
+ * This is the same code as the base WP_Background_Process class.
+ *
+ * @since 4.7.23
+ *
+ * @param null|array $data_source An optional data source.
+ */
+ public function maybe_handle( $data_source = null ) {
+ // Don't lock up other requests while processing
+ session_write_close();
+
+ check_ajax_referer( $this->identifier, 'nonce' );
+
+ $this->handle();
+
+ wp_die();
+ }
+
+ /**
+ * The task this class will perform is just setting a transient.
+ *
+ * The transient existence will be used as a canary to detect if
+ * background processing is supported.
+ *
+ * @since 4.7.23
+ *
+ * @param array|null $data_source Unused.
+ */
+ protected function handle( array $data_source = null ) {
+ tec_timed_option()->set( static::TRANSIENT_NAME, 1, WEEK_IN_SECONDS );
+ }
+
+ /**
+ * Returns this handler action identifier.
+ *
+ * @since 4.7.23
+ *
+ * @return string This handler action identifier.
+ */
+ public static function action() {
+ return 'async_process_support_test';
+ }
+}
\ No newline at end of file
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promise.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promise.php
new file mode 100644
index 000000000..d3c1dc4b0
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promise.php
@@ -0,0 +1,309 @@
+save()->dispatch();
+ * $promise_id = $promise->get_id();
+ *
+ * The promise is really a background process in disguise and will work, for all
+ * intents and purposes, like one.
+ *
+ * @since 4.9.5
+ */
+
+class Tribe__Promise extends Tribe__Process__Queue {
+
+ /**
+ * The action that will be done when the promise is done.
+ *
+ * @var string
+ */
+ protected $resolved;
+
+ /**
+ * An array of arguments that will be used to call a callback on completion.
+ *
+ * @var array
+ */
+ protected $resolved_args;
+
+ /**
+ * The action that will be done when the promise fails due to an error.
+ *
+ * @var string
+ */
+ protected $rejected;
+
+ /**
+ * An array of arguments that will be used to call a callback on failure.
+ *
+ * @var array
+ */
+ protected $rejected_args;
+
+ /**
+ * Whether this promise did resolve correctly or not.
+ *
+ * @var bool
+ */
+ protected $resolved_correctly = true;
+
+ /**
+ * Tribe__Promise constructor.
+ *
+ * @param string|array|Tribe__Utils__Callback $callback The callback that should run to perform the promise task.
+ * @param array $items The items to process, each item will be passed as first
+ * argument to the callback at run-time.
+ * @param array $extra_args An array of extra arguments that will be passed to the
+ * callback function.
+ */
+ public function __construct( $callback = null, array $items = null, array $extra_args = [] ) {
+ parent::__construct();
+
+ if ( ! empty( $callback ) && ! empty( $items ) ) {
+ foreach ( $items as $target ) {
+ $item['callback'] = $callback;
+ $item['args'] = array_merge( [ $target ], $extra_args );
+ $this->push_to_queue( $item );
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function action() {
+ return 'promise';
+ }
+
+ /**
+ * Sets a callback, and optional arguments, that will be called when the promise
+ * is resolved.
+ *
+ * The callback and arguments must be serializable and make sense in the context of,
+ * potentially, a different call from the one where this method is called.
+ *
+ * @since 4.9.5
+ *
+ * @param callable|Tribe__Utils__Callback $resolved The callback to call on success.
+ * @param callable|Tribe__Utils__Callback $rejected The callback to call on failure.
+ * @param array $resolved_args The arguments that will be passed to the resolved
+ * callback.
+ * @param array $rejected_args The arguments that will be passed to the rejected
+ * callback.
+ *
+ * @return Tribe__Promise This promise.
+ *
+ * @throws LogicException If this method is called after saving the promise.
+ */
+ public function then( $resolved, $rejected = null, array $resolved_args = null, array $rejected_args = null ) {
+ if ( $this->did_save ) {
+ throw new LogicException( 'The promise "then" method should be called before the "save" one' );
+ }
+
+ $this->resolved = $resolved;
+ $this->resolved_args = $resolved_args;
+ $this->rejected = $rejected;
+ $this->rejected_args = $rejected_args;
+
+ foreach ( $this->data as &$item ) {
+ $item['resolved'] = $this->resolved;
+ $item['resolved_args'] = $this->resolved_args;
+ $item['rejected'] = $this->rejected;
+ $item['rejected_args'] = $this->rejected_args;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Overrides the base method to save before dispatching.
+ *
+ * @since 4.9.5
+ *
+ * @return mixed The dispatch return value.
+ */
+ public function dispatch() {
+ if ( empty( $this->data ) ) {
+ $this->complete();
+
+ return null;
+ }
+
+ if ( ! $this->did_save ) {
+ $this->save();
+ }
+
+ return parent::dispatch();
+ }
+
+ /**
+ * A custom error handler to log any error tha might happen while invoking a promise
+ * callback.
+ *
+ * @since 4.9.5
+ *
+ * @param int $code The error code.
+ * @param string $error_message The error message.
+ *
+ * @see set_error_handler()
+ */
+ public function error_handler( $code, $error_message ) {
+ $message = 'There was an error (' . $code . ') while invoking a promise callback:';
+ $message .= "\n\t" . $error_message;
+ tribe( 'logger' )->log( $message, Tribe__Log::ERROR, __CLASS__ );
+ }
+
+ /**
+ * Performs the task associated with the promise.
+ *
+ * The promise is really just a flexible background process that
+ *
+ * @since 4.9.5
+ *
+ * @param array $item The promise payload, keys:
+ * {
+ * @param callable|Tribe__Utils__Callback $callback The callback this promise will
+ * call to perform the task.
+ * @param array $args An array of arguments that will be passed to the callback.
+ * @param callable|Tribe__Utils__Callback $then The callback this promise will
+ * call when complete.
+ * @param array $then_args An array of arguments that will be passed to the then callback.
+ * }
+ *
+ *
+ * @return bool `true` if the task needs to run again, `false` if the task is complete.
+ */
+ protected function task( $item ) {
+ if ( isset( $item['resolved'] ) ) {
+ $this->resolved = $item['resolved'];
+ if ( isset( $item['resolved_args'] ) ) {
+ $this->resolved_args = $item['resolved_args'];
+ }
+ }
+
+ if ( isset( $item['rejected'] ) ) {
+ $this->rejected = $item['rejected'];
+ if ( isset( $item['rejected_args'] ) ) {
+ $this->rejected_args = $item['rejected_args'];
+ }
+ }
+
+ $callback_args = isset( $item['args'] ) ? $item['args'] : null;
+ $done = $this->do_callback( $item['callback'], $callback_args );
+
+ // If we are done then return `false` to indicate "no need to run again".
+ return $done ? false : true;
+ }
+
+ /**
+ * Overrides the base method to allow building promises on empty objects
+ * without actually writing to the database.
+ *
+ * A fake queue id is set for compatibility reasons.
+ *
+ * @since 4.9.5
+ *
+ * @return Tribe__Process__Queue This object.
+ */
+ public function save() {
+ if ( empty( $this->data ) ) {
+ $this->id = uniqid( 'promise_', true );
+
+ return $this;
+ }
+
+ return parent::save();
+ }
+
+ /**
+ * Invokes a callback function with optional arguments.
+ *
+ * If the callback invocation results in an exception or error then the callback will return `true`
+ * and log.
+ *
+ * @since 4.9.5
+ *
+ * @param callable|Tribe__Utils__Callback $callback The callback to call.
+ * @param array|null $callback_args An optional array of arguments to call the
+ * callback with.
+ *
+ * @return mixed The callback invocation return value.
+ */
+ protected function do_callback( $callback, array $callback_args = null ) {
+ try {
+ set_error_handler( [ $this, 'error_handler' ] );
+
+ $callback = $this->unpack_callback( $callback );
+
+ if ( count( $callback_args ) ) {
+ $done = call_user_func_array( $callback, $callback_args );
+ } else {
+ $done = call_user_func( $callback );
+ }
+
+ restore_error_handler();
+
+ return $done;
+ } catch ( Exception $e ) {
+ $message = 'Exception (' . get_class( $e ) . ') thrown while invoking a promise callback:';
+ $message .= "\n\t" . $e->getMessage();
+ tribe( 'logger' )->log( $message, Tribe__Log::ERROR, __CLASS__ );
+
+ $this->resolved_correctly = false;
+ $this->complete();
+
+ return true;
+ }
+ }
+
+ /**
+ * Unpacks a callback returning a callable array for callbacks wrapped using the
+ * Tribe__Utils__Callback class.
+ *
+ * @since 4.9.5
+ *
+ * @param string|array|Tribe__Utils__Callback $callback The callback to unpack.
+ *
+ * @return array|string A callable array of string.
+ */
+ protected function unpack_callback( $callback ) {
+ if ( $callback instanceof Tribe__Utils__Callback ) {
+ $callback = [ tribe( $callback->get_slug() ), $callback->get_method() ];
+ }
+
+ return $callback;
+ }
+
+ /**
+ * Overrides the base method to call the success callback on completion.
+ *
+ * @since 4.9.5
+ */
+ protected function complete() {
+ parent::complete();
+
+ if ( $this->resolved_correctly && null !== $this->resolved ) {
+ $callback_args = isset( $this->resolved_args ) ? $this->resolved_args : null;
+ $this->do_callback( $this->resolved, $callback_args );
+ } elseif ( ! $this->resolved_correctly && null !== $this->rejected ) {
+ $callback_args = isset( $this->rejected_args ) ? $this->rejected_args : null;
+ $this->do_callback( $this->rejected, $callback_args );
+ }
+ }
+
+ /**
+ * An alias of the dispatch method to stick with the expected naming
+ * standard.
+ *
+ * @since 4.9.5
+ *
+ * @return mixed The dispatch operation return value.
+ */
+ public function resolve() {
+ return $this->dispatch();
+ }
+}
\ No newline at end of file
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Auth.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Auth.php
new file mode 100644
index 000000000..a7939e25a
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Auth.php
@@ -0,0 +1,112 @@
+connector = $connector;
+ }
+
+ /**
+ * Register the promoter auth key as part of the settings in order to make it available into the REST API.
+ *
+ * @since 4.12.6
+ *
+ * @return void
+ */
+ public function register_setting() {
+ register_setting(
+ 'options',
+ 'tribe_promoter_auth_key',
+ [
+ 'type' => 'string',
+ 'show_in_rest' => true,
+ 'description' => __( 'Promoter Key', 'tribe-common' ),
+ 'sanitize_callback' => 'sanitize_text_field',
+ ]
+ );
+ }
+
+ /**
+ * Add an update the KEY used for promoter during the connection.
+ *
+ * @since 4.9.12
+ *
+ * @param $secret_key
+ *
+ * @return string
+ */
+ public function filter_promoter_secret_key( $secret_key ) {
+
+ _deprecated_function( __METHOD__, '4.12.6' );
+
+ return empty( $secret_key ) ? $this->generate_secret_key() : $secret_key;
+ }
+
+ /**
+ * Authorize the request with the Promoter Connector.
+ *
+ * @since 4.9
+ *
+ * @return bool Whether the request was authorized successfully.
+ */
+ public function authorize_with_connector() {
+ $secret_key = $this->generate_secret_key();
+ $promoter_key = tribe_get_request_var( 'promoter_key' );
+ $license_key = tribe_get_request_var( 'license_key' );
+
+ // send request to auth connector
+ $result = $this->connector->authorize_with_connector( get_current_user_id(), $secret_key, $promoter_key, $license_key );
+
+ // If the secret was not stored correctly on Connector Application, remove it!
+ if ( ! $result ) {
+ delete_option( 'tribe_promoter_auth_key' );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Grab the WP constant and store it as the auth key, if none exists or is it empty
+ * it creates a dynamic one.
+ *
+ * @since 4.9.12
+ *
+ * @since 4.9
+ *
+ * @return string The secret key.
+ */
+ public function generate_secret_key() {
+
+ $salt = wp_generate_password( 6 );
+
+ if ( defined( 'AUTH_KEY' ) ) {
+ $key = AUTH_KEY;
+ } else {
+ $key = wp_generate_password( 25 );
+ }
+
+ $key = sha1( $salt . get_current_blog_id() . $key . get_bloginfo( 'url' ) );
+
+ update_option( 'tribe_promoter_auth_key', $key );
+
+ return $key;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Connector.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Connector.php
new file mode 100644
index 000000000..e62d9fa7b
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Connector.php
@@ -0,0 +1,277 @@
+base_url() . 'connect';
+
+ $payload = [
+ 'clientSecret' => $secret_key,
+ 'licenseKey' => $license_key,
+ 'userId' => $user_id,
+ ];
+
+ $token = TEC_JWT::encode( $payload, $promoter_key, 'HS256' );
+
+ $response = $this->make_call( $url, [
+ 'body' => [ 'token' => $token ],
+ 'sslverify' => false,
+ ] );
+
+ return (bool) $response;
+ }
+
+ /**
+ * Authenticate the current request user with the Auth Connector
+ *
+ * @param string $user_id User ID.
+ *
+ * @return bool|string User ID or if promoter is authorized then it return true like a valid user.
+ *
+ * @since 4.9
+ */
+ public function authenticate_user_with_connector( $user_id ) {
+ $this->authorized = false;
+
+ // If user is already authenticated no need to move forward (wp-admin) and others.
+ if ( ! empty( $user_id ) ) {
+ $this->authorized = true;
+ return $user_id;
+ }
+
+ $token = $this->get_token();
+
+ if ( empty( $token ) ) {
+ return $user_id;
+ }
+
+ $url = $this->base_url() . 'connect/auth';
+
+ $response = $this->make_call( $url, [
+ 'body' => [ 'token' => $token ],
+ 'sslverify' => false,
+ ] );
+
+ if ( ! $response ) {
+ return $user_id;
+ }
+
+ $this->authorized = true;
+
+ return $response;
+ }
+
+ /**
+ * Get the token either from a request or a header
+ *
+ * @since 4.9.20
+ *
+ * @return mixed
+ */
+ protected function get_token() {
+ $request_token = $this->get_token_from_request();
+
+ return ( $request_token )
+ ? sanitize_text_field( $request_token )
+ : $this->get_token_from_headers();
+ }
+
+ /**
+ * Get the token from a Request variable if present, otherwise fallback to `null`
+ *
+ * @since 4.9.20
+ *
+ * @return mixed
+ */
+ protected function get_token_from_request() {
+ // Used in favor of tribe_get_request_var as at this point tribe_get_request_var is not defined.
+ return \Tribe__Utils__Array::get_in_any(
+ [ $_GET, $_POST, $_REQUEST ],
+ 'tribe_promoter_auth_token'
+ );
+ }
+
+ /**
+ * Get the token directly from a Bearer Authentication Header, for hosts that
+ * does not support large Query strings
+ *
+ * @since 4.9.20
+ *
+ * @return mixed
+ */
+ protected function get_token_from_headers() {
+ $headers = [
+ 'HTTP_AUTHORIZATION',
+ 'REDIRECT_HTTP_AUTHORIZATION',
+ ];
+
+ foreach ( $headers as $header ) {
+ if ( empty( $_SERVER[ $header ] ) ) {
+ continue;
+ }
+
+ list( $token ) = sscanf( $_SERVER[ $header ], 'Bearer %s' );
+
+ if ( $token ) {
+ return sanitize_text_field( $token );
+ }
+ }
+ }
+
+ /**
+ * Notify the Promoter app of changes within this system.
+ *
+ * @param int $post_id Post ID.
+ *
+ * @since 4.9
+ */
+ public function notify_promoter_of_changes( $post_id ) {
+ $post_type = get_post_type( $post_id );
+
+ if ( ! in_array( $post_type, [ 'tribe_events', 'tribe_tickets' ], true ) ) {
+ return;
+ }
+
+ $secret_key = $this->get_secret_key();
+
+ if ( empty( $secret_key ) ) {
+ return;
+ }
+
+ /** @var Tribe__Promoter__PUE $promoter_pue */
+ $promoter_pue = tribe( 'promoter.pue' );
+ $license_info = $promoter_pue->get_license_info();
+
+ if ( ! $license_info ) {
+ return;
+ }
+
+ $license_key = $license_info['key'];
+
+ $payload = [
+ 'licenseKey' => $license_key,
+ 'sourceId' => $post_id instanceof WP_Post ? $post_id->ID : $post_id,
+ ];
+
+ $token = TEC_JWT::encode( $payload, $secret_key, 'HS256' );
+
+ $url = $this->base_url() . 'connect/notify';
+
+ $args = [
+ 'body' => [ 'token' => $token ],
+ 'sslverify' => false,
+ ];
+
+ $this->make_call( $url, $args );
+ }
+
+ /**
+ * Get the value for the option `tribe_promoter_auth_key`
+ *
+ * @since 4.9.12
+ *
+ * @return mixed
+ */
+ public function get_secret_key() {
+ $secret_key = get_option( 'tribe_promoter_auth_key' );
+
+ /**
+ * @since 4.9.12
+ *
+ * @param string $secret_key
+ */
+ return apply_filters( 'tribe_promoter_secret_key', $secret_key );
+ }
+
+ /**
+ * Make the call to the remote endpoint.
+ *
+ * @since 4.9
+ *
+ * @param array $args Data to send.
+ *
+ * @param string $url URL to send data to.
+ *
+ * @return string|false The response body or false if not successful.
+ *
+ */
+ public function make_call( $url, $args ) {
+ $response = wp_remote_post( $url, wp_parse_args( $args, [ 'timeout' => 30 ] ) );
+ $code = wp_remote_retrieve_response_code( $response );
+ $body = wp_remote_retrieve_body( $response );
+
+ if ( $code > 299 || is_wp_error( $response ) ) {
+ do_action(
+ 'tribe_log',
+ 'debug',
+ __METHOD__,
+ [
+ 'url' => $url,
+ 'args' => $args,
+ 'response' => $response,
+ 'response_code' => $code,
+ ]
+ );
+
+ return false;
+ }
+
+ return $body;
+ }
+
+ /**
+ * Check whether the user request is currently authorized by Promoter.
+ *
+ * @since 4.9.4
+ *
+ * @return bool Whether the user request is currently authorized by Promoter.
+ */
+ public function is_user_authorized() {
+ return $this->authorized;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/PUE.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/PUE.php
new file mode 100644
index 000000000..0900c1d4e
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/PUE.php
@@ -0,0 +1,96 @@
+pue_checker = new Tribe__PUE__Checker( 'http://tri.be/', $this->slug, [
+ 'context' => 'service',
+ 'plugin_name' => __( 'Promoter', 'tribe-common' ),
+ ] );
+ }
+
+ /**
+ * Get whether service has a license and if the license is activated on network.
+ *
+ * @return array|false License information or false if not set.
+ *
+ * @since 4.9
+ */
+ public function get_license_info() {
+ $option_name = 'pue_install_key_' . $this->slug;
+
+ $key = get_option( $option_name );
+
+ $is_network_key = false;
+
+ if ( is_multisite() ) {
+ $network_key = get_network_option( null, $option_name );
+
+ if ( empty( $key ) ) {
+ $key = $network_key;
+
+ $is_network_key = true;
+ }
+ }
+
+ if ( empty( $key ) ) {
+ return false;
+ }
+
+ return [
+ 'key' => $key,
+ 'is_network_key' => $is_network_key,
+ ];
+ }
+
+ /**
+ * Check whether service has a license key set or not.
+ *
+ * @return bool Whether service has a license key set.
+ *
+ * @since 4.9
+ */
+ public function has_license_key() {
+ return ! empty( $this->get_license_info() );
+ }
+
+ /**
+ * Check whether service has a valid license key or not.
+ *
+ * @return bool Whether service has a valid license key.
+ *
+ * @since 4.9
+ */
+ public function has_valid_license() {
+ $license_info = $this->get_license_info();
+
+ if ( ! $license_info ) {
+ return false;
+ }
+
+ $response = $this->pue_checker->validate_key( $license_info['key'], $license_info['is_network_key'] );
+
+ return isset( $response['status'] ) && 1 === (int) $response['status'];
+ }
+
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/View.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/View.php
new file mode 100644
index 000000000..f2fe93a82
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/View.php
@@ -0,0 +1,96 @@
+set_template_origin( Tribe__Main::instance() );
+ $this->set_template_folder( 'src/views/promoter' );
+ $this->set_template_context_extract( true );
+ $this->set_template_folder_lookup( true );
+ }
+
+ /**
+ * Add the rewrite rules and tags.
+ *
+ * @since 4.9
+ */
+ public function add_rewrites() {
+ add_rewrite_rule( 'tribe-promoter-auth/?$', 'index.php?tribe-promoter-auth-check=1', 'top' );
+ add_rewrite_tag( '%tribe-promoter-auth-check%', '([^&]+)' );
+ }
+
+ /**
+ * Get the redirect URL for finishing onboarding
+ *
+ * @since 4.9.6
+ *
+ * @return string Redirect URL for completing onboarding.
+ */
+ public function authorized_redirect_url() {
+ $url = 'https://promoter.theeventscalendar.com/welcome/review';
+
+ if ( defined( 'TRIBE_PROMOTER_AUTHORIZED_REDIRECT_URL' ) ) {
+ $url = TRIBE_PROMOTER_AUTHORIZED_REDIRECT_URL;
+ }
+
+ /**
+ * The url for redirecting in order to complete onboarding
+ *
+ * @since 4.9.6
+ *
+ * @param string $url Redirect URL.
+ */
+ return apply_filters( 'tribe_promoter_authorized_redirect_url', $url );
+ }
+
+ /**
+ * Display the auth check page when the correct permalink is loaded.
+ *
+ * @since 4.9
+ */
+ public function display_auth_check_view() {
+ global $wp_query;
+
+ $promoter_key = tribe_get_request_var( 'promoter_key' );
+ $license_key = tribe_get_request_var( 'license_key' );
+
+ if ( empty( $promoter_key ) || empty( $wp_query->query_vars['tribe-promoter-auth-check'] ) ) {
+ return;
+ }
+
+ $is_admin = is_user_logged_in() && current_user_can( 'manage_options' ) && current_user_can( 'read_private_posts' );
+ $authorized = false;
+ $auth_error = false;
+
+ if ( $is_admin && ! empty( $_POST['promoter_authenticate'] ) ) {
+ /** @var Tribe__Promoter__Auth $promoter_auth */
+ $promoter_auth = tribe( 'promoter.auth' );
+ $authorized = $promoter_auth->authorize_with_connector();
+ $auth_error = ! $authorized;
+ }
+
+ if ( $authorized ) {
+ wp_redirect( esc_url_raw( $this->authorized_redirect_url() ) );
+ } else {
+ $this->template( 'auth', [
+ 'authorized' => $authorized,
+ 'auth_error' => $auth_error,
+ 'logged_in' => is_user_logged_in(),
+ 'admin' => $is_admin,
+ 'promoter_key' => $promoter_key,
+ 'license_key' => $license_key,
+ ], true );
+ }
+
+ tribe_exit();
+ }
+
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Endpoints/CREATE_Endpoint_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Endpoints/CREATE_Endpoint_Interface.php
new file mode 100644
index 000000000..0b4b5eee2
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Endpoints/CREATE_Endpoint_Interface.php
@@ -0,0 +1,29 @@
+message = $message;
+ $this->code = $code;
+ $this->status = $status;
+ }
+
+ /**
+ * Return the error status.
+ * @return int
+ */
+ public function getStatus() {
+ return $this->status;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Header.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Header.php
new file mode 100644
index 000000000..a420ca07b
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Header.php
@@ -0,0 +1,19 @@
+base = $base;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Interface.php
new file mode 100644
index 000000000..4cf32f0ad
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Interface.php
@@ -0,0 +1,49 @@
+base->get_api_version_header() . ': disabled' );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Headers_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Headers_Interface.php
new file mode 100644
index 000000000..554520867
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Headers_Interface.php
@@ -0,0 +1,18 @@
+main = $main;
+ }
+
+ /**
+ * Prints TEC REST API related meta on the site.
+ */
+ public function add_header() {
+ $api_root = $this->base->get_rest_url();
+
+ if ( empty( $api_root ) ) {
+ return;
+ }
+
+ printf( '', esc_attr( $this->base->get_api_version_meta_name() ), esc_attr( $this->main->get_version() ) );
+ printf( '', esc_attr( $this->base->get_api_origin_meta_name() ), esc_url( $this->base->get_rest_origin_url() ) );
+ printf( '', esc_attr( $this->main->get_reference_url() ), esc_url( $api_root ) );
+ }
+
+ /**
+ * Sends TEC REST API related headers.
+ */
+ public function send_header() {
+ if ( headers_sent() ) {
+ return;
+ }
+
+ $api_root = $this->base->get_rest_url();
+
+ if ( empty( $api_root ) ) {
+ return;
+ }
+
+ header( $this->base->get_api_version_header() . ': ' . $this->main->get_version() );
+ header( $this->base->get_api_root_header() . ': ' . esc_url_raw( $api_root ) );
+ header( $this->base->get_api_origin_header() . ': ' . esc_url_raw( $this->base->get_rest_origin_url() ) );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Unsupported.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Unsupported.php
new file mode 100644
index 000000000..af8e3b5a5
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Unsupported.php
@@ -0,0 +1,39 @@
+main = $main;
+ }
+
+ /**
+ * Prints TEC REST API related meta on the site.
+ */
+ public function add_header() {
+ // no-op
+ }
+
+ /**
+ * Sends TEC REST API related headers.
+ */
+ public function send_header() {
+ if ( headers_sent() ) {
+ return;
+ }
+
+ header( $this->base->get_api_version_header() . ': unsupported' );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Main.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Main.php
new file mode 100644
index 000000000..5fcc28dcc
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Main.php
@@ -0,0 +1,149 @@
+namespace;
+ }
+
+ /**
+ * Returns the REST API URL prefix.
+ *
+ * @return string The REST API URL prefix.
+ */
+ public function get_url_prefix() {
+ $use_builtin = $this->use_builtin();
+
+ if ( $use_builtin ) {
+ $prefix = rest_get_url_prefix();
+ } else {
+ $prefix = apply_filters( 'rest_url_prefix', 'wp-json' );
+ }
+
+ $default_tec_prefix = $this->namespace . '/' . trim( $this->url_prefix(), '/' );
+ $prefix = rtrim( $prefix, '/' ) . '/' . trim( $default_tec_prefix, '/' );
+
+ /**
+ * Filters the TEC REST API URL prefix
+ *
+ * @param string $prefix The complete URL prefix.
+ * @param string $default_tec_prefix The default URL prefix appended to the REST URL by The Events Calendar.
+ */
+ return apply_filters( 'tribe_events_rest_url_prefix', $prefix, $default_tec_prefix );
+ }
+
+ /**
+ * Retrieves the URL to a TEC REST endpoint on a site.
+ *
+ * Note: The returned URL is NOT escaped.
+ *
+ * @global WP_Rewrite $wp_rewrite
+ *
+ * @param string $path Optional. TEC REST route. Default '/'.
+ * @param string $scheme Optional. Sanitization scheme. Default 'rest'.
+ * @param int $blog_id Optional. Blog ID. Default of null returns URL for current blog.
+ *
+ * @return string Full URL to the endpoint.
+ */
+ public function get_url( $path = '/', $scheme = 'rest', $blog_id = null ) {
+ if ( empty( $path ) ) {
+ $path = '/';
+ }
+
+ $tec_path = '/' . trim( $this->namespace, '/' ) . $this->url_prefix() . '/' . ltrim( $path, '/' );
+
+ if ( $this->use_builtin() ) {
+ $url = get_rest_url( $blog_id, $tec_path, $scheme );
+ } else {
+ if ( ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) ) || get_option( 'permalink_structure' ) ) {
+ global $wp_rewrite;
+
+ if ( $wp_rewrite->using_index_permalinks() ) {
+ $url = get_home_url( $blog_id, $wp_rewrite->index . '/' . $this->get_url_prefix(), $scheme );
+ } else {
+ $url = get_home_url( $blog_id, $this->get_url_prefix(), $scheme );
+ }
+
+ $url .= '/' . ltrim( $path, '/' );
+ } else {
+ $url = get_home_url( $blog_id, 'index.php', $scheme );
+
+ $url = add_query_arg( 'rest_route', $tec_path, $url );
+ }
+
+ if ( is_ssl() ) {
+ // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS.
+ if ( $_SERVER['SERVER_NAME'] === parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) ) {
+ $url = set_url_scheme( $url, 'https' );
+ }
+ }
+ }
+
+ /**
+ * Filters The Events Calendar REST URL.
+ *
+ * @param string $url TEC REST URL.
+ * @param string $path REST route.
+ * @param int $blog_id Blog ID.
+ * @param string $scheme Sanitization scheme.
+ */
+ return apply_filters( 'tribe_rest_url', $url, $path, $blog_id, $scheme );
+ }
+
+ /**
+ * Whether built-in WP REST API functions and functionalities should/can be used or not.
+ *
+ * @return bool
+ */
+ protected function use_builtin() {
+ /**
+ * Filters whether builtin WordPress REST API functions should be used or not if available.
+ */
+ $use_builtin = apply_filters( 'tribe_events_rest_use_builtin', true );
+
+ return $use_builtin && function_exists( 'get_rest_url' );
+ }
+
+ /**
+ * Returns the REST API URL prefix that will be appended to the namespace.
+ *
+ * The prefix should be in the `/some/path` format.
+ *
+ * @return string
+ */
+ abstract protected function url_prefix();
+
+ /**
+ * Returns the string indicating the REST API version.
+ *
+ * @return string
+ */
+ abstract public function get_version();
+
+ /**
+ * Returns the URL where the API users will find the API documentation.
+ *
+ * @return string
+ */
+ abstract public function get_reference_url();
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Messages_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Messages_Interface.php
new file mode 100644
index 000000000..13edb0360
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Messages_Interface.php
@@ -0,0 +1,30 @@
+ => ]` format.
+ */
+ public function get_messages();
+
+ /**
+ * Prefixes a message slug with a common root.
+ *
+ * @param string $message_slug
+ *
+ * @return string The prefixed message slug.
+ */
+ public function prefix_message_slug( $message_slug );
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository.php
new file mode 100644
index 000000000..274e18327
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository.php
@@ -0,0 +1,107 @@
+ $full_url,
+ 'id' => $image_id,
+ 'extension' => pathinfo( $file, PATHINFO_EXTENSION ),
+ ];
+
+ $metadata = wp_get_attachment_metadata( $image_id );
+
+ if (
+ false !== $metadata
+ && isset( $metadata['image_meta'], $metadata['file'], $metadata['sizes'] )
+ ) {
+ unset( $metadata['image_meta'], $metadata['file'] );
+
+ foreach ( $metadata['sizes'] as $size => &$meta ) {
+ $size_image_src = wp_get_attachment_image_src( $image_id, $size );
+ $meta['url'] = ! empty( $size_image_src[0] ) ? $size_image_src[0] : '';
+ unset( $meta['file'] );
+ }
+ unset( $meta );
+
+ $data = array_filter( array_merge( $data, $metadata ) );
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param string $date A date string in a format `strtotime` can parse.
+ *
+ * @return array An array of date details for the end date; each entry will be
+ * empty if the date is empty.
+ */
+ protected function get_date_details( $date ) {
+ if ( empty( $date ) ) {
+ return [
+ 'year' => '',
+ 'month' => '',
+ 'day' => '',
+ 'hour' => '',
+ 'minutes' => '',
+ 'seconds' => '',
+ ];
+ }
+
+ $time = strtotime( $date );
+
+ return [
+ 'year' => date( 'Y', $time ),
+ 'month' => date( 'm', $time ),
+ 'day' => date( 'd', $time ),
+ 'hour' => date( 'H', $time ),
+ 'minutes' => date( 'i', $time ),
+ 'seconds' => date( 's', $time ),
+ ];
+ }
+
+ /**
+ * Returns a localized and formatted list of cost values in ASC order.
+ *
+ * @since 4.7.19
+ *
+ * @param array $cost_couples An array of cost couples in the [ => ] format.
+ *
+ * @return array
+ */
+ protected function format_and_sort_cost_couples( array $cost_couples = [] ) {
+ global $wp_locale;
+
+ $cost_values = [];
+ foreach ( $cost_couples as $key => $value ) {
+ $value = str_replace(
+ [
+ $wp_locale->number_format['decimal_point'],
+ $wp_locale->number_format['thousands_sep'],
+ ],
+ [ '.', '' ],
+ '' . $value
+ );
+ if ( is_numeric( $value ) ) {
+ $cost_values[] = $value;
+ } else {
+ $cost_values[] = $key;
+ }
+ }
+
+ sort( $cost_values, SORT_NUMERIC );
+
+ return $cost_values;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository_Interface.php
new file mode 100644
index 000000000..096c8908f
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository_Interface.php
@@ -0,0 +1,14 @@
+ 'post',
+ 'suppress_filters' => false,
+ 'posts_per_page' => -1,
+ ];
+
+ /**
+ * @var array A list of query modifiers that will trigger a overriding merge, thus
+ * replacing previous values, when set multiple times.
+ */
+ protected static $replacing_modifiers = [
+ 'p',
+ 'author',
+ 'author_name',
+ 'author__in',
+ 'author__not_in',
+ 'has_password',
+ 'post_password',
+ 'cat',
+ 'category__and',
+ 'category__in',
+ 'category__not_in',
+ 'category_name',
+ 'comment_count',
+ 'comment_status',
+ 'menu_order',
+ 'title',
+ 'title_like',
+ 'name',
+ 'post_name__in',
+ 'ping_status',
+ 'post__in',
+ 'post__not_in',
+ 'post_parent',
+ 'post_parent__in',
+ 'post_parent__not_in',
+ 'post_mime_type',
+ 's',
+ 'search',
+ 'tag',
+ 'tag__and',
+ 'tag__in',
+ 'tag__not_in',
+ 'tag_id',
+ 'tag_slug__and',
+ 'tag_slug__in',
+ 'ID',
+ 'id',
+ 'date',
+ 'after_date',
+ 'before_date',
+ 'date_gmt',
+ 'after_date_gmt',
+ 'before_date_gmt',
+ 'post_title',
+ 'post_content',
+ 'post_excerpt',
+ 'post_status',
+ 'to_ping',
+ 'post_modified',
+ 'post_modified_gmt',
+ 'post_content_filtered',
+ 'guid',
+ 'perm',
+ 'order',
+ ];
+
+ /**
+ * @var int
+ */
+ protected static $meta_alias = 0;
+
+ /**
+ * @var array A list of keys that denote the value to check should be cast to array.
+ */
+ protected static $multi_value_keys = [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ];
+
+ /**
+ * @var array A map of SQL comparison operators to their human-readable counterpart.
+ */
+ protected static $comparison_operators = [
+ '=' => 'equals',
+ '!=' => 'not-equals',
+ '>' => 'gt',
+ '>=' => 'gte',
+ '<' => 'lt',
+ '<=' => 'lte',
+ 'LIKE' => 'like',
+ 'NOT LIKE' => 'not-like',
+ 'IN' => 'in',
+ 'NOT IN' => 'not-in',
+ 'BETWEEN' => 'between',
+ 'NOT BETWEEN' => 'not-between',
+ 'EXISTS' => 'exists',
+ 'NOT EXISTS' => 'not-exists',
+ 'REGEXP' => 'regexp',
+ 'NOT REGEXP' => 'not-regexp',
+ ];
+
+ /**
+ * A counter to keep track, on the class level, of the aliases generated for the terms table
+ * while building multi queries.
+ *
+ * @var int
+ */
+ protected static $alias_counter = 1;
+
+ /**
+ * @var string
+ */
+ protected $filter_name = 'default';
+ /**
+ * @var array The post IDs that will be updated.
+ */
+ protected $ids = [];
+ /**
+ * @var bool Whether the post IDs to update have already been fetched or not.
+ */
+ protected $has_ids = false;
+ /**
+ * @var array The updates that will be saved to the database.
+ */
+ protected $updates = [];
+
+ /**
+ * @var array A list of taxonomies this repository will recognize.
+ */
+ protected $taxonomies = [];
+
+ /**
+ * @var array A map detailing which fields should be converted from a
+ * GMT time and date to a local one.
+ */
+ protected $to_local_time_map = [
+ 'post_date_gmt' => 'post_date',
+ ];
+
+ /**
+ * @var array A map detailing which fields should be converted from a
+ * localized time and date to a GMT one.
+ */
+ protected $to_gmt_map = [
+ 'post_date' => 'post_date_gmt',
+ ];
+
+ /**
+ * @var array
+ */
+ protected $default_args = [ 'post_type' => 'post' ];
+
+ /**
+ * @var array An array of query modifying callbacks populated while applying
+ * the filters.
+ */
+ protected $query_modifiers = [];
+
+ /**
+ * @var bool Whether the current query is void or not.
+ */
+ protected $void_query = false;
+
+ /**
+ * @var array An array of query arguments that will be populated while applying
+ * filters.
+ */
+ protected $query_args = [
+ 'meta_query' => [ 'relation' => 'AND' ],
+ 'tax_query' => [ 'relation' => 'AND' ],
+ 'date_query' => [ 'relation' => 'AND' ],
+ ];
+
+ /**
+ * @var array An array of query arguments that support 'relation'.
+ */
+ protected $relation_query_args = [
+ 'meta_query',
+ 'tax_query',
+ 'date_query',
+ ];
+
+ /**
+ * @var WP_Query The current query object built and modified by the instance.
+ */
+ protected $current_query;
+
+ /**
+ * @var array An associative array of the filters that will be applied and the used values.
+ */
+ protected $current_filters = [];
+
+ /**
+ * @var string|null The current filter being applied.
+ */
+ protected $current_filter;
+
+ /**
+ * @var Tribe__Repository__Query_Filters
+ */
+ public $filter_query;
+
+ /**
+ * @var string The filter that should be used to get a post by its primary key.
+ */
+ protected $primary_key = 'p';
+
+ /**
+ * @var array A map of callbacks in the shape [ => ]
+ */
+ protected $schema = [];
+
+ /**
+ * @var array A map of schema slugs and their meta keys to be queried.
+ */
+ protected $simple_meta_schema = [];
+
+ /**
+ * @var array A map of schema slugs and their taxonomies to be queried.
+ */
+ protected $simple_tax_schema = [];
+
+ /**
+ * @var Tribe__Repository__Interface
+ */
+ protected $main_repository;
+
+ /**
+ * @var Tribe__Repository__Formatter_Interface
+ */
+ protected $formatter;
+
+ /**
+ * @var bool
+ */
+ protected $skip_found_rows = true;
+
+ /**
+ * @var Tribe__Repository__Interface
+ */
+ protected $query_builder;
+
+ /**
+ * A map relating aliases to their real update field name.
+ *
+ * E.g. the `title` alias might be an alias of `post_title` in update/save operations.
+ * This is done to allow using set-like methods with human-readable names.
+ * Extending classes should pre-fill this with default aliases.
+ *
+ * @var array
+ */
+ protected $update_fields_aliases = [
+ 'title' => 'post_title',
+ 'content' => 'post_content',
+ 'description' => 'post_content',
+ 'slug' => 'post_name',
+ 'excerpt' => 'post_excerpt',
+ 'status' => 'post_status',
+ 'parent' => 'post_parent',
+ 'author' => 'post_author',
+ 'date' => 'post_date',
+ 'date_gmt' => 'post_date_gmt',
+ 'date_utc' => 'post_date_gmt',
+ 'tag' => 'post_tag',
+ 'image' => '_thumbnail_id',
+ ];
+
+ /**
+ * The default create args that will be used by the repository
+ * to create posts of the managed type.
+ *
+ * @var
+ */
+ protected $create_args;
+
+ /**
+ * Indicates the current display context if any.
+ * Extending classes can support and use this property to know the
+ * display context.
+ *
+ * @var string
+ */
+ protected $display_context = 'default';
+
+ /**
+ * Indicates the current render context if any.
+ * Extending classes can support and use this property to know the
+ * render context.
+ *
+ * @var string
+ */
+ protected $render_context = 'default';
+
+ /**
+ * The query last built from the repository instance.
+ *
+ * @var WP_Query|null
+ */
+ protected $last_built_query;
+
+ /**
+ * The hash of the last built query.
+ *
+ * @var string
+ */
+ protected $last_built_hash = '';
+
+ /**
+ * Tribe__Repository constructor.
+ *
+ * @since 4.7.19
+ */
+ public function __construct() {
+ $this->filter_query = new Tribe__Repository__Query_Filters();
+ $this->default_args = array_merge( [ 'posts_per_page' => -1 ], $this->default_args );
+ $post_types = (array) Tribe__Utils__Array::get( $this->default_args, 'post_type', [] );
+ $this->taxonomies = get_taxonomies( [ 'object_type' => $post_types ], 'names' );
+
+ /**
+ * Allow plugins to init their classes and setup hooks at the initial setup of a repository.
+ *
+ * @param Tribe__Repository $this This repository instance
+ *
+ * @since 4.9.5
+ */
+ do_action( "tribe_repository_{$this->filter_name}_init", $this );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_default_args() {
+ return $this->default_args;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_default_args( array $default_args ) {
+ $this->default_args = $default_args;
+ }
+
+ /**
+ * Returns the value of a protected property.
+ *
+ * @since 4.7.19
+ *
+ * @param string $name
+ *
+ * @return mixed|null
+ * @throws Tribe__Repository__Usage_Error If trying to access a non defined property.
+ */
+ public function __get( $name ) {
+ if ( ! property_exists( $this, $name ) ) {
+ throw Tribe__Repository__Usage_Error::because_property_is_not_defined( $name, $this );
+ }
+
+ return $this->{$name};
+ }
+
+ /**
+ * Magic method to set protected properties.
+ *
+ * @since 4.7.19
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @throws Tribe__Repository__Usage_Error As properties have to be set extending
+ * the class, using setter methods or via constructor injection
+ */
+ public function __set( $name, $value ) {
+ throw Tribe__Repository__Usage_Error::because_properties_should_be_set_correctly( $name, $this );
+ }
+
+ /**
+ * Whether the class has a property with the specific name or not.
+ *
+ * @since 4.7.19
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function __isset( $name ) {
+ return property_exists( $this, $name ) && isset( $this->{$name} );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where( $key, $value = null ) {
+ $call_args = func_get_args();
+
+ return call_user_func_array( [ $this, 'by' ], $call_args );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function page( $page ) {
+ $this->query_args['paged'] = absint( $page );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function per_page( $per_page ) {
+ // we allow for `-1` here
+ $this->query_args['posts_per_page'] = $per_page;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count() {
+ if ( $this->void_query ) {
+ return 0;
+ }
+
+ $query = $this->build_query();
+
+ // The request property will be set during the `get_posts` method and empty before it.
+ if ( ! empty( $query->request ) ) {
+ return (int) $query->post_count;
+ }
+
+ $original_fields_value = $query->get( 'fields', '' );
+
+ $query->set( 'fields', 'ids' );
+
+ /**
+ * Filters the query object by reference before counting found posts in the current page.
+ *
+ * @since 4.7.19
+ *
+ * @param WP_Query $query
+ */
+ do_action( "tribe_repository_{$this->filter_name}_pre_count_posts", $query );
+
+ $ids = $query->get_posts();
+
+ $query->set( 'fields', $original_fields_value );
+
+ return is_array( $ids ) ? count( $ids ) : 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build_query( $use_query_builder = true ) {
+ $query = null;
+
+ if ( array_key_exists( 'void_query', $this->query_args ) && false !== $this->query_args['void_query'] ) {
+ $this->void_query = true;
+ }
+
+ // We'll let the query builder decide if the query has to be rebuilt or not.
+ if ( $use_query_builder && null !== $this->query_builder ) {
+ $query = $this->build_query_with_builder();
+ }
+
+ if ( null !== $this->last_built_query && $this->last_built_hash === $this->hash()) {
+ return $this->last_built_query;
+ }
+
+ if ( null === $query ) {
+ $query = $this->build_query_internally();
+ }
+
+ /**
+ * Fires after the query has been built and before it's returned.
+ *
+ * @since 4.9.5
+ *
+ * @param WP_Query $query The built query.
+ * @param array $query_args An array of query arguments used to build the query.
+ * @param Tribe__Repository $this This repository instance.
+ * @param bool $use_query_builder Whether a query builder was used to build this query or not.
+ * @param Tribe__Repository__Interface $query_builder The query builder in use, if any.
+ */
+ do_action( "tribe_repository_{$this->filter_name}_query",
+ $query,
+ $this,
+ $use_query_builder,
+ $this->query_builder
+ );
+
+ $this->last_built_query = $query;
+ $this->last_built_hash = $this->hash();
+
+ return $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function found() {
+ if ( $this->void_query ) {
+ return 0;
+ }
+
+ $query = $this->build_query();
+
+ $original_no_found_rows_value = $query->get( 'no_found_rows' );
+
+ // The request property will be set during the `get_posts` method and empty before it.
+ if ( ! empty( $query->request ) && ( false === (boolean) $original_no_found_rows_value || ! $this->skip_found_rows ) ) {
+ return (int) $query->found_posts;
+ }
+
+ $original_fields_value = $query->get( 'fields' );
+
+ $query->set( 'fields', 'ids' );
+ $query->set( 'no_found_rows', false );
+
+ /**
+ * Filters the query object by reference before counting found posts.
+ *
+ * @since 4.7.19
+ *
+ * @param WP_Query $query
+ */
+ do_action( "tribe_repository_{$this->filter_name}_pre_found_posts", $query );
+
+ $query->get_posts();
+
+ $query->set( 'fields', $original_fields_value );
+ $query->set( 'no_found_rows', $original_no_found_rows_value );
+
+ return (int) $query->found_posts;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all() {
+ if ( $this->void_query ) {
+ return [];
+ }
+
+ $query = $this->build_query();
+
+ // The request property will be set during the `get_posts` method and empty before it.
+ if ( ! empty( $query->request ) ) {
+ return array_map( [ $this, 'format_item' ], $query->posts );
+ }
+
+ $original_fields_value = $query->get( 'fields', '' );
+
+ $return_ids = 'ids' === $original_fields_value;
+
+ /**
+ * Do not skip counting the rows if we have some filtering to do on
+ * `found_posts`.
+ */
+ $query->set( 'no_found_rows', $this->skip_found_rows );
+
+ // We'll let the class build the items later.
+ $query->set( 'fields', 'ids' );
+
+ /**
+ * Filters the query object by reference before getting the posts.
+ *
+ * @since 4.7.19
+ *
+ * @param WP_Query $query
+ */
+ do_action( "tribe_repository_{$this->filter_name}_pre_get_posts", $query );
+
+ $results = $query->get_posts();
+
+ /**
+ * Allow extending classes to customize the return value.
+ * Since we are filtering the array returning empty values while formatting
+ * the item will exclude it from the return values.
+ */
+ $formatted = $return_ids
+ ? $results
+ : array_filter( array_map( [ $this, 'format_item' ], $results ) );
+
+ // Reset the fields if required.
+ $query->set( 'fields', $original_fields_value );
+
+ return $formatted;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offset( $offset, $increment = false ) {
+ /**
+ * The `offset` argument will only be used when `posts_per_page` is not -1
+ * and will ignore pagination.
+ * So we filter to apply a real SQL OFFSET; we also leave in place the `offset`
+ * query var to have a fallback should the LIMIT cause proving difficult to filter.
+ */
+ $this->query_args['offset'] = $increment
+ ? absint( $offset ) + (int) Tribe__Utils__Array::get( $this->query_args, 'offset', 0 )
+ : absint( $offset );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order( $order = 'ASC' ) {
+ $order = strtoupper( $order );
+
+ if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
+ return $this;
+ }
+
+ $this->query_args['order'] = $order;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order_by( $order_by, $order = 'DESC' ) {
+ $this->query_args['orderby'] = $order_by;
+
+ // Based on `WP_Query->parse_orderby` we should ignore the global order passed, and use the value on for each item in array.
+ if ( ! is_array( $order_by ) ) {
+ $this->query_args['order'] = $order;
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fields( $fields ) {
+ $this->query_args['fields'] = $fields;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function permission( $permission ) {
+ if ( ! in_array( $permission, [ self::PERMISSION_READABLE, self::PERMISSION_EDITABLE ], true ) ) {
+ return $this;
+ }
+
+ $this->query_args['perm'] = $permission;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function in( $post_ids ) {
+ $this->add_args( 'post__in', $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * Merges arguments into a query arg.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ * @param array|int $value
+ */
+ protected function add_args( $key, $value ) {
+ $this->query_args[ $key ] = (array) $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function not_in( $post_ids ) {
+ $this->add_args( 'post__not_in', $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parent( $post_id ) {
+ $this->add_args( 'post_parent__in', $post_id );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parent_in( $post_ids ) {
+ $this->add_args( 'post_parent__in', $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parent_not_in( $post_ids ) {
+ $this->add_args( 'post_parent__not_in', $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function search( $search ) {
+ $this->query_args['s'] = $search;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function first() {
+ $query = $this->build_query();
+
+ $original_fields_value = $query->get( 'fields', '' );
+
+ $return_id = 'ids' === $original_fields_value;
+
+ // The request property will be set during the `get_posts` method and empty before it.
+ if ( ! empty( $query->request ) ) {
+ $ids = $this->get_ids();
+
+ if ( empty( $ids ) ) {
+ return null;
+ }
+
+ return $return_id ? reset( $ids ) : $this->format_item( reset( $ids ) );
+ }
+
+ $query->set( 'fields', 'ids' );
+
+ /**
+ * Filters the query object by reference before getting the first post from the query.
+ *
+ * @since 4.14.8
+ *
+ * @param WP_Query $query The WP_Query object before get_posts() is called.
+ */
+ do_action( "tribe_repository_{$this->filter_name}_pre_first_post", $query );
+
+ $ids = $query->get_posts();
+
+ $query->set( 'fields', $original_fields_value );
+
+ if ( empty( $ids ) ) {
+ return null;
+ }
+
+ return $return_id ? reset( $ids ) : $this->format_item( reset( $ids ) );
+ }
+
+ /**
+ * Formats a post handled by the repository to the expected
+ * format.
+ *
+ * Extending classes should use this method to format return values to the expected format.
+ *
+ * @since 4.7.19
+ *
+ * @param int|WP_Post $id
+ *
+ * @return WP_Post
+ */
+ protected function format_item( $id ) {
+ $formatted = null === $this->formatter
+ ? get_post( $id )
+ : $this->formatter->format_item( $id );
+
+ /**
+ * Filters a single formatted result.
+ *
+ * @since 4.9.11
+ *
+ * @param mixed|WP_Post $formatted The formatted post result, usually a post object.
+ * @param int $id The formatted post ID.
+ * @param Tribe__Repository__Interface $this The current repository object.
+ */
+ $formatted = apply_filters( "tribe_repository_{$this->filter_name}_format_item", $formatted, $id, $this );
+
+ return $formatted;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function last() {
+ $query = $this->build_query();
+
+ $original_fields_value = $query->get('fields', '');
+
+ $return_id = 'ids' === $original_fields_value;
+
+ // The request property will be set during the `get_posts` method and empty before it.
+ if ( ! empty( $query->request ) ) {
+ $ids = $this->get_ids();
+
+ if ( empty( $ids ) ) {
+ return null;
+ }
+
+ return $return_id ? end( $ids ) : $this->format_item( end( $ids ) );
+ }
+
+ $query->set( 'fields', 'ids' );
+
+ /**
+ * Filters the query object by reference before getting the last post from the query.
+ *
+ * @since 4.14.8
+ *
+ * @param WP_Query $query The WP_Query object before get_posts() is called.
+ */
+ do_action( "tribe_repository_{$this->filter_name}_pre_last_post", $query );
+
+ $ids = $query->get_posts();
+
+ $query->set( 'fields', $original_fields_value );
+
+ if ( empty( $ids ) ) {
+ return null;
+ }
+
+ return $return_id ? end( $ids ) : $this->format_item( end( $ids ) );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function nth( $n ) {
+ $per_page = (int) Tribe__Utils__Array::get_in_any( [
+ $this->query_args,
+ $this->default_args,
+ ], 'posts_per_page', get_option( 'posts_per_page' ) );
+
+ if ( - 1 !== $per_page && $n > $per_page ) {
+ return null;
+ }
+
+ $query = $this->build_query();
+
+ $return_ids = 'ids' === $query->get( 'fields', '' );
+
+ $i = absint( $n ) - 1;
+
+ $ids = $this->get_ids();
+
+ if ( empty( $ids[ $i ] ) ) {
+ return null;
+ }
+
+ return $return_ids ? $ids[ $i ] : $this->format_item( $ids[ $i ] );
+ }
+
+ /**
+ * Applies and returns a schema entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param mixed ...$args Additional arguments for the application.
+ *
+ * @return mixed A scalar value or a callable.
+ */
+ public function apply_modifier( $key, $value = null ) {
+ $call_args = func_get_args();
+
+ $application = Tribe__Utils__Array::get( $this->schema, $key, null );
+
+ /**
+ * Return primitives, including `null`, as they are.
+ */
+ if ( ! is_callable( $application ) ) {
+ return $application;
+ }
+
+ /**
+ * Allow for callbacks to fire immediately and return more complex values.
+ * This also means that callbacks meant to run on the next step, the one
+ * where args are applied, will need to be "wrapped" in callbacks themselves.
+ * The `$key` is removed from the args to get the value first and avoid
+ * unused args.
+ */
+ $args_without_key = array_splice( $call_args, 1 );
+
+ $schema_entry = call_user_func_array( $application, $args_without_key );
+
+ /**
+ * Filters the applied modifier schema entry response.
+ *
+ * @param mixed $schema_entry A scalar value or a callable.
+ * @param Tribe__Repository $this This repository instance
+ *
+ * @since 4.9.5
+ */
+ return apply_filters( "tribe_repository_{$this->filter_name}_apply_modifier_schema_entry", $schema_entry, $this );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function take( $n ) {
+ $query = $this->build_query();
+
+ $return_ids = 'ids' === $query->get( 'fields', '' );
+
+ $matching_ids = $this->get_ids();
+
+ if ( empty( $matching_ids ) ) {
+ return [];
+ }
+
+ $spliced = array_splice( $matching_ids, 0, $n );
+
+ return $return_ids ? $spliced : array_map( [ $this, 'format_item' ], $spliced );
+ }
+
+ /**
+ * Fetches a single instance of the post type handled by the repository.
+ *
+ * Similarly to the `get_post` function permissions are not taken into account when returning
+ * an instance by its primary key; extending classes can refine this behaviour to suit.
+ *
+ * @param mixed $primary_key
+ *
+ * @return WP_Post|null|mixed
+ */
+ public function by_primary_key( $primary_key ) {
+ return $this->by( $this->primary_key, $primary_key )->first();
+ }
+
+ /**
+ * Filters posts by simple meta schema value.
+ *
+ * @since 4.9.5
+ *
+ * @param mixed $value Meta value.
+ */
+ public function filter_by_simple_meta_schema( $value ) {
+ $filter = $this->get_current_filter();
+
+ if ( ! array_key_exists( $filter, $this->simple_meta_schema ) ) {
+ return;
+ }
+
+ $simple_meta = $this->simple_meta_schema[ $filter ];
+
+ $by = Tribe__Utils__Array::get( $simple_meta, 'by', 'meta_regexp_or_like' );
+
+ $this->by( $by, $simple_meta['meta_key'], $value );
+ }
+
+ /**
+ * Filters posts by simple tax schema value.
+ *
+ * @since 4.9.5
+ *
+ * @param int|string|array $value Term value(s).
+ */
+ public function filter_by_simple_tax_schema( $value ) {
+ $filter = $this->get_current_filter();
+
+ if ( ! array_key_exists( $filter, $this->simple_tax_schema ) ) {
+ return;
+ }
+
+ $simple_tax = $this->simple_tax_schema[ $filter ];
+
+ $by = Tribe__Utils__Array::get( $simple_tax, 'by', 'term_in' );
+
+ $this->by( $by, $simple_tax['taxonomy'], $value );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by( $key, $value = null ) {
+ if ( $this->void_query || ( 'void_query' === $key && false !== $value ) ) {
+ $this->void_query = true;
+
+ // No point in doing more computations if the query is void.
+ return $this;
+ }
+
+ $call_args = func_get_args();
+
+ $this->current_filters[ $key ] = array_slice( $call_args, 1 );
+
+ try {
+ // Set current filter as which one we are running.
+ $this->current_filter = $key;
+
+ $query_modifier = $this->modify_query( $key, $call_args );
+
+ // Set current filter as no longer active, we aren't running it anymore.
+ $this->current_filter = null;
+
+ /**
+ * Here we allow the repository to call one of its own methods and return `null`.
+ * A repository might have a `where` or `by` that is just building
+ * a more complex query using a base `where` or `by`.
+ */
+ if ( null === $query_modifier ) {
+ return $this;
+ }
+
+ /**
+ * Primitives are just merged in.
+ * Since we are using `array_merge_recursive` we expect them to be arrays.
+ */
+ if ( ! ( is_object( $query_modifier ) || is_callable( $query_modifier ) ) ) {
+
+ if ( ! is_array( $query_modifier ) ) {
+ throw new InvalidArgumentException( 'Query modifier should be an array!' );
+ }
+
+ $replace_modifiers = in_array( $key, $this->replacing_modifiers(), true );
+ if ( $replace_modifiers ) {
+ /**
+ * We do a merge to make sure new values will override and replace the old
+ * ones.
+ */
+ $this->query_args = array_merge( $this->query_args, $query_modifier );
+ } else {
+ /**
+ * We do a recursive merge to allow "stacking" of same kind of queries;
+ * e.g. two or more `tax_query` or `meta_query` entries should merge into one.
+ */
+ $this->query_args = Arr::merge_recursive_query_vars( $this->query_args, $query_modifier );
+ }
+ } else {
+ /**
+ * If we get back something that is not an array then we add it to
+ * the stack of query modifying callbacks we'll call on the query
+ * after building it.
+ */
+ $this->query_modifiers[] = $query_modifier;
+ }
+ } catch ( Exception $e ) {
+ /**
+ * We allow for the `apply` method to orderly fail to micro-optimize.
+ * If applying one parameter would yield no results then let's immediately bail.
+ * Schema should throw t
+ * his Exception if a light-weight on the filters would already
+ * deem a query as yielding nothing.
+ */
+ $this->void_query = true;
+
+ return $this;
+ }
+
+ /**
+ * Catching other type of exceptions is something the client code should handle!
+ */
+
+ return $this;
+ }
+
+ /**
+ * Returns the query modifier for a key.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ * @param array $call_args
+ *
+ * @return mixed
+ *
+ * @throws Tribe__Repository__Usage_Error If the required filter is not defined by the class.
+ * @throws Tribe__Repository__Void_Query_Exception To signal the query would yield no results.
+ */
+ protected function modify_query( $key, $call_args ) {
+ if ( ! $this->schema_has_modifier_for( $key ) ) {
+ if ( $this->has_default_modifier( $key ) ) {
+ // let's use the default filters normalizing the key first
+ $call_args[0] = $this->normalize_key( $key );
+ $query_modifier = call_user_func_array( [ $this, 'apply_default_modifier' ], $call_args );
+ } elseif ( 2 === count( $call_args ) ) {
+ // Pass query argument $key with the single value argument.
+ $query_modifier = [
+ $key => $call_args[1],
+ ];
+ } else {
+ // More than two $call_args were sent (key, value), assume it was meant for a filter that was not defined yet.
+ throw Tribe__Repository__Usage_Error::because_the_read_filter_is_not_defined( $key, $this );
+ }
+ } else {
+ $query_modifier = call_user_func_array( [ $this, 'apply_modifier' ], $call_args );
+ }
+
+ return $query_modifier;
+ }
+
+ /**
+ * Whether the current schema defines an application for the key or not.
+ *
+ * @since 4.7.19
+ *
+ * @param $key
+ *
+ * @return bool
+ */
+ protected function schema_has_modifier_for( $key ) {
+ return isset( $this->schema[ $key ] );
+ }
+
+ /**
+ * Whether a filter defined and handled by the repository exists or not.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ protected function has_default_modifier( $key ) {
+ $normalized_key = $this->normalize_key( $key );
+
+ return in_array( $normalized_key, self::$default_modifiers, true );
+ }
+
+ /**
+ * Normalizes the filter key to allow broad matching of the `by` filters.
+ *
+ * @since 4.7.19
+ *
+ * E.g. `by( 'id', 23 )` is the same as `by( 'ID', 23 ).
+ * E.g. `by( 'parent', 23 )` is the same as `by( `post_parent`, 23 )`
+ *
+ * @param string $key
+ *
+ * @return string The normalized filter key
+ */
+ protected function normalize_key( $key ) {
+ // `ID` to `id`
+ $normalized = strtolower( $key );
+
+ $post_prefixed = [
+ 'password',
+ 'name__in',
+ '_in',
+ '_not_in',
+ 'parent',
+ 'parent__in',
+ 'parent__not_in',
+ 'mime_type',
+ 'content',
+ 'excerpt',
+ 'status',
+ 'modified',
+ 'modified_gmt',
+ 'content_filtered',
+ ];
+
+ if ( in_array( $key, $post_prefixed, true ) ) {
+ $normalized = 'post_' . $key;
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * Returns a list of modifiers that, when applied multiple times,
+ * will replace the previous value.
+ *
+ * This behaviour is in opposition to "stackable" modifiers that will,
+ * instead, be composed and stacked.
+ *
+ * @since 4.7.19
+ *
+ * @return array
+ */
+ protected function replacing_modifiers() {
+ return self::$replacing_modifiers;
+ }
+
+ /**
+ * Batch filter application method.
+ *
+ * This is the same as calling `where` multiple times with different arguments.
+ *
+ * @since 4.7.19
+ *
+ * @param array $args An associative array of arguments to filter
+ * the posts by in the shape [ , ].
+ *
+ * @return Tribe__Repository__Read_Interface|Tribe__Repository__Update_Interface
+ */
+ public function where_args( array $args ) {
+ return $this->by_args( $args );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_args( array $args ) {
+ foreach ( $args as $key => $value ) {
+ $this->by( $key, $value );
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save( $return_promise = false ) {
+ $to_update = $this->get_ids();
+
+ if ( empty( $to_update ) ) {
+ return $return_promise ? new Tribe__Promise() : [];
+ }
+
+ $exit = [];
+ $postarrs = [];
+
+ foreach ( $to_update as $id ) {
+ $postarrs[ $id ] = $this->filter_postarr_for_update( $this->build_postarr( $id ), $id );
+ }
+
+ // If any `filter_postarr_for_update` call returned a falsy value then drop it.
+ $postarrs = array_filter( $postarrs );
+
+ if (
+ $this->is_background_update_active( $to_update )
+ && count( $to_update ) > $this->get_background_update_threshold( $to_update )
+ ) {
+ return $this->async_update( $postarrs, true );
+ }
+
+ $update_callback = $this->get_update_callback( $to_update, false );
+
+ foreach ( $postarrs as $id => $postarr ) {
+ $this_exit = $update_callback( $postarr );
+ $exit[ $id ] = $id === $this_exit ? true : $this_exit;
+ }
+
+ return $return_promise ? new Tribe__Promise : $exit;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_ids() {
+ if ( $this->void_query ) {
+ return [];
+ }
+
+
+ try {
+ /** @var WP_Query $query */
+ $query = $this->get_query();
+
+ // The request property will be set during the `get_posts` method and empty before it.
+ if ( empty( $query->request ) ) {
+ $query->set( 'fields', 'ids' );
+
+ /**
+ * Filters the query object by reference before getting the post IDs from the query.
+ *
+ * @since 4.14.8
+ *
+ * @param WP_Query $query The WP_Query object before get_posts() is called.
+ */
+ do_action( "tribe_repository_{$this->filter_name}_pre_get_ids_for_posts", $query );
+
+ return $query->get_posts();
+ }
+
+ return array_map(
+ static function ( $post ) {
+ if ( is_int( $post ) ) {
+ return $post;
+ }
+ $post_arr = (array) $post;
+
+ return Arr::get( $post_arr, 'ID', Arr::get( $post_arr, 'id', 0 ) );
+ },
+ $query->posts
+ );
+
+ } catch ( Tribe__Repository__Void_Query_Exception $e ) {
+ /*
+ * Extending classes might use this method to run sub-queries
+ * and signal a void query; let's return an empty array.
+ */
+ return [];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_query() {
+ return $this->build_query();
+ }
+
+ /**
+ * Whether the current key can be updated by this repository or not.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ protected function can_be_updated( $key ) {
+ return ! in_array( $key, self::$blocked_keys, true );
+ }
+
+ /**
+ * Whether the current key is a date one requiring a converted key pair too or not.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ protected function requires_converted_date( $key ) {
+ return array_key_exists( $key, $this->to_local_time_map ) || array_key_exists( $key, $this->to_gmt_map );
+ }
+
+ /**
+ * Updates the update post payload to add dates that should be provided in GMT
+ * and localized version.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ * @param string|int $value
+ * @param array $postarr
+ */
+ protected function update_postarr_dates( $key, $value, array &$postarr ) {
+ if ( array_key_exists( $key, $this->to_gmt_map ) ) {
+ $postarr[ $this->to_gmt_map[ $key ] ] = Tribe__Timezones::to_tz( $value, 'UTC' );
+ } elseif ( array_key_exists( $key, $this->to_local_time_map ) ) {
+ $postarr[ $this->to_local_time_map[ $key ] ] = Tribe__Timezones::to_tz( $value, Tribe__Timezones::wp_timezone_string() );
+ }
+ $postarr[ $key ] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_args( array $update_map ) {
+ foreach ( $update_map as $key => $value ) {
+ $this->set( $key, $value );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets the args to be updated during save process.
+ *
+ * @param string $key Argument key.
+ * @param mixed $value Argument value.
+ *
+ * @throws Tribe__Repository__Usage_Error
+ *
+ * @return $this
+ */
+ public function set( $key, $value ) {
+ if ( ! is_string( $key ) ) {
+ throw Tribe__Repository__Usage_Error::because_update_key_should_be_a_string( $this );
+ }
+
+ $this->updates[ $key ] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Sets the create args the repository will use to create posts.
+ *
+ * @since 4.9.5
+ *
+ * @param string|int $image The path to an image file, an image URL, or an attachment post ID.
+ *
+ * @return $this
+ */
+ public function set_featured_image( $image ) {
+ if ( '' === $image || false === $image ) {
+ $thumbnail_id = false;
+ } elseif ( 0 === $image || null === $image ) {
+ $thumbnail_id = '';
+ } else {
+ $thumbnail_id = tribe_upload_image( $image );
+ }
+
+ if ( false === $thumbnail_id ) {
+ return $this;
+ }
+
+ return $this->set( '_thumbnail_id', $thumbnail_id );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter_name( $filter_name ) {
+ $this->filter_name = trim( $filter_name );
+
+ return $this;
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_formatter( Tribe__Repository__Formatter_Interface $formatter ) {
+ $this->formatter = $formatter;
+ }
+
+ /**
+ * Filters the query to only return posts that are related, via a meta key, to posts
+ * that satisfy a condition.
+ *
+ * @param string|array $meta_keys One or more `meta_keys` relating the queried post type(s)
+ * to another post type.
+ * @param string $compare The SQL comparison operator.
+ * @param string $field One (a column in the `posts` table) that should match
+ * the comparison criteria; required if the comparison operator is not `EXISTS` or
+ * `NOT EXISTS`.
+ * @param string|array $values One or more values the post field(s) should be compared to;
+ * required if the comparison operator is not `EXISTS` or `NOT EXISTS`.
+ *
+ * @return $this
+ * @throws Tribe__Repository__Usage_Error If the comparison operator requires
+ */
+ public function where_meta_related_by( $meta_keys, $compare, $field = null, $values = null ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
+
+ if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
+ if ( empty( $field ) || empty( $values ) ) {
+ throw Tribe__Repository__Usage_Error::because_this_comparison_operator_requires_fields_and_values( $meta_keys, $compare, $this );
+ }
+ }
+
+ $field = esc_sql( $field );
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+ $p = $this->sql_slug( 'meta_related_post', $compare, $meta_keys );
+ $pm = $this->sql_slug( 'meta_related_post_meta', $compare, $meta_keys );
+
+ $this->filter_query->join( "LEFT JOIN {$wpdb->postmeta} {$pm} ON {$wpdb->posts}.ID = {$pm}.post_id" );
+ $this->filter_query->join( "LEFT JOIN {$wpdb->posts} {$p} ON {$pm}.meta_value = {$p}.ID" );
+
+ $keys_in = $this->prepare_interval( $meta_keys );
+
+ if ( 'EXISTS' === $compare ) {
+ $this->filter_query->where( "{$pm}.meta_key IN {$keys_in} AND {$pm}.meta_id IS NOT NULL" );
+ } elseif ( 'NOT EXISTS' === $compare ) {
+ $this->filter_query->where( "{$pm}.meta_id IS NULL" );
+ } else {
+ if ( in_array( $compare, self::$multi_value_keys, true ) ) {
+ $values = $this->prepare_interval( $values );
+ } else {
+ $values = $this->prepare_value( $values );
+ }
+ $this->filter_query->where( "{$pm}.meta_key IN {$keys_in} AND {$p}.{$field} {$compare} {$values}" );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filters the query to only return posts that are related, via a meta key, to posts
+ * that satisfy a condition.
+ *
+ * @since 4.10.3
+ *
+ * @throws Tribe__Repository__Usage_Error If the comparison operator requires and no value provided.
+ *
+ * @param string|array $meta_keys One or more `meta_keys` relating the queried post type(s)
+ * to another post type.
+ * @param string $compare The SQL comparison operator.
+ * @param string $meta_field One (a column in the `postmeta` table) that should match
+ * the comparison criteria; required if the comparison operator is not `EXISTS` or
+ * `NOT EXISTS`.
+ * @param string|array $meta_values One or more values the post field(s) should be compared to;
+ * required if the comparison operator is not `EXISTS` or `NOT EXISTS`.
+ * @param boolean $or_not_exists Whether or not to also include a clause to check if value IS NULL.
+ * Example with this as true: `value = X OR value IS NULL`.
+ *
+ * @return $this
+ */
+ public function where_meta_related_by_meta( $meta_keys, $compare, $meta_field = null, $meta_values = null, $or_not_exists = false ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
+
+ if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
+ if ( empty( $meta_field ) || empty( $meta_values ) ) {
+ throw Tribe__Repository__Usage_Error::because_this_comparison_operator_requires_fields_and_values( $meta_keys, $compare, $this );
+ }
+ }
+
+ $meta_field = esc_sql( $meta_field );
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ $pm = $this->sql_slug( 'post_meta_related_post_meta', $compare, $meta_keys );
+ $pmm = $this->sql_slug( 'meta_post_meta_related_post_meta', $compare, $meta_keys );
+
+ $this->filter_query->join( "LEFT JOIN {$wpdb->postmeta} {$pm} ON {$pm}.post_id = {$wpdb->posts}.ID" );
+ $this->filter_query->join( "
+ LEFT JOIN {$wpdb->postmeta} {$pmm}
+ ON {$pmm}.post_id = {$pm}.meta_value
+ AND {$pmm}.meta_key = '{$meta_field}'
+ " );
+
+ $keys_in = $this->prepare_interval( $meta_keys );
+
+ if ( 'EXISTS' === $compare ) {
+ $this->filter_query->where( "
+ {$pm}.meta_key IN {$keys_in}
+ AND {$pmm}.meta_id IS NOT NULL
+ " );
+ } elseif ( 'NOT EXISTS' === $compare ) {
+ $this->filter_query->where( "
+ {$pm}.meta_key IN {$keys_in}
+ AND {$pmm}.meta_id IS NULL
+ " );
+ } else {
+ if ( in_array( $compare, static::$multi_value_keys, true ) ) {
+ $meta_values = $this->prepare_interval( $meta_values );
+ } else {
+ $meta_values = $this->prepare_value( $meta_values );
+ }
+
+ $clause = "{$pmm}.meta_value {$compare} {$meta_values}";
+
+ if ( $or_not_exists ) {
+ $clause = "
+ (
+ {$clause}
+ OR {$pmm}.meta_id IS NULL
+ )
+ ";
+ }
+
+ $this->filter_query->where( "
+ {$pm}.meta_key IN {$keys_in}
+ AND {$clause}
+ " );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Builds a fenced group of WHERE clauses that will be used with OR logic.
+ *
+ * Mind that this is a lower level implementation of WHERE logic that requires
+ * each callback method to add, at least, one WHERE clause using the repository
+ * own `where_clause` method.
+ *
+ * @param array $callbacks One or more WHERE callbacks that will be called
+ * this repository. The callbacks have the shape
+ * [ , <...args>]
+ *
+ * @return $this
+ * @throws Tribe__Repository__Usage_Error If one of the callback methods does
+ * not add any WHERE clause.
+ *
+ * @see Tribe__Repository::where_clause()
+ * @see Tribe__Repository__Query_Filters::where()
+ */
+ public function where_or( $callbacks ) {
+ $all_callbacks = func_get_args();
+ $buffered = $this->filter_query->get_buffered_where_clauses( true );
+
+ $this->filter_query->buffer_where_clauses( true );
+
+ $buffered_count = count( $buffered );
+
+ foreach ( $all_callbacks as $c ) {
+ call_user_func_array( [ $this, $c[0] ], array_slice( $c, 1 ) );
+
+ if ( $buffered_count === count( $this->filter_query->get_buffered_where_clauses() ) ) {
+ throw Tribe__Repository__Usage_Error::because_where_or_should_only_be_used_with_methods_that_add_where_clauses( $c, $this );
+ }
+
+ $buffered_count ++;
+ }
+
+ $buffered = $this->filter_query->get_buffered_where_clauses( true );
+
+ $fenced = sprintf( '( %s )', implode( ' OR ', $buffered ) );
+
+ $this->where_clause( $fenced );
+
+ return $this;
+ }
+
+ /**
+ * Adds an entry to the repository filter schema.
+ *
+ * @since 4.9.5
+ *
+ * @param string $key The filter key, the one that will be used in `by` and `where`
+ * calls.
+ * @param callable $callback The function that should be called to apply this filter.
+ */
+ public function add_schema_entry( $key, $callback ) {
+ $this->schema[ $key ] = $callback;
+ }
+
+ /**
+ * Adds a simple meta entry to the repository filter schema.
+ *
+ * @since 4.9.5
+ *
+ * @param string $key The filter key, the one that will be used in `by` and `where` calls.
+ * @param string|array $meta_key The meta key(s) to use for the meta lookup.
+ * @param string|null $by The ->by() lookup to use (defaults to meta_regexp_or_like).
+ */
+ public function add_simple_meta_schema_entry( $key, $meta_key, $by = null ) {
+ $this->schema[ $key ] = [ $this, 'filter_by_simple_meta_schema' ];
+
+ $this->simple_meta_schema[ $key ] = [
+ 'meta_key' => $meta_key,
+ 'by' => $by,
+ ];
+ }
+
+ /**
+ * Adds a simple taxonomy entry to the repository filter schema.
+ *
+ * @since 4.9.5
+ *
+ * @param string $key The filter key, the one that will be used in `by` and `where` calls.
+ * @param string|array $taxonomy The taxonomy/taxonomies to use for the tax lookup.
+ * @param string|null $by The ->by() lookup to use (defaults to term_in).
+ */
+ public function add_simple_tax_schema_entry( $key, $taxonomy, $by = null ) {
+ $this->schema[ $key ] = [ $this, 'filter_by_simple_tax_schema' ];
+
+ $this->simple_tax_schema[ $key ] = [
+ 'taxonomy' => $taxonomy,
+ 'by' => $by,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add_update_field_alias( $alias, $field_name ) {
+ $this->update_fields_aliases[ $alias ] = $field_name;
+ }
+
+ /**
+ * Returns modified query arguments after applying a default filter.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return array
+ * @throws Tribe__Repository__Usage_Error If a filter is called with wrong arguments.
+ */
+ protected function apply_default_modifier( $key, $value ) {
+ $args = [];
+
+ $call_args = func_get_args();
+ $arg_1 = isset( $call_args[2] ) ? $call_args[2] : null;
+ $arg_2 = isset( $call_args[3] ) ? $call_args[3] : null;
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ switch ( $key ) {
+ default:
+ // leverage built-in WP_Query filters
+ $args = [ $key => $value ];
+ break;
+ case 'ID':
+ case 'id':
+ $args = [ 'p' => $value ];
+ break;
+ case 'search':
+ if ( '' !== $value ) {
+ $args = [ 's' => $value ];
+ }
+ break;
+ case 'post_status':
+ $this->query_args['post_status'] = (array) $value;
+ break;
+ case 'date':
+ case 'after_date':
+ $args = $this->get_posts_after( $value, 'post_date' );
+ break;
+ case 'before_date':
+ $args = $this->get_posts_before( $value, 'post_date' );
+ break;
+ case 'date_gmt':
+ case 'after_date_gmt':
+ $args = $this->get_posts_after( $value, 'post_date_gmt' );
+ break;
+ case 'before_date_gmt':
+ $args = $this->get_posts_before( $value, 'post_date_gmt' );
+ break;
+ case 'title_like':
+ $this->filter_query->to_get_posts_with_title_like( $value );
+ break;
+ case 'post_content':
+ $this->filter_query->to_get_posts_with_content_like( $value );
+ break;
+ case 'post_excerpt':
+ $this->filter_query->to_get_posts_with_excerpt_like( $value );
+ break;
+ case 'to_ping':
+ $this->filter_query->to_get_posts_to_ping( $value );
+ $args = [ 'to_ping' => $value ];
+ break;
+ case 'post_modified':
+ $args = $this->get_posts_after( $value, 'post_modified' );
+ break;
+ case 'post_modified_gmt':
+ $args = $this->get_posts_after( $value, 'post_modified_gmt' );
+ break;
+ case 'post_content_filtered':
+ $this->filter_query->to_get_posts_with_filtered_content_like( $value );
+ break;
+ case 'guid':
+ $this->filter_query->to_get_posts_with_guid_like( $value );
+ break;
+ case 'menu_order':
+ $args = [ 'menu_order' => $value ];
+ break;
+ case 'meta':
+ case 'meta_equals':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '=', $format = $arg_2 );
+ break;
+ case 'meta_not_equals':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '!=', $format = $arg_2 );
+ break;
+ case 'meta_gt':
+ case 'meta_greater_than':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '>', $format = $arg_2 );
+ break;
+ case 'meta_gte':
+ case 'meta_greater_than_or_equal':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '>=', $format = $arg_2 );
+ break;
+ case 'meta_like':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'LIKE' );
+ break;
+ case 'meta_not_like':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT LIKE' );
+ break;
+ case 'meta_lt':
+ case 'meta_less_than':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '<', $format = $arg_2 );
+ break;
+ case 'meta_lte':
+ case 'meta_less_than_or_equal':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '<=', $format = $arg_2 );
+ break;
+ case 'meta_in':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'IN', $format = $arg_2 );
+ break;
+ case 'meta_not_in':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT IN', $format = $arg_2 );
+ break;
+ case 'meta_between':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'BETWEEN', $format = $arg_2 );
+ break;
+ case 'meta_not_between':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT BETWEEN', $format = $arg_2 );
+ break;
+ case 'meta_exists':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'EXISTS' );
+ break;
+ case 'meta_not_exists':
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT EXISTS' );
+ break;
+ case 'meta_regexp':
+ case 'meta_equals_regexp':
+ // Check if Regexp is fenced.
+ if ( tribe_is_regex( $arg_1 ) ) {
+ // Unfence the Regexp.
+ $arg_1 = tribe_unfenced_regex( $arg_1 );
+ }
+
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'REGEXP' );
+ break;
+ case 'meta_not_regexp':
+ case 'meta_not_equals_regexp':
+ // Check if Regexp is fenced.
+ if ( tribe_is_regex( $arg_1 ) ) {
+ // Unfence the Regexp.
+ $arg_1 = tribe_unfenced_regex( $arg_1 );
+ }
+
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT REGEXP' );
+ break;
+ case 'meta_regexp_or_like':
+ case 'meta_equals_regexp_or_like':
+ $compare = 'LIKE';
+
+ // Check if Regexp is fenced (the only way for Regexp to be supported in this context).
+ if ( tribe_is_regex( $arg_1 ) ) {
+ $compare = 'REGEXP';
+
+ // Unfence the Regexp.
+ $arg_1 = tribe_unfenced_regex( $arg_1 );
+ }
+
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, $compare );
+ break;
+ case 'meta_not_regexp_or_like':
+ case 'meta_not_equals_regexp_or_like':
+ $compare = 'NOT LIKE';
+
+ // Check if Regexp is fenced (the only way for Regexp to be supported in this context).
+ if ( tribe_is_regex( $arg_1 ) ) {
+ $compare = 'NOT REGEXP';
+
+ // Unfence the Regexp.
+ $arg_1 = tribe_unfenced_regex( $arg_1 );
+ }
+
+ $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, $compare );
+ break;
+ case 'taxonomy_exists':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'EXISTS' );
+ break;
+ case 'taxonomy_not_exists':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT EXISTS' );
+ break;
+ case 'term_id_in':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'IN' );
+ break;
+ case 'term_id_not_in':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT IN' );
+ break;
+ case 'term_id_and':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'AND' );
+ break;
+ case 'term_name_in':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'IN' );
+ break;
+ case 'term_name_not_in':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'NOT IN' );
+ break;
+ case 'term_name_and':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'AND' );
+ break;
+ case 'term_slug_in':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'IN' );
+ break;
+ case 'term_slug_not_in':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'NOT IN' );
+ break;
+ case 'term_slug_and':
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'AND' );
+ break;
+ case 'term_in':
+ $arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false );
+
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'IN' );
+ break;
+ case 'term_not_in':
+ $arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false );
+
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT IN' );
+ break;
+ case 'term_and':
+ $arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false );
+
+ $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'AND' );
+ break;
+ }
+
+
+ return $args;
+ }
+
+ /**
+ * Builds a date query entry to get posts after a date.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ * @param string $column
+ *
+ * @return array
+ */
+ protected function get_posts_after( $value, $column = 'post_date' ) {
+ $timezone = in_array( $column, [ 'post_date_gmt', 'post_modified_gmt' ], true )
+ ? 'UTC'
+ : Tribe__Timezones::generate_timezone_string_from_utc_offset( Tribe__Timezones::wp_timezone_string() );
+
+ if ( is_numeric( $value ) ) {
+ $value = "@{$value}";
+ }
+
+ $date = new DateTime( $value, new DateTimeZone( $timezone ) );
+
+ $array_key = sprintf( '%s-after', $column );
+
+ return [
+ 'date_query' => [
+ 'relation' => 'AND',
+ $array_key => [
+ 'inclusive' => true,
+ 'column' => $column,
+ 'after' => $date->format( 'Y-m-d H:i:s' ),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Builds a date query entry to get posts before a date.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ * @param string $column
+ *
+ * @return array
+ */
+ protected function get_posts_before( $value, $column = 'post_date' ) {
+ $timezone = in_array( $column, [ 'post_date_gmt', 'post_modified_gmt' ], true )
+ ? 'UTC'
+ : Tribe__Timezones::generate_timezone_string_from_utc_offset( Tribe__Timezones::wp_timezone_string() );
+
+ if ( is_numeric( $value ) ) {
+ $value = "@{$value}";
+ }
+
+ $date = new DateTime( $value, new DateTimeZone( $timezone ) );
+
+ $array_key = sprintf( '%s-before', $column );
+
+ return [
+ 'date_query' => [
+ 'relation' => 'AND',
+ $array_key => [
+ 'inclusive' => true,
+ 'column' => $column,
+ 'before' => $date->format( 'Y-m-d H:i:s' ),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Builds a meta query entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $meta_key
+ * @param string|array $meta_value
+ * @param string $compare
+ * @param string $type_or_format The type of value to compare
+ *
+ * @return array|null
+ * @throws Tribe__Repository__Usage_Error If trying to compare multiple values with a single
+ * comparison operator.
+ */
+ protected function build_meta_query( $meta_key, $meta_value = 'value', $compare = '=', $type_or_format = null ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_key );
+
+ $postfix = Tribe__Utils__Array::get( self::$comparison_operators, $compare, '' );
+
+ if ( count( $meta_keys ) === 1 ) {
+ $array_key = $this->sql_slug( $meta_keys[0], $postfix );
+
+ $args = [
+ 'meta_query' => [
+ $array_key => [
+ 'key' => $meta_keys[0],
+ 'compare' => strtoupper( $compare ),
+ ],
+ ],
+ ];
+
+ if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
+ $args['meta_query'][ $array_key ]['value'] = $meta_value;
+ }
+
+ if ( is_string( $type_or_format ) && 0 === strpos( $type_or_format, '%' ) ) {
+ throw Tribe__Repository__Usage_Error::because_the_type_is_a_wpdb_prepare_format( $meta_key, $type_or_format, $this );
+ }
+
+ if ( null !== $type_or_format ) {
+ $args['meta_query'][ $array_key ]['type'] = $type_or_format;
+ }
+
+ return $args;
+ }
+
+
+ if ( null === $type_or_format ) {
+ $type_or_format = '%s';
+ } elseif ( 0 !== strpos( $type_or_format, '%' ) ) {
+ throw Tribe__Repository__Usage_Error::because_the_format_is_not_a_wpdb_prepare_one( $meta_key, $type_or_format, $this );
+ }
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ // Build custom WHERE and JOINS to reduce the JOIN clauses
+ $pm_alias = $this->sql_slug( 'meta', $postfix, ++ self::$meta_alias );
+ $meta_keys_in = sprintf( "('%s')", implode( "','", array_map( 'esc_sql', $meta_keys ) ) );
+
+ $this->validate_operator_and_values( $compare, $meta_keys, $meta_value );
+
+ if ( in_array( $compare, self::$multi_value_keys, true ) ) {
+ $meta_values = $this->prepare_interval( Tribe__Utils__Array::list_to_array( $meta_value ), $type_or_format );
+ } else {
+ $meta_values = $this->prepare_value( $meta_value, $type_or_format );
+ }
+
+ $this->filter_query->join( "JOIN {$wpdb->postmeta} {$pm_alias} ON {$wpdb->posts}.ID = {$pm_alias}.post_id" );
+
+ if ( 'EXISTS' === $compare ) {
+ $this->filter_query->where( "{$pm_alias}.meta_key IN {$meta_keys_in} AND {$pm_alias}.meta_id IS NOT NULL" );
+ } elseif ( 'NOT EXISTS' === $compare ) {
+ $this->filter_query->where( "{$pm_alias}.meta_key NOT IN {$meta_keys_in} AND {$pm_alias}.meta_id IS NOT NULL" );
+ } else {
+ $this->filter_query->where( "{$pm_alias}.meta_key IN {$meta_keys_in} AND {$pm_alias}.meta_value {$compare} {$meta_values}" );
+ }
+ }
+
+ /**
+ * Generates a SQL friendly slug from the provided, variadic, fragments.
+ *
+ * @since 4.7.19
+ *
+ * @param ...string $frag
+ *
+ * @return string
+ */
+ protected function sql_slug( $frag ) {
+ $frags = func_get_args();
+
+ foreach ( $frags as &$frag ) {
+ if ( is_string( $frag ) ) {
+ Tribe__Utils__Array::get( self::$comparison_operators, $frag, $frag );
+ } elseif ( is_array( $frag ) ) {
+ $frag = implode( '_', $frag );
+ }
+ }
+
+
+ $frags = array_filter( $frags );
+
+ return strtolower( str_replace( '-', '_', sanitize_title( implode( '_', $frags ) ) ) );
+ }
+
+ /**
+ * Builds a taxonomy query entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $taxonomy
+ * @param int|string|array $terms
+ * @param string $field
+ * @param string $operator
+ *
+ * @return array
+ */
+ protected function build_tax_query( $taxonomy, $terms, $field, $operator ) {
+ if ( in_array( $operator, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
+ $array_key = $this->sql_slug( $taxonomy, $operator );
+ } else {
+ $array_key = $this->sql_slug( $taxonomy, $field, $operator );
+ }
+
+ return [
+ 'tax_query' => [
+ $array_key => [
+ 'taxonomy' => $taxonomy,
+ 'field' => $field,
+ 'terms' => $terms,
+ 'operator' => strtoupper( $operator ),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function join_clause( $join ) {
+ $this->filter_query->join( $join );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where_clause( $where ) {
+ $this->filter_query->where( $where );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_query_builder( $query_builder ) {
+ $this->query_builder = $query_builder;
+ }
+
+ /**
+ * Builds and escapes an interval of strings.
+ *
+ * The return string includes opening and closing braces.
+ *
+ * @since 4.7.19
+ *
+ * @param string|array $values One or more values to use to build
+ * the interval
+ * .
+ * @param string $format The format that should be used to escape
+ * the values; default to '%s'.
+ * @param string $operator The operator the interval is being prepared for;
+ * defaults to `IN`.
+ *
+ * @return string
+ */
+ public function prepare_interval( $values, $format = '%s', $operator = 'IN' ) {
+ $values = Tribe__Utils__Array::list_to_array( $values );
+
+ $prepared = [];
+ foreach ( $values as $value ) {
+ $prepared[] = $this->prepare_value( $value, $format );
+ }
+
+ return in_array( $operator, [ 'BETWEEN', 'NOT BETWEEN' ] )
+ ? sprintf( '%s AND %s', $prepared[0], $prepared[1] )
+ : sprintf( '(%s)', implode( ',', $prepared ) );
+ }
+
+ /**
+ * Prepares a single value to be used in a SQL query.
+ *
+ * @since 4.7.19
+ *
+ * @param mixed $value
+ * @param string $format
+ *
+ * @return string
+ */
+ public function prepare_value( $value, $format = '%s' ) {
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ return $wpdb->prepare( $format, $value );
+ }
+
+ /**
+ * Validates that a comparison operator is used with the correct type of values.
+ *
+ * This is just a wrap to signal this kind of code error not in bad SQL error but
+ * with a visible exception.
+ *
+ * @since 4.7.19
+ *
+ * @param string $compare A SQL comparison operator
+ * @param string|array $meta_key
+ * @param mixed $meta_value
+ *
+ * @throws Tribe__Repository__Usage_Error
+ */
+ protected function validate_operator_and_values( $compare, $meta_key, $meta_value ) {
+ if ( is_array( $meta_value ) && ! in_array( $compare, self::$multi_value_keys, true ) ) {
+ throw Tribe__Repository__Usage_Error::because_single_value_comparisons_should_be_used_with_one_value(
+ $meta_key,
+ $meta_value,
+ $compare,
+ $this
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_related_to_min( $by_meta_keys, $min, $keys = null, $values = null ) {
+ $min = $this->prepare_value( $min, '%d' );
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ $by_meta_keys = $this->prepare_interval( $by_meta_keys );
+
+ $join = '';
+ $and_where = '';
+ if ( ! empty( $keys ) || ! empty( $values ) ) {
+ $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n";
+ }
+ if ( ! empty( $keys ) ) {
+ $keys = $this->prepare_interval( $keys );
+ $and_where .= "\nAND pm2.meta_key IN {$keys}\n";
+ }
+ if ( ! empty( $values ) ) {
+ $values = $this->prepare_interval( $values );
+ $and_where .= "\nAND pm2.meta_value IN {$values}\n";
+ }
+
+ $this->where_clause( "{$wpdb->posts}.ID IN (
+ SELECT pm1.meta_value
+ FROM {$wpdb->postmeta} pm1 {$join}
+ WHERE pm1.meta_key IN {$by_meta_keys} {$and_where}
+ GROUP BY( pm1.meta_value )
+ HAVING COUNT(DISTINCT pm1.post_id) >= {$min}
+ )" );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_related_to_max( $by_meta_keys, $max, $keys = null, $values = null ) {
+ $max = $this->prepare_value( $max, '%d' );
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ $join = '';
+ $and_where = '';
+ if ( ! empty( $keys ) || ! empty( $values ) ) {
+ $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n";
+ }
+ if ( ! empty( $keys ) ) {
+ $keys = $this->prepare_interval( $keys );
+ $and_where .= "\nAND pm2.meta_key IN {$keys}\n";
+ }
+ if ( ! empty( $values ) ) {
+ $values = $this->prepare_interval( $values );
+ $and_where .= "\nAND pm2.meta_value IN {$values}\n";
+ }
+
+ $by_meta_keys = $this->prepare_interval( $by_meta_keys );
+
+ $this->where_clause( "{$wpdb->posts}.ID IN (
+ SELECT pm1.meta_value
+ FROM {$wpdb->postmeta} pm1 {$join}
+ WHERE pm1.meta_key IN {$by_meta_keys} {$and_where}
+ GROUP BY( pm1.meta_value )
+ HAVING COUNT(DISTINCT pm1.post_id) <= {$max}
+ )" );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_related_to_between( $by_meta_keys, $min, $max, $keys = null, $values = null ) {
+ $min = $this->prepare_value( $min, '%d' );
+ $max = $this->prepare_value( $max, '%d' );
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ $by_meta_keys = $this->prepare_interval( $by_meta_keys );
+
+ $join = '';
+ $and_where = '';
+ if ( ! empty( $keys ) || ! empty( $values ) ) {
+ $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n";
+ }
+ if ( ! empty( $keys ) ) {
+ $keys = $this->prepare_interval( $keys );
+ $and_where .= "\nAND pm2.meta_key IN {$keys}\n";
+ }
+ if ( ! empty( $values ) ) {
+ $values = $this->prepare_interval( $values );
+ $and_where .= "\nAND pm2.meta_value IN {$values}\n";
+ }
+
+ $this->where_clause( "{$wpdb->posts}.ID IN (
+ SELECT pm1.meta_value
+ FROM {$wpdb->postmeta} pm1 {$join}
+ WHERE pm1.meta_key IN {$by_meta_keys} {$and_where}
+ GROUP BY( pm1.meta_value )
+ HAVING COUNT(DISTINCT pm1.post_id) BETWEEN {$min} AND {$max}
+ )" );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_not_related_to( $by_meta_keys, $keys = null, $values = null ) {
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ $by_meta_keys = $this->prepare_interval( $by_meta_keys );
+
+ $join = '';
+ $and_where = '';
+ if ( ! empty( $keys ) || ! empty( $values ) ) {
+ $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n";
+ }
+ if ( ! empty( $keys ) ) {
+ $keys = $this->prepare_interval( $keys );
+ $and_where .= "\nAND pm2.meta_key IN {$keys}\n";
+ }
+ if ( ! empty( $values ) ) {
+ $values = $this->prepare_interval( $values );
+ $and_where .= "\nAND pm2.meta_value IN {$values}\n";
+ }
+
+ $this->where_clause( "{$wpdb->posts}.ID NOT IN (
+ SELECT pm1.meta_value
+ FROM {$wpdb->postmeta} pm1 {$join}
+ WHERE pm1.meta_key IN {$by_meta_keys} {$and_where}
+ GROUP BY( pm1.meta_value )
+ )" );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has_filter( $key, $value = null ) {
+ $args = func_get_args();
+ $values = array_slice( $args, 1 );
+
+ if ( null === $value ) {
+ // We just want to check if a filter is applied.
+ return array_key_exists( $key, $this->current_filters );
+ }
+
+ // We check if the filter exists and the arguments match; inline to prevent "Undefined index" errors.
+ return array_key_exists( $key, $this->current_filters ) && array_slice(
+ $this->current_filters[ $key ],
+ 0,
+ min( count( $this->current_filters[ $key ] ), count( $values ) )
+ ) === $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_current_filter() {
+ return $this->current_filter;
+ }
+
+ /**
+ * Returns a map relating comparison operators to their "pretty" name.
+ *
+ * @since 4.9.5
+ *
+ * @return array
+ */
+ public static function get_comparison_operators() {
+ return self::$comparison_operators;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete( $return_promise = false ) {
+ $to_delete = $this->get_ids();
+
+ if ( empty( $to_delete ) ) {
+ return $return_promise ? new Tribe__Promise() : [];
+ }
+
+
+ /**
+ * Filters the post delete operation allowing third party code to bail out of
+ * the process completely.
+ *
+ * @since 4.9.5
+ *
+ * @param array|null $deleted An array containing the the IDs of the deleted posts.
+ * @param self $this This repository instance.
+ */
+ $deleted = apply_filters( "tribe_repository_{$this->filter_name}_delete", null, $to_delete );
+ if ( null !== $deleted ) {
+ return $deleted;
+ }
+
+ if (
+ $this->is_background_delete_active( $to_delete )
+ && count( $to_delete ) > $this->get_background_delete_threshold( $to_delete )
+ ) {
+ return $this->async_delete( $to_delete, $return_promise );
+ }
+
+ $delete_callback = $this->get_delete_callback( $to_delete );
+
+ foreach ( $to_delete as $id ) {
+ $done = $delete_callback( $id );
+
+ if ( empty( $done ) ) {
+ tribe( 'logger' )->log(
+ __( 'Could not delete post with ID ' . $id, 'tribe-common' ),
+ Tribe__Log::WARNING,
+ $this->filter_name
+ );
+ continue;
+ }
+ $deleted[] = $id;
+ }
+
+ return $return_promise ? new Tribe__Promise() : $deleted;
+ }
+
+ /**
+ * Whether background delete is activated for the repository or not.
+ *
+ * @since 4.9.5
+ *
+ * @param array $to_delete An array of post IDs to delete.
+ *
+ * @return bool Whether background delete is activated for the repository or not.
+ */
+ protected function is_background_delete_active( $to_delete ) {
+ /**
+ * Whether background, asynchronous, deletion of posts is active or not for all repositories.
+ *
+ * If active then if the number of posts to delete is over the threshold, defined
+ * by the `tribe_repository_delete_background_threshold` filter, then the deletion will happen
+ * in background in other requests.
+ *
+ * @since 4.9.5
+ *
+ * @param bool $background_active Whether background deletion is active or not.
+ * @param array $to_delete The array of post IDs to delete.
+ */
+ $background_active = (bool) apply_filters( 'tribe_repository_delete_background_activated', true, $to_delete );
+
+ /**
+ * Whether background, asynchronous, deletion of posts is active or not for this specific repository.
+ *
+ * If active then if the number of posts to delete is over the threshold, defined
+ * by the `tribe_repository_delete_background_threshold` filter, then the deletion will happen
+ * in background in other requests.
+ *
+ * @since 4.9.5
+ *
+ * @param bool $background_active Whether background deletion is active or not.
+ * @param array $to_delete The array of post IDs to delete.
+ */
+ $background_active = (bool) apply_filters(
+ "tribe_repository_{$this->filter_name}_delete_background_activated",
+ $background_active,
+ $to_delete
+ );
+
+ return $background_active;
+ }
+
+ /**
+ * Returns the threshold above which posts will be deleted in background.
+ *
+ * @since 4.9.5
+ *
+ * @param array $to_delete An array of post IDs to delete.
+ *
+ * @return int The threshold above which posts will be deleted in background.
+ */
+ protected function get_background_delete_threshold( $to_delete ) {
+ /**
+ * The number of posts above which the deletion will happen in background.
+ *
+ * This filter will be ignored if background delete is deactivated with the `tribe_repository_delete_background_activated`
+ * or `tribe_repository_{$this->filter_name}_delete_background_activated` filter.
+ *
+ * @since 4.9.5
+ *
+ * @param int The threshold over which posts will be deleted in background.
+ * @param array $to_delete The post IDs to delete.
+ */
+ $background_threshold = (int) apply_filters( 'tribe_repository_delete_background_threshold', 20, $to_delete );
+
+ /**
+ * The number of posts above which the deletion will happen in background.
+ *
+ * This filter will be ignored if background delete is deactivated with the `tribe_repository_delete_background_activated`
+ * or `tribe_repository_{$this->filter_name}_delete_background_activated` filter.
+ *
+ * @since 4.9.5
+ *
+ * @param int The threshold over which posts will be deleted in background.
+ * @param array $to_delete The post IDs to delete.
+ */
+ $background_threshold = (int) apply_filters(
+ "tribe_repository_{$this->filter_name}_delete_background_threshold",
+ $background_threshold,
+ $to_delete
+ );
+
+ return $background_threshold;
+ }
+
+ /**
+ * Whether background update is activated for the repository or not.
+ *
+ * @since 4.9.5
+ *
+ * @param array $to_update An array of post IDs to update.
+ *
+ * @return bool Whether background update is activated for the repository or not.
+ */
+ protected function is_background_update_active( $to_update ) {
+ /**
+ * Whether background, asynchronous, update of posts is active or not for all repositories.
+ *
+ * If active then if the number of posts to update is over the threshold, defined
+ * by the `tribe_repository_update_background_threshold` filter, then the update will happen
+ * in background in other requests.
+ *
+ * @since 4.9.5
+ *
+ * @param bool $background_active Whether background update is active or not.
+ * @param array $to_update The array of post IDs to update.
+ */
+ $background_active = (bool) apply_filters( 'tribe_repository_update_background_activated', true, $to_update );
+
+ /**
+ * Whether background, asynchronous, update of posts is active or not for this specific repository.
+ *
+ * If active then if the number of posts to update is over the threshold, defined
+ * by the `tribe_repository_update_background_threshold` filter, then the update will happen
+ * in background in other requests.
+ *
+ * @since 4.9.5
+ *
+ * @param bool $background_active Whether background update is active or not.
+ * @param array $to_update The array of post IDs to update.
+ */
+ $background_active = (bool) apply_filters(
+ "tribe_repository_{$this->filter_name}_update_background_activated",
+ $background_active,
+ $to_update
+ );
+
+ return $background_active;
+ }
+
+ /**
+ * Returns the threshold above which posts will be updated in background.
+ *
+ * @since 4.9.5
+ *
+ * @param array $to_update An array of post IDs to update.
+ *
+ * @return int The threshold above which posts will be updated in background.
+ */
+ protected function get_background_update_threshold( $to_update ) {
+ /**
+ * The number of posts above which the update will happen in background.
+ *
+ * This filter will be ignored if background update is deactivated with the `tribe_repository_update_background_activated`
+ * or `tribe_repository_{$this->filter_name}_update_background_activated` filter.
+ *
+ * @since 4.9.5
+ *
+ * @param int The threshold over which posts will be updated in background.
+ * @param array $to_update The post IDs to update.
+ */
+ $background_threshold = (int) apply_filters( 'tribe_repository_update_background_threshold', 20, $to_update );
+
+ /**
+ * The number of posts above which the update will happen in background.
+ *
+ * This filter will be ignored if background update is deactivated with the `tribe_repository_update_background_activated`
+ * or `tribe_repository_{$this->filter_name}_update_background_activated` filter.
+ *
+ * @since 4.9.5
+ *
+ * @param int The threshold over which posts will be updated in background.
+ * @param array $to_update The post IDs to update.
+ */
+ $background_threshold = (int) apply_filters(
+ "tribe_repository_{$this->filter_name}_update_background_threshold",
+ $background_threshold,
+ $to_update
+ );
+
+ return $background_threshold;
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function async_delete( array $to_delete, $return_promise = true ) {
+ $promise = new Tribe__Promise( $this->get_delete_callback( $to_delete, true ), $to_delete );
+ if ( ! $return_promise ) {
+ // Dispatch it immediately and return the IDs that will be deleted.
+ $promise->save()->dispatch();
+
+ return $to_delete;
+ }
+
+ // Return the promise and let the client do the dispatching.
+ return $promise;
+ }
+
+ /**
+ * Returns the delete callback function or method to use to delete posts.
+ *
+ * @since 4.9.5
+ *
+ * @param int|array $to_delete The post ID to delete or an array of post IDs to delete.
+ * @param bool $background Whether the callback will be used in background delete operations or not.
+ *
+ * @return callable The callback to use.
+ */
+ protected function get_delete_callback( $to_delete, $background = false ) {
+ /**
+ * Filters the callback that all repositories should use to delete posts.
+ *
+ * @since 4.9.5
+ *
+ * @param callable $callback The callback that should be used to delete each post; defaults
+ * to `wp_delete_post`; falsy return values will be interpreted as
+ * failures to delete.
+ * @param array|int $to_delete An array of post IDs to delete.
+ * @param bool $background Whether the delete operation will happen in background or not.
+ */
+ $callback = apply_filters( 'tribe_repository_delete_callback', 'wp_delete_post', (array) $to_delete, (bool) $background );
+
+ /**
+ * Filters the callback that all repositories should use to delete posts.
+ *
+ * @since 4.9.5
+ *
+ * @param callable $callback The callback that should be used to delete each post; defaults
+ * to `wp_delete_post`; falsy return values will be interpreted as
+ * failures to delete.
+ * @param array|int $to_delete An array of post IDs to delete.
+ * @param bool $background Whether the delete operation will happen in background or not.
+ */
+ $callback = apply_filters(
+ "tribe_repository_{$this->filter_name}_delete_callback",
+ $callback,
+ (array) $to_delete,
+ (bool) $background
+ );
+
+ return $callback;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_filter_name() {
+ return $this->filter_name;
+ }
+
+ /**
+ * Returns the update callback function or method to use to update posts.
+ *
+ * @since 4.9.5
+ *
+ * @param int|array $to_update The post ID to update or an array of post IDs to update.
+ * @param bool $background Whether the callback will be used in background update operations or not.
+ *
+ * @return callable The callback to use.
+ */
+ protected function get_update_callback( $to_update, $background = false ) {
+ /**
+ * Filters the callback that all repositories should use to update posts.
+ *
+ * @since 4.9.5
+ *
+ * @param callable $callback The callback that should be used to update each post; defaults
+ * to `wp_update_post`; falsy return values will be interpreted as
+ * failures to update.
+ * @param array|int $to_update An array of post IDs to update.
+ * @param bool $background Whether the update operation will happen in background or not.
+ */
+ $callback = apply_filters( 'tribe_repository_update_callback', 'wp_update_post', (array) $to_update, (bool) $background );
+
+ /**
+ * Filters the callback that all repositories should use to update posts.
+ *
+ * @since 4.9.5
+ *
+ * @param callable $callback The callback that should be used to update each post; defaults
+ * to `wp_update_post`; falsy return values will be interpreted as
+ * failures to update.
+ * @param array|int $to_update An array of post IDs to update.
+ * @param bool $background Whether the update operation will happen in background or not.
+ */
+ $callback = apply_filters(
+ "tribe_repository_{$this->filter_name}_update_callback",
+ $callback,
+ (array) $to_update,
+ (bool) $background
+ );
+
+ return $callback;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function async_update( array $to_update, $return_promise = true ) {
+ $promise = new Tribe__Promise( $this->get_update_callback( $to_update, true ), $to_update );
+ if ( ! $return_promise ) {
+ // Dispatch it immediately and return the IDs that will be deleted.
+ $promise->save()->dispatch();
+
+ return $to_update;
+ }
+
+ // Return the promise and let the client do the dispatching.
+ return $promise;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_update_fields_aliases() {
+ return $this->update_fields_aliases;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_update_fields_aliases( array $update_fields_aliases ) {
+ $this->update_fields_aliases = $update_fields_aliases;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter_postarr_for_update( array $postarr, $post_id ) {
+ /**
+ * Filters the post array that will be used for an update.
+ *
+ * @since 4.9.5
+ *
+ * @param array $postarr The post array that will be sent to the update callback.
+ * @param int The post ID if set.
+ */
+ return apply_filters( "tribe_repository_{$this->filter_name}_update_postarr", $postarr, $post_id );
+ }
+
+ /**
+ * A utility method to cast any PHP error into an exception proper.
+ *
+ * Usage: `set_error_handler( array( $repository, 'cast_error_to_exception' ) );
+ *
+ * @since 4.9.5
+ *
+ * @param int $code The error code.
+ * @param string $message The error message.
+ */
+ public function cast_error_to_exception( $code, $message ) {
+ throw new RuntimeException( $message, $code );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create() {
+ $postarr = $this->filter_postarr_for_create( array_merge( $this->build_postarr(), $this->create_args ) );
+
+ // During the filtering allow extending classes or filters to prevent the create completely.
+ if ( false === ( bool ) $postarr ) {
+ return false;
+ }
+
+ $created = call_user_func( $this->get_create_callback( $postarr ), $postarr );
+
+ $post = $this->format_item( $created );
+
+ return $post instanceof WP_Post && $post->ID === $created ? $post : false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter_postarr_for_create( array $postarr ) {
+ /**
+ * Filters the post array that will be used for the creation of a post
+ * of the type managed by the repository.
+ *
+ * @since 4.9.5
+ *
+ * @param array $postarr The post array that will be sent to the create callback.
+ */
+ return apply_filters( "tribe_repository_{$this->filter_name}_update_postarr", $postarr );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build_postarr( $id = null ) {
+ $postarr = [
+ 'tax_input' => [],
+ 'meta_input' => [],
+ ];
+
+ /*
+ * The check is lax here by design: we leave space for the client code
+ * to use this method to build post arrays; when this is used by the
+ * repository the integrity of `$id` is granted.
+ */
+ $is_update = null !== $id && is_numeric( $id );
+
+ // But still let's provide values that make sense.
+ if ( $is_update ) {
+ $postarr['ID'] = (int) $id;
+ }
+
+ foreach ( $this->updates as $key => $value ) {
+ if (
+ $value instanceof Closure ||
+ ( is_array( $value ) && is_callable( $value ) )
+ ) {
+ $value = $value( $id, $key, $this );
+ }
+
+ // Allow fields to be aliased
+ $key = Tribe__Utils__Array::get( $this->update_fields_aliases, $key, $key );
+
+ if ( ! $this->can_be_updated( $key ) ) {
+ throw Tribe__Repository__Usage_Error::because_this_field_cannot_be_updated( $key, $this );
+ }
+
+ if ( $this->is_a_post_field( $key ) ) {
+ if ( $this->requires_converted_date( $key ) ) {
+ $this->update_postarr_dates( $key, $value, $postarr );
+ } else {
+ $postarr[ $key ] = $value;
+ }
+ } elseif ( $this->is_a_taxonomy( $key ) ) {
+ $taxonomy = get_taxonomy( $key );
+ if ( $taxonomy instanceof WP_Taxonomy ) {
+ $postarr['tax_input'][ $key ] = Tribe__Utils__Array::list_to_array( $value );
+ }
+ } else {
+ // it's a custom field
+ $postarr['meta_input'][ $key ] = $value;
+ }
+ }
+
+ return $postarr;
+ }
+
+ /**
+ * Returns the create callback function or method to use to create posts.
+ *
+ * @since 4.9.5
+ *
+ * @param array $postarr The post array that will be used for the creation.
+ *
+ * @return callable The callback to use.
+ */
+ protected function get_create_callback( array $postarr ) {
+ /**
+ * Filters the callback that all repositories should use to create posts.
+ *
+ * @since 4.9.5
+ *
+ * @param callable $callback The callback that should be used to create posts; defaults
+ * to `wp_insert_post`; non numeric and existing post ID return
+ * values will be interpreted as failures to create the post.
+ * @param array $postarr The post array that will be used for the creation.
+ */
+ $callback = apply_filters( 'tribe_repository_create_callback', 'wp_insert_post', $postarr );
+
+ /**
+ * Filters the callback that all repositories should use to create posts.
+ *
+ * @since 4.9.5
+ *
+ * @param callable $callback The callback that should be used to create posts; defaults
+ * to `wp_insert_post`; non numeric and existing post ID return
+ * values will be interpreted as failures to create the post.
+ * @param array $postarr The post array that will be used for the creation.
+ */
+ $callback = apply_filters(
+ "tribe_repository_{$this->filter_name}_create_callback",
+ $callback,
+ $postarr
+ );
+
+ return $callback;
+ }
+
+ /**
+ * Returns the create args the repository will use to create posts.
+ *
+ * @since 4.9.5
+ *
+ * @return array The create args the repository will use to create posts.
+ */
+ public function get_create_args() {
+ return $this->create_args;
+ }
+
+ /**
+ * Sets the create args the repository will use to create posts.
+ *
+ * @since 4.9.5
+ *
+ * @param array $create_args The create args the repository will use to create posts.
+ */
+ public function set_create_args( array $create_args ) {
+ $this->create_args = $create_args;
+ }
+
+ /**
+ * Returns a value trying to fetch it from an array first and then
+ * reading it from the meta.
+ *
+ * @since 4.9.5
+ *
+ * @param array $postarr The array to look into.
+ * @param string $key The key to retrieve.
+ * @param int|null $post_id The post ID to fetch the value for.
+ * @param mixed $default The default value to return if nothing was found.
+ *
+ * @return mixed The found value if any.
+ */
+ protected function get_from_postarr_or_meta( array $postarr, $key, $post_id = null, $default = null ) {
+ $default_value = get_post_meta( $post_id, $key, true );
+ if ( '' === $default_value || null === $post_id ) {
+ $default_value = $default;
+ }
+
+ return Tribe__Utils__Array::get( $postarr['meta_input'], $key, $default_value );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_display_context( $context = 'default' ) {
+ $this->display_context = $context;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_render_context( $context = 'default' ) {
+ $this->render_context = $context;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_query_for_posts( array $posts ) {
+ $posts = array_filter( array_map( 'get_post', $posts ) );
+ $query = new \WP_Query();
+ // Let's make it look like the posts are the result of a query using `post__in`.
+ $query->set( 'post__in', wp_list_pluck( $posts, 'ID' ) );
+ $query->found_posts = count( $posts );
+ $query->posts = $posts;
+ $query->post_count = count( $posts );
+ $query->current_post = - 1;
+
+ return $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pluck( $field ) {
+ $list = new WP_List_Util( $this->all() );
+
+ return $list->pluck( $field );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter( $args = [], $operator = 'AND' ) {
+ $list = new WP_List_Util( $this->all() );
+
+ return $list->filter( $args, $operator );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sort( $orderby = [], $order = 'ASC', $preserve_keys = false ) {
+ $list = new WP_List_Util( $this->all() );
+
+ return $list->sort( $orderby, $order, $preserve_keys );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function collect() {
+ return new Tribe__Utils__Post_Collection( $this->all() );
+ }
+
+ /**
+ * Builds the ORM query with the query builder.
+ *
+ * Allow classes extending or decorating the repository to act before
+ * the query is built or replace its building completely.
+ *
+ * @since 4.9.5
+ *
+ * @return WP_Query|null A built query object or `null` if the builder failed or bailed.
+ */
+ protected function build_query_with_builder() {
+ $built = $this->query_builder->build_query();
+
+ $built->builder = $this->query_builder;
+
+ if ( null !== $built ) {
+ $query = $built;
+ }
+
+ return $query;
+ }
+
+ /**
+ * Builds the ORM query internally, without a query builder.
+ *
+ * @since 4.9.5
+ *
+ * @return WP_Query The built query object.
+ */
+ protected function build_query_internally() {
+ $query = new WP_Query();
+
+ $query->builder = $this;
+
+ $this->filter_query->set_query( $query );
+
+ /**
+ * Here we merge, not recursively, to allow user-set query arguments
+ * to override the default ones.
+ */
+ $query_args = array_merge( $this->default_args, $this->query_args );
+
+ $default_post_status = [ 'publish' ];
+ if ( current_user_can( 'read_private_posts' ) ) {
+ $default_post_status[] = 'private';
+ }
+
+ $query_args['post_status'] = Tribe__Utils__Array::get( $query_args, 'post_status', $default_post_status );
+
+ /**
+ * Filters the query arguments that will be used to fetch the posts.
+ *
+ * @param array $query_args An array of the query arguments the query will be
+ * initialized with.
+ * @param WP_Query $query The query object, the query arguments have not been parsed yet.
+ * @param $this $this This repository instance
+ */
+ $query_args = apply_filters( "tribe_repository_{$this->filter_name}_query_args", $query_args, $query, $this );
+
+ /**
+ * Provides a last-ditch effort to override the filtered offset.
+ *
+ * This should only be used if doing creating pagination for performance purposes.
+ *
+ * @since 4.11.0
+ *
+ * @param null|int $filtered_offset Offset parameter setting.
+ * @param array $query_args List of query arguments.
+ */
+ $filtered_offset = apply_filters( 'tribe_repository_query_arg_offset_override', null, $query_args );
+
+ if ( $filtered_offset || isset( $query_args['offset'] ) ) {
+ $per_page = (int) Tribe__Utils__Array::get( $query_args, 'posts_per_page', get_option( 'posts_per_page' ) );
+
+ if ( $filtered_offset ) {
+ $query_args['offset'] = $filtered_offset;
+ } elseif ( isset( $query_args['offset'] ) ) {
+ $offset = absint( $query_args['offset'] );
+ $page = (int) Tribe__Utils__Array::get( $query_args, 'paged', 1 );
+
+ $real_offset = $per_page === -1 ? $offset : ( $per_page * ( $page - 1 ) ) + $offset;
+ $query_args['offset'] = $real_offset;
+
+ /**
+ * Unset the `offset` query argument to avoid applying it multiple times when this method
+ * is used, on the same repository, more than once.
+ */
+ unset( $this->query_args['offset'] );
+ }
+
+ $query_args['posts_per_page'] = $per_page === -1 ? self::MAX_NUMBER_OF_POSTS_PER_PAGE : $per_page;
+ }
+
+ foreach ( $query_args as $key => $value ) {
+ $query->set( $key, $value );
+ }
+
+ /**
+ * Here process the previously set query modifiers passing them the
+ * query object before it executes.
+ * The query modifiers should modify the query by reference.
+ */
+ foreach ( $this->query_modifiers as $arg ) {
+ if ( is_object( $arg ) && method_exists( $arg, '__invoke' ) ) {
+ // __invoke, assume changes are made by reference
+ $arg( $query );
+ } elseif ( is_callable( $arg ) ) {
+ // assume changes are made by reference
+ $arg( $query );
+ }
+ }
+
+ return $query;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function hash( array $settings = [], WP_Query $query = null ) {
+ return md5( json_encode( $this->get_hash_data( $settings, $query ) ) );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_hash_data( array $settings, WP_Query $query = null ) {
+ $filters = $this->current_filters;
+ $query_vars = null !== $query
+ ? $query->query
+ : array_merge( $this->default_args, $this->query_args );
+
+ if ( isset( $settings['exclude'] ) ) {
+ $filters = array_diff_key(
+ $filters,
+ array_combine( $settings['exclude'], $settings['exclude'] )
+ );
+ $query_vars = array_diff_key(
+ $query_vars,
+ array_combine( $settings['exclude'], $settings['exclude'] )
+ );
+ }
+
+ if ( isset( $settings['include'] ) ) {
+ $filters = array_intersect_key(
+ $filters,
+ array_combine( $settings['include'], $settings['include'] )
+ );
+ $query_vars = array_intersect_key(
+ $query_vars,
+ array_combine( $settings['include'], $settings['include'] )
+ );
+ }
+
+ Tribe__Utils__Array::recursive_ksort( $filters );
+ Tribe__Utils__Array::recursive_ksort( $query_vars );
+
+ return [ 'filters' => $filters, 'query_vars' => $query_vars ];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_last_built_query() {
+ return $this->last_built_query;
+ }
+
+ /**
+ * Checks a SQL relation is valid.
+ *
+ * Allowed values are 'OR' and 'AND'.
+ *
+ * @since 4.9.6
+ *
+ * @param string $relation The relation to check.
+ *
+ * @throws \Tribe__Repository__Usage_Error If the relation is not a valid one.
+ */
+ protected function validate_relation( $relation ) {
+ if ( ! in_array( $relation, [ 'OR', 'AND' ], true ) ) {
+ throw Tribe__Repository__Usage_Error::because_this_relation_is_not_valid( $relation );
+ }
+ }
+
+ /**
+ * Sanitizes and prepares string to be used in a LIKE comparison.
+ *
+ * If no leading and trailing `%` was found it will be added at the start and end of the string.
+ *
+ * @since 4.9.6
+ *
+ * @param string|array $value The string to prepare or an array of strings to prepare.
+ *
+ * @return string|array The sanitized string, or strings.
+ */
+ protected function prepare_like_string( $value ) {
+ $original_value = $value;
+ $values = (array) $value;
+ $prepared = [];
+ $pattern = '/^(?
%{0,1})(?.*?)(?%{0,1})$/u';
+
+ global $wpdb;
+
+ foreach ( $values as $v ) {
+ preg_match( $pattern, $v, $matches );
+ $pre = $matches['pre'] ?: '';
+ $post = $matches['post'] ?: '';
+ $string = $wpdb->esc_like( $matches['string'] );
+
+ if ( '' === $pre && '' === $post ) {
+ // If the string does not contain any starting and ending placeholder we'll add all combinations.
+ $prepared[] = '%' . $string;
+ $prepared[] = $string . '%';
+ $prepared[] = $string;
+ $pre = $post = '%';
+ }
+
+ $prepared[] = $pre . $string . $post;
+ }
+
+ return is_array( $original_value ) ? $prepared : reset( $prepared );
+ }
+
+ /**
+ * Builds the WHERE clause for a set of fields.
+ *
+ * This method is table-agnostic. While flexible it will also require some care to be used.
+ *
+ * @since 4.9.6
+ *
+ * @param string|array $fields One or more fields to build the clause for.
+ * @param string $compare The comparison operator to use to build the
+ * @param string|array $values One or more values to build the WHERE clause for.
+ * @param string $value_format The format, a `$wpdb::prepare()` compatible one, to use to format the values.
+ * @param string $where_relation The relation to apply between each WHERE fragment.
+ * @param string $value_relation The relation to apply between each value fragment.
+ *
+ * @return string The built WHERE clause.
+ *
+ * @throws \Tribe__Repository__Usage_Error If the relations are not valid or another WHERE building issue happens.
+ */
+ protected function build_fields_where_clause(
+ $fields,
+ $compare,
+ $values,
+ $value_format = '%s',
+ $where_relation = 'OR',
+ $value_relation = 'OR'
+ ) {
+ $this->validate_relation( $where_relation );
+ $this->validate_relation( $value_relation );
+ global $wpdb;
+ $fields_where_clauses = [];
+ $fields = (array) $fields;
+ $values = (array) $values;
+ foreach ( $fields as $field ) {
+ $value_clauses = [];
+ foreach ( $values as $compare_value ) {
+ if ( ! is_array( $compare_value ) || count( $compare_value ) === 1 ) {
+ $value_clauses[] = $wpdb->prepare(
+ "({$field} {$compare} {$value_format})",
+ $compare_value
+ );
+ } else {
+ $value_format = implode(
+ ',',
+ array_fill( 0, count( $compare_value ), $value_format )
+ );
+ $value_clauses[] = $wpdb->prepare(
+ "({$field} {$compare} ({$value_format}))",
+ $compare_value
+ );
+ }
+ }
+ $fields_where_clauses[] = '(' . implode( " {$value_relation} ", $value_clauses ) . ')';
+ }
+
+ $fields_where = $wpdb->remove_placeholder_escape(
+ implode( " {$where_relation} ", $fields_where_clauses )
+ );
+
+ return $fields_where;
+ }
+
+ /**
+ * Returns the term IDs of terms matching a criteria, the match is made on the terms slug and name.
+ *
+ * This should be used to break-down a query and fetch term IDs, to then use in a "lighter" join, later.
+ *
+ * @since 4.9.6
+ *
+ * @param string|array $taxonomy The taxonomy, or taxonomies, to fetch the terms for.
+ * @param string $compare The comparison operator to use, e.g. 'LIKE' or '=>'.
+ * @param string|array $value An array of values to compare the terms slug or names with.
+ * @param string $relation The relation, either 'OR' or 'AND', to apply to the matching.
+ * @param string $format The format, a `$wpdb::prepare()` supported one, to use to format the values for the query.
+ *
+ * @return array An array of term IDs matching the query, if any.
+ */
+ protected function fetch_taxonomy_terms_matches( $taxonomy, $compare, $value, $relation = 'OR', $format = '%s' ) {
+ global $wpdb;
+ $taxonomies = (array) $taxonomy;
+ $values = (array) $value;
+
+ $compare_target = count( $values ) > 1
+ ? '(' . $this->filter_query->create_interval_of_strings( $values ) . ')'
+ : $wpdb->prepare( $format, reset( $values ) );
+
+ $taxonomies_interval = $this->filter_query->create_interval_of_strings( $taxonomies );
+
+ $query = "SELECT tt.term_taxonomy_id FROM {$wpdb->terms} AS t
+ INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id
+ WHERE tt.taxonomy IN ({$taxonomies_interval}) AND
+ ( t.slug {$compare} {$compare_target} {$relation} t.name {$compare} {$compare_target} )";
+
+ return $wpdb->get_col( $wpdb->remove_placeholder_escape( $query ) );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function where_multi( array $fields, $compare, $value, $where_relation = 'OR', $value_relation = 'OR' ) {
+ $compare = strtoupper( trim( $compare ) );
+
+ // Check each value is compatible with the comparison operator.
+ $values = (array) $value;
+ foreach ( $values as $v ) {
+ $this->validate_operator_and_values( $compare, 'where_multi', $v );
+ }
+
+ global $wpdb;
+
+ if ( in_array( $compare, [ 'LIKE', 'NOT LIKE' ], true ) ) {
+ $values = $this->prepare_like_string( $values );
+ }
+
+ $where_relation = strtoupper( trim( $where_relation ) );
+ $this->validate_relation( $where_relation );
+ $value_relation = strtoupper( trim( $value_relation ) );
+ $this->validate_relation( $value_relation );
+
+ $post_fields = [];
+ $taxonomies = [];
+
+ foreach ( $fields as $field ) {
+ if ( $this->is_a_post_field( $field ) ) {
+ $post_fields[] = $field;
+ } elseif ( array_key_exists( $field, $this->simple_tax_schema ) ) {
+ // Handle simple tax schema aliases.
+ $schema = $this->simple_tax_schema[ $field ]['taxonomy'];
+
+ if ( ! is_array( $schema ) ) {
+ $taxonomies[] = $schema;
+
+ continue;
+ }
+
+ // If doing an AND where relation, pass all taxonomies in to be grouped with OR.
+ if ( 'AND' === $where_relation ) {
+ $this->where_multi( $schema, $compare, $value, 'OR', $value_relation );
+
+ continue;
+ }
+
+ foreach ( $schema as $taxonomy ) {
+ $taxonomies[] = $taxonomy;
+ }
+ } elseif ( array_key_exists( $field, $this->simple_meta_schema ) ) {
+ // Handle simple meta schema aliases.
+ $schema = $this->simple_meta_schema[ $field ]['meta_key'];
+
+ if ( ! is_array( $schema ) ) {
+ $custom_fields[] = $schema;
+
+ continue;
+ }
+
+ // If doing an AND where relation, pass all meta keys in to be grouped with OR.
+ if ( 'AND' === $where_relation ) {
+ $this->where_multi( $schema, $compare, $value, 'OR', $value_relation );
+
+ continue;
+ }
+
+ foreach ( $schema as $meta_key ) {
+ $custom_fields[] = $meta_key;
+ }
+ } elseif ( $this->is_a_taxonomy( $field ) ) {
+ $taxonomies[] = $field;
+ } else {
+ $custom_fields[] = $field;
+ }
+ }
+
+ $value_formats = [];
+
+ foreach ( $values as $v ) {
+ $value_format = '%d';
+ if ( is_string( $v ) ) {
+ $value_format = '%s';
+ } elseif ( (int) $v !== (float) $v ) {
+ $value_format = '%f';
+ }
+ $value_formats[] = $value_format;
+ }
+
+ // If the value formats differ then treat all of them as strings.
+ if ( count( array_unique( $value_formats ) ) > 1 ) {
+ $value_format = '%s';
+ } else {
+ $value_format = reset( $value_formats );
+ }
+
+ $where = [];
+
+ if ( ! empty( $post_fields ) ) {
+ $post_fields = array_map( static function ( $post_field ) use ( $wpdb ) {
+ return "{$wpdb->posts}.$post_field";
+ }, $post_fields );
+
+ $post_fields_where = $this->build_fields_where_clause(
+ $post_fields,
+ $compare,
+ $values,
+ $value_format,
+ $where_relation,
+ $value_relation
+ );
+
+ $wheres[] = $post_fields_where;
+ }
+
+ if ( ! empty( $taxonomies ) ) {
+ $all_matching_term_ids = [];
+ $taxonomy_values = $values;
+
+ if ( in_array( $compare, [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
+ // We can use multiple values in the same query.
+ $taxonomy_values = [ $values ];
+ }
+
+ foreach ( $taxonomy_values as $taxonomy_value ){
+ $matching_term_ids = $this->fetch_taxonomy_terms_matches(
+ $taxonomies,
+ $compare,
+ $taxonomy_value,
+ $where_relation,
+ $value_format
+ );
+
+ if ( empty( $matching_term_ids ) ) {
+ if ( 'AND' === $value_relation ) {
+ // No reason to waste any more time.
+ $this->void_query = true;
+
+ return $this;
+ }
+
+ continue;
+ }
+
+ $all_matching_term_ids[] = $matching_term_ids;
+ }
+
+ $intersection = count( $all_matching_term_ids ) > 1
+ ? array_intersect( ...$all_matching_term_ids )
+ : reset( $all_matching_term_ids );
+
+ if ( 'AND' === $where_relation && 0 === count( $intersection ) ) {
+ // Let's not waste any more time.
+ $this->void_query = true;
+
+ return $this;
+ }
+
+ $merge = count( $all_matching_term_ids ) > 1
+ ? array_unique( array_merge( ...$all_matching_term_ids ) )
+ : (array) reset( $all_matching_term_ids );
+ $matching_term_ids = $where_relation === 'OR' ? array_filter( $merge ) : array_filter( $intersection );
+
+ if ( 'AND' === $where_relation || ! empty( $matching_term_ids ) ) {
+ // Let's not add WHERE and JOIN clauses if there is nothing to add.
+ $tt_alias = 'tribe_tt_' . self::$alias_counter ++;
+ $this->filter_query->join(
+ "JOIN {$wpdb->term_relationships} {$tt_alias} ON {$wpdb->posts}.ID = {$tt_alias}.object_id"
+ );
+ $matching_term_ids_interval = implode( ',', $matching_term_ids );
+ $wheres[] = "{$tt_alias}.term_taxonomy_id IN ({$matching_term_ids_interval})";
+ }
+ }
+
+ if ( ! empty( $custom_fields ) ) {
+ $meta_alias = 'tribe_meta_' . self::$alias_counter ++;
+
+ $custom_fields = array_map( static function ( $custom_field ) use ( $wpdb, $meta_alias ) {
+ return $wpdb->prepare(
+ "{$meta_alias}.meta_key = %s AND {$meta_alias}.meta_value",
+ $custom_field
+ );
+ }, $custom_fields );
+
+ $meta_where = $this->build_fields_where_clause(
+ $custom_fields,
+ $compare,
+ $values,
+ $value_format,
+ $where_relation,
+ $value_relation
+ );
+
+ $this->filter_query->join(
+ "JOIN {$wpdb->postmeta} {$meta_alias} ON {$wpdb->posts}.ID = {$meta_alias}.post_id"
+ );
+
+ $wheres[] = $meta_where;
+ }
+
+ $this->filter_query->where( implode( " {$where_relation} ", $wheres ) );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function set_query( WP_Query $query ) {
+ if (
+ $this->last_built_query instanceof WP_Query
+ && !empty($this->last_built_query->request)
+ ){
+ throw Tribe__Repository__Usage_Error::because_query_cannot_be_set_after_it_ran();
+ }
+ $this->last_built_query = $query;
+ $this->last_built_hash = $this->hash();
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function set_found_rows( $found_rows ) {
+ $this->skip_found_rows = ! $found_rows;
+
+ return $this;
+ }
+
+ /**
+ * Flush current filters and query information.
+ *
+ * @since 4.9.10
+ *
+ * @return self
+ */
+ public function flush() {
+ $this->current_query = null;
+ $this->current_filters = [];
+ $this->current_filter = null;
+ $this->last_built_query = null;
+ $this->last_built_hash = '';
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function next() {
+ $next = clone $this;
+ $current_page = isset( $this->query_args['paged'] )
+ ? (int) $this->query_args['paged']
+ : 1;
+ $next->page( $current_page + 1 );
+
+ // Let's try to avoid running a query if we already know if a next page will yield any result or not.
+ $query_ran = ! empty( $this->last_built_query ) && ! empty( $this->last_built_query->request );
+ if ( $query_ran && ( false === (bool) $this->last_built_query->get( 'no_found_rows' ) ) ) {
+ $found = $this->last_built_query->found_posts;
+ $posts_per_page = $this->last_built_query->get( 'posts_per_page' );
+ $this_is_last_page = ( $current_page * $posts_per_page ) >= $found;
+ if ( $this_is_last_page ) {
+ $next->void_query = true;
+ }
+ }
+
+ $next->last_built_query = null;
+
+ return $next;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function prev() {
+ $prev = clone $this;
+ $current_page = isset( $this->query_args['paged'] )
+ ? (int) $this->query_args['paged']
+ : 1;
+
+ if ( $current_page === 1 ) {
+ $prev->void_query = true;
+
+ return $prev;
+ }
+
+ // If we're on page 1 we know there will be previous posts.
+ $prev->page( $current_page - 1 );
+ $prev->last_built_query = null;
+
+ return $prev;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function void_query( $void_query = true ) {
+ $this->void_query = (bool) $void_query;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_last_sql(): ?string {
+ return $this->last_built_query instanceof WP_Query ?
+ $this->last_built_query->request
+ : null;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Core_Read_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Core_Read_Interface.php
new file mode 100644
index 000000000..ca777fb70
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Core_Read_Interface.php
@@ -0,0 +1,402 @@
+, ]. * * @return Tribe__Repository__Read_Interface
+ */
+ public function by_args( array $args );
+
+ /**
+ * Applies a filter to the query.
+ *
+ * While the signature only shows 2 arguments additional arguments will be passed
+ * to the schema filters.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param mixed ...$args Additional, optional, call arguments that will be passed to
+ * the schema.
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function by( $key, $value = null );
+
+ /**
+ * Just an alias of the `by` method to allow for easier reading.
+ *
+ * @since 4.7.19
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function where( $key, $value = null );
+
+ /**
+ * Sets the page of posts to fetch.
+ *
+ * Mind that this implementation does not support a `by( 'page', 2 )`
+ * filter to force more readable code.
+ *
+ * @since 4.7.19
+ *
+ * @param int $page
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function page( $page );
+
+ /**
+ * Sets the number of posts to retrieve per page.
+ *
+ * Mind that this implementation does not support a `by( 'per_page', 5 )`
+ * filter to force more readable code; by default posts per page is set to
+ * the pagination defaults for the post type.
+ *
+ * @param int $per_page
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function per_page( $per_page );
+
+ /**
+ * Returns the number of posts found matching the query.
+ *
+ * Mind that this value ignores the offset returning the
+ * number of results if limits where not applied.
+ *
+ * @since 4.7.19
+ *
+ * @return int
+ */
+ public function found();
+
+ /**
+ * Returns all posts matching the query.
+ *
+ * Mind that "all" means "all the posts matching all the filters" so pagination applies.
+ *
+ * @return array
+ */
+ public function all();
+
+ /**
+ * Sets the offset on the query.
+ *
+ * Mind that this implementation does not support a `by( 'offset', 2 )`
+ * filter to force more readable code.
+ *
+ * @since 4.7.19
+ *
+ * @param int $offset
+ * @param bool $increment Whether to increment the offset by the value
+ * or replace it.
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function offset( $offset, $increment = false );
+
+ /**
+ * Sets the order on the query.
+ *
+ * Mind that this implementation does not support a `by( 'order', 2 )`
+ * filter to force more readable code.
+ *
+ * @since 4.7.19
+ *
+ * @param string $order
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function order( $order = 'ASC' );
+
+ /**
+ * Sets the order criteria results should be fetched by.
+ *
+ * Mind that this implementation does not support a `by( 'order_by', 'title' )`
+ * filter to force more readable code.
+ *
+ * @since 4.7.19
+ *
+ * @param string|array $order_by The post field, custom field or alias key to order posts by.
+ * @param string $order The order direction; optional; shortcut for the `order` method; defaults
+ * to `DESC`.
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function order_by( $order_by, $order = 'DESC' );
+
+ /**
+ * Sets the fields that should be returned by the query.
+ *
+ * Mind that this implementation does not support a `by( 'fields', 'ids' )`
+ * filter to force more readable code.
+ *
+ * @since 4.7.19
+ *
+ * @param string $fields
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function fields( $fields );
+
+ /**
+ * Sugar method to set the `post__in` argument.
+ *
+ * Successive calls will stack, not replace each one.
+ *
+ * @since 4.7.19
+ *
+ * @param array|int $post_ids
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function in( $post_ids );
+
+ /**
+ * Sugar method to set the `post__not_in` argument.
+ *
+ * Successive calls will stack, not replace each one.
+ *
+ * @since 4.7.19
+ *
+ * @param array|int $post_ids
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function not_in( $post_ids );
+
+ /**
+ * Sugar method to set the `post_parent__in` argument.
+ *
+ * Successive calls will stack, not replace each one.
+ *
+ * @since 4.7.19
+ *
+ * @param array|int $post_id
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function parent( $post_id );
+
+ /**
+ * Sugar method to set the `post_parent__in` argument.
+ *
+ * Successive calls will stack, not replace each one.
+ *
+ * @since 4.7.19
+ *
+ * @param array $post_ids
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function parent_in( $post_ids );
+
+ /**
+ * Sugar method to set the `post_parent__not_in` argument.
+ *
+ * Successive calls will stack, not replace each one.
+ *
+ * @since 4.7.19
+ *
+ * @param array $post_ids
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function parent_not_in( $post_ids );
+
+ /**
+ * Sugar method to set the `s` argument.
+ *
+ * Successive calls will replace the search string.
+ * This is the default WordPress search, to search by title,
+ * content or excerpt only use the `title`, `content`, `excerpt` filters.
+ *
+ * @param $search
+ *
+ * @return Tribe__Repository__Read_Interface
+ */
+ public function search( $search );
+
+ /**
+ * Returns the number of posts found matching the query in the current page.
+ *
+ * While the `found` method will return the number of posts found
+ * across all pages this method will only return the number of
+ * posts found in the current page.
+ * Differently from the `found` method this method will apply the
+ * offset if set.
+ *
+ * @since 4.7.19
+ *
+ * @return int
+ */
+ public function count();
+
+ /**
+ * Returns the first post of the page matching the current query.
+ *
+ * If, by default or because set with the `per_page` method, all
+ * posts matching the query should be returned then this will be
+ * the first post of all those matching the query.
+ *
+ * @since 4.7.19
+ *
+ * @return WP_Post|mixed|null
+ *
+ * @see Tribe__Repository__Read_Interface::per_page()
+ */
+ public function first();
+
+ /**
+ * Returns the last post of the page matching the current query.
+ *
+ * If, by default or because set with the `per_page` method, all
+ * posts matching the query should be returned then this will be
+ * the last post of all those matching the query.
+ *
+ * @since 4.7.19
+ *
+ * @return WP_Post|mixed|null
+ *
+ * @see Tribe__Repository__Read_Interface::per_page()
+ */
+ public function last();
+
+ /**
+ * Returns the nth post (1-based) of the page matching the current query.
+ *
+ * Being 1-based the second post can be fetched using `nth( 2 )`.
+ * If, by default or because set with the `per_page` method, all
+ * posts matching the query should be returned then this will be
+ * the nth post of all those matching the query.
+ *
+ * @since 4.7.19
+ *
+ * @param int $n
+ *
+ * @return WP_Post|mixed|null
+ *
+ * @see Tribe__Repository__Read_Interface::per_page()
+ */
+ public function nth( $n );
+
+ /**
+ * Returns the first n posts of the page matching the current query.
+ *
+ * If, by default or because set with the `per_page` method, all
+ * posts matching the query should be returned then this method will
+ * return the first n posts of all those matching the query.
+ *
+ * @since 4.7.19
+ *
+ * @return array An array of posts matching the query.
+ *
+ * @see Tribe__Repository__Read_Interface::per_page()
+ */
+ public function take( $n );
+
+ /**
+ * Plucks a field from all results and returns it.
+ *
+ * This method will implicitly build and use a `WP_List_Util` instance on the return
+ * value of a call to the `all` method.
+ *
+ * @since 4.9.5
+ *
+ * @param string $field The field to pluck from each result.
+ *
+ * @return array An array of the plucked results.
+ *
+ * @see \wp_list_pluck()
+ */
+ public function pluck( $field );
+
+ /**
+ * Filters the results according to the specified criteria.
+ *
+ * This method will implicitly build and use a `WP_List_Util` instance on the return
+ * value of a call to the `all` method.
+ *
+ * @since 4.9.5
+ *
+ * @param array $args Optional. An array of key => value arguments to match
+ * against each object. Default empty array.
+ * @param string $operator Optional. The logical operation to perform. 'AND' means
+ * all elements from the array must match. 'OR' means only
+ * one element needs to match. 'NOT' means no elements may
+ * match. Default 'AND'.
+ *
+ * @return array An array of the filtered results.
+ *
+ * @see \wp_list_filter()
+ */
+ public function filter( $args = [], $operator = 'AND' );
+
+ /**
+ * Sorts the results according to the specified criteria.
+ *
+ * This method will implicitly build and use a `WP_List_Util` instance on the return
+ * value of a call to the `all` method.
+ *
+ * @since 4.9.5
+ *
+ * @param string|array $orderby Optional. Either the field name to order by or an array
+ * of multiple orderby fields as $orderby => $order.
+ * @param string $order Optional. Either 'ASC' or 'DESC'. Only used if $orderby
+ * is a string.
+ * @param bool $preserve_keys Optional. Whether to preserve keys. Default false.
+ *
+ * @return array An array of the sorted results.
+ *
+ * @see \wp_list_sort()
+ */
+ public function sort( $orderby = [], $order = 'ASC', $preserve_keys = false );
+
+ /**
+ * Builds a collection on the result of the `all()` method call.
+ *
+ * @since 4.9.5
+ *
+ * @return \Tribe__Utils__Post_Collection
+ */
+ public function collect();
+
+ /**
+ * Gets the ids of the posts matching the query.
+ *
+ * @return array An array containing the post IDs to update.
+ */
+ public function get_ids();
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Decorator.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Decorator.php
new file mode 100644
index 000000000..6947f609d
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Decorator.php
@@ -0,0 +1,704 @@
+decorated->get_default_args();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_default_args( array $default_args ) {
+ return $this->decorated->set_default_args( $default_args );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter_name( $filter_name ) {
+ $this->decorated->filter_name( $filter_name );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_args( array $args ) {
+ $this->decorated->by_args( $args );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by( $key, $value = null ) {
+ $call_args = func_get_args();
+ call_user_func_array( [ $this->decorated, 'by' ], $call_args );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where( $key, $value = null ) {
+ $call_args = func_get_args();
+ call_user_func_array( [ $this->decorated, 'where' ], $call_args );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function page( $page ) {
+ $this->decorated->page( $page );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function per_page( $per_page ) {
+ $this->decorated->per_page( $per_page );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function found() {
+ return $this->decorated->found();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all() {
+ return $this->decorated->all();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offset( $offset, $increment = false ) {
+ $this->decorated->offset( $offset );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order( $order = 'ASC' ) {
+ $this->decorated->order( $order );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order_by( $order_by, $order = 'DESC' ) {
+ $this->decorated->order_by( $order_by, $order );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fields( $fields ) {
+ $this->decorated->fields( $fields );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function permission( $permission ) {
+ $this->decorated->permission( $permission );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function in( $post_ids ) {
+ $this->decorated->in( $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function not_in( $post_ids ) {
+ $this->decorated->not_in( $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parent( $post_id ) {
+ $this->decorated->parent( $post_id );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parent_in( $post_ids ) {
+ $this->decorated->parent_in( $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parent_not_in( $post_ids ) {
+ $this->decorated->parent_not_in( $post_ids );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function search( $search ) {
+ $this->decorated->search( $search );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count() {
+ return $this->decorated->count();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function first() {
+ return $this->decorated->first();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function last() {
+ return $this->decorated->last();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function nth( $n ) {
+ return $this->decorated->nth( $n );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function take( $n ) {
+ return $this->decorated->take( $n );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_primary_key( $primary_key ) {
+ return $this->decorated->by_primary_key( $primary_key );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set( $key, $value ) {
+ $this->decorated->set( $key, $value );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_query() {
+ return $this->decorated->get_query();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_args( array $update_map ) {
+ $this->decorated->set_args( $update_map );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save( $return_promise = true ) {
+ $this->decorated->save( $return_promise );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_formatter( Tribe__Repository__Formatter_Interface $formatter ) {
+ $this->decorated->set_formatter( $formatter );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function join_clause( $join ) {
+ $this->decorated->join_clause( $join );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where_clause( $where ) {
+ $this->decorated->where_clause( $where );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_query_builder( $query_builder ) {
+ $this->decorated->set_query_builder( $query_builder );
+ }
+
+ /**
+ * Sets the repository to be decorated.
+ *
+ * @since 4.7.19
+ *
+ * @param Tribe__Repository__Interface $decorated
+ */
+ protected function set_decorated_repository( Tribe__Repository__Interface $decorated ) {
+ $this->decorated = $decorated;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build_query( $use_query_builder = true ) {
+ return $this->decorated->build_query( $use_query_builder );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where_or( $callbacks ) {
+ $call_args = func_get_args();
+ call_user_func_array( [ $this->decorated, 'where_or' ], $call_args );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_related_to_min( $by_meta_keys, $min, $keys = null, $values = null ) {
+ $this->decorated->by_related_to_min( $by_meta_keys, $min, $keys, $values );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_related_to_max( $by_meta_keys, $max, $keys = null, $values = null ) {
+ $this->decorated->by_related_to_max( $by_meta_keys, $max, $keys, $values );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_related_to_between( $by_meta_keys, $min, $max, $keys = null, $values = null ) {
+ $this->decorated->by_related_to_between( $by_meta_keys, $min, $max, $keys, $values );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function by_not_related_to( $by_meta_keys, $keys = null, $values = null ) {
+ $this->decorated->by_not_related_to( $by_meta_keys, $keys, $values );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has_filter( $key, $value = null ) {
+ return $this->decorated->has_filter( $key, $value );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_current_filter() {
+ return $this->decorated->get_current_filter();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_ids() {
+ return $this->decorated->get_ids();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add_schema_entry( $key, $callback ) {
+ $this->decorated->add_schema_entry( $key, $callback );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function prepare_interval( $values, $format = '%s' ) {
+ return $this->decorated->prepare_interval( $values, $format );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete( $return_promise = false ) {
+ return $this->decorated->delete( $return_promise );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function async_delete( array $to_delete, $return_promise = true ) {
+ return $this->decorated->async_delete( $to_delete, $return_promise );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add_update_field_alias( $alias, $field_name ) {
+ $this->decorated->add_update_field_alias( $alias, $field_name );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function async_update( array $to_update, $return_promise = true ) {
+ return $this->decorated->async_update( $to_update, $return_promise );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_update_fields_aliases() {
+ return $this->decorated->get_update_fields_aliases();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_update_fields_aliases( array $update_fields_aliases ) {
+ $this->decorated->set_update_fields_aliases( $update_fields_aliases );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_filter_name() {
+ return $this->decorated->get_filter_name();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter_postarr_for_update( array $postarr, $post_id ) {
+ return $this->decorated->filter_postarr_for_update( $postarr, $post_id );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build_postarr( $id = null ) {
+ return $this->decorated->build_postarr();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create() {
+ return $this->decorated->create();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter_postarr_for_create( array $postarr ) {
+ return $this->decorated->filter_postarr_for_create( $postarr, $post_id );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_create_args( array $create_args ) {
+ $this->decorated->set_create_args( $create_args );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_create_args() {
+ return $this->decorated->get_create_args();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_display_context( $context = 'default' ) {
+ $this->decorated->set_display_context( $context );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_render_context( $context = 'default' ) {
+ $this->decorated->set_render_context( $context );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_query_for_posts( array $posts ) {
+ return $this->decorated->get_query_for_posts( $posts );
+ }
+
+ /**
+ * Whether the decorator is decorating an instance of a specific repository class or not.
+ *
+ * The check is made recursively for decorators to get to the first repository implementation.
+ *
+ * @since 4.9.5
+ *
+ * @param string $class The class to check for.
+ *
+ * @return bool Whether the decorator is decorating an instance of a specific repository class or not.
+ */
+ public function decorates_an_instance_of( $class ) {
+ return $this->decorated instanceof Tribe__Repository__Decorator
+ ? $this->decorated->decorates_an_instance_of( $class )
+ : $this->decorated instanceof $class;
+ }
+
+ /**
+ * Returns the concrete repository implementation that's "hidden" under the decorator(s).
+ *
+ * @since 4.9.5
+ *
+ * @return \Tribe__Repository__Interface The concrete repository instance.
+ */
+ public function get_decorated_repository() {
+ return $this->decorated instanceof Tribe__Repository__Decorator
+ ? $this->decorated->get_decorated_repository()
+ : $this->decorated;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pluck( $field ) {
+ return $this->decorated->pluck( $field );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter( $orderby = [], $order = 'ASC', $preserve_keys = false ) {
+ return $this->decorated->filter( $orderby, $order, $preserve_keys );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sort( $orderby = [], $order = 'ASC', $preserve_keys = false ) {
+ return $this->decorated->sort( $orderby, $order, $preserve_keys );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function collect() {
+ return $this->decorated->collect();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hash( array $settings = [], WP_Query $query = null ) {
+ return $this->decorated->hash( $settings );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_hash_data( array $settings, WP_Query $query = null ) {
+ return $this->decorated->get_hash_data( $settings, $query );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_last_built_query() {
+ return $this->decorated->last_built_query;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function where_multi( array $fields, $compare, $value, $where_relation = 'OR', $value_relation = 'OR' ) {
+ $this->decorated->where_multi( $fields, $compare, $value, $where_relation, $value_relation );
+
+ return $this;
+ }
+
+ /**
+ * Handle getting additional property from decorated object.
+ *
+ * @since 4.9.6.1
+ *
+ * @param string $name Property name.
+ *
+ * @return mixed
+ */
+ public function __get( $name ) {
+ return $this->decorated->{$name};
+ }
+
+ /**
+ * Handle setting additional property on decorated object.
+ *
+ * @since 4.9.6.1
+ *
+ * @param string $name Property name.
+ * @param mixed $value Property value.
+ */
+ public function __set( $name, $value ) {
+ $this->decorated->{$name} = $value;
+ }
+
+ /**
+ * Check if additional property on decorated object exists.
+ *
+ * @since 4.9.6.1
+ *
+ * @param string $name Property name.
+ *
+ * @return bool
+ */
+ public function __isset( $name ) {
+ return isset( $this->decorated->{$name} );
+ }
+
+ /**
+ * Call methods on decorated object.
+ *
+ * @since 4.9.6.1
+ *
+ * @param string $name Method name.
+ * @param array $arguments Method arguments.
+ *
+ * @return mixed
+ */
+ public function __call( $name, $arguments ) {
+ return call_user_func_array( [ $this->decorated, $name ], $arguments );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function set_query( WP_Query $query ) {
+ $this->decorated->set_query( $query );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function next() {
+ return $this->decorated->next();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function prev() {
+ return $this->decorated->prev();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function set_found_rows( $found_rows ) {
+ $this->decorated->set_found_rows( $found_rows );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function void_query( $void_query = true ) {
+ $this->decorated->void_query( $void_query );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_last_sql(): ?string {
+ return $this->decorated->get_last_sql();
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Filter_Validation.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Filter_Validation.php
new file mode 100644
index 000000000..11573f1e5
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Filter_Validation.php
@@ -0,0 +1,61 @@
+ => [ => ]
+ * ]
+ * ```
+ *
+ * @since 4.10.2
+ *
+ * @param string $filter The name of the filter currently validating.
+ * @param array $call_args The current filter call args, usually `func_get_args()`.
+ *
+ * @throws Usage_Error If there is a definition for the filter and the argument count or nature is not correct.
+ */
+ protected function ensure_args_for_filter( $filter, array $call_args ) {
+ $map = isset( static::$filter_args_map ) ? static::$filter_args_map : false;
+
+ if ( empty( $map ) ) {
+ return;
+ }
+
+ $required_args = Arr::get( $filter, $map, false );
+
+ if ( false === $required_args ) {
+ return;
+ }
+
+ if ( count( $required_args ) !== count( $call_args ) ) {
+ throw Usage_Error::because_filter_requires_args( $filter, array_keys( $required_args ) );
+ }
+
+ $iterator = new \MultipleIterator();
+ $iterator->attachIterator( new \ArrayIterator( array_keys( $required_args ) ) );
+ $iterator->attachIterator( new \ArrayIterator( array_values( $required_args ) ) );
+ $iterator->attachIterator( new \ArrayIterator( $call_args ) );
+
+ foreach ( $required_args as list( $arg_name, $validator, $input ) ) {
+ if ( empty( $validator( $input ) ) ) {
+ throw Usage_Error::because_filter_arg_is_not_valid( $filter, $arg_name );
+ }
+ }
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Formatter_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Formatter_Interface.php
new file mode 100644
index 000000000..4e484a769
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Formatter_Interface.php
@@ -0,0 +1,24 @@
+, <...args>]
+ *
+ * @return $this
+ * @throws Tribe__Repository__Usage_Error If one of the callback methods does
+ * not add any WHERE clause.
+ *
+ * @see Tribe__Repository::where_clause()
+ * @see Tribe__Repository__Query_Filters::where()
+ */
+ public function where_or( $callbacks );
+
+ /**
+ * Filters the query to return posts that have got a number or posts
+ * related to them by meta at least equal to a value.
+ *
+ * @since 4.7.19
+ *
+ * @param string|array $by_meta_keys One or more `meta_keys` relating
+ * another post TO this post type.
+ * @param int $min The minimum number of posts of another type that should
+ * be related to the queries post type(s).
+ * @param string|array $keys One or more meta_keys to check on the post type in relation
+ * with the query post type(s); if the `$values` parameter is
+ * not provided then this will trigger an EXISTS check.
+ * @param string|array $values One or more value the meta_key specified with `$keys` should
+ * match.
+ *
+ * @return $this
+ */
+ public function by_related_to_min( $by_meta_keys, $min, $keys = null, $values = null );
+
+ /**
+ * Filters the query to return posts that have got a number or posts
+ * related to them by meta at most equal to a value.
+ *
+ * @since 4.7.19
+ *
+ * @param string|array $by_meta_keys One or more `meta_keys` relating
+ * another post TO this post type.
+ * be related to the queries post type(s).
+ * @param int $max The maximum number of posts of another type that should
+ * be related to the queries post type(s).
+ * @param string|array $keys One or more meta_keys to check on the post type in relation
+ * with the query post type(s); if the `$values` parameter is
+ * not provided then this will trigger an EXISTS check.
+ * @param string|array $values One or more value the meta_key specified with `$keys` should
+ * match.
+ *
+ * @return $this
+ */
+ public function by_related_to_max( $by_meta_keys, $max, $keys = null, $values = null );
+
+ /**
+ * Filters the query to return posts that have got a number or posts
+ * related to them by meta between two values.
+ *
+ * @since 4.7.19
+ *
+ * @param string|array $by_meta_keys One or more `meta_keys` relating
+ * another post TO this post type.
+ * @param int $min The minimum number of posts of another type that should
+ * be related to the queries post type(s).
+ * @param int $max The maximum number of posts of another type that should
+ * be related to the queries post type(s).
+ *
+ * @param string|array $keys One or more meta_keys to check on the post type in relation
+ * with the query post type(s); if the `$values` parameter is
+ * not provided then this will trigger an EXISTS check.
+ * @param string|array $values One or more value the meta_key specified with `$keys` should
+ * match.
+ *
+ * @return $this
+ */
+ public function by_related_to_between( $by_meta_keys, $min, $max, $keys = null, $values = null );
+
+ /**
+ * Filters the query to return posts that are not related to posts that have a specific meta value.
+ *
+ * @since 5.0.2.1
+ *
+ * @param string|array $by_meta_keys One or more `meta_keys` relating
+ * another post TO this post type.
+ *
+ * @param string|array $keys One or more meta_keys to check on the post type in relation
+ * with the query post type(s); if the `$values` parameter is
+ * not provided then this will trigger an EXISTS check.
+ * @param string|array $values One or more value the meta_key specified with `$keys` should
+ * match.
+ *
+ * @return $this
+ */
+ public function by_not_related_to( $by_meta_keys, $keys = null, $values = null );
+
+ /**
+ * Adds an entry to the repository filter schema.
+ *
+ * @since 4.9.5
+ *
+ * @param string $key The filter key, the one that will be used in `by` and `where`
+ * calls.
+ * @param callable $callback The function that should be called to apply this filter.
+ */
+ public function add_schema_entry( $key, $callback );
+
+ /**
+ * Returns an hash string for this repository instance filters and, optionally, a generated query.
+ *
+ * By default all applied filters, and query vars, will be included but specific filters can
+ * be excluded, or included, from the hash generation.
+ * The possibility to include the query in the hash generation is required as the query vars could
+ * be further modified after the repository filters are applied and the query is built.
+ *
+ * @since 4.9.5
+ *
+ * @param array $settings An array of settings to define how the hash should be produced in the shape
+ * `[ 'exclude' => [ 'ex_1', ... ], 'include' => [ 'inc_1', ... ] ]`. This array
+ * will apply both to the Repository filters and the query vars.
+ * @param WP_Query|null $query An optional query object to include in the hashing.
+ *
+ * @return string The generated hash string.
+ *
+ */
+ public function hash( array $settings = [], WP_Query $query = null );
+
+ /**
+ * Returns the data the repository would use to build the hash.
+ *
+ * @since 4.9.5
+ *
+ * @param array $settings An array of settings to define how the hash should be produced in the shape
+ * `[ 'exclude' => [ 'ex_1', ... ], 'include' => [ 'inc_1', ... ] ]`. This array
+ * will apply both to the Repository filters and the query vars.
+ * @param WP_Query|null $query An optional query object to include in the hashing.
+ *
+ * @return array An array of hash data components.
+ */
+ public function get_hash_data( array $settings, WP_Query $query = null );
+
+ /**
+ * Returns the last built query from the repository instance.
+ *
+ * @since 4.9.6
+ *
+ * @return WP_Query|null The last built query instance if any.
+ */
+ public function get_last_built_query();
+
+ /**
+ * Builds, and adds to the query, a WHERE clause to the query on multiple fields.
+ *
+ * @since 4.9.6
+ *
+ * @param array $fields The fields to add WHERE clauses for. The fields can be post fields, custom fields or
+ * taxonomy terms.
+ * @param string $compare The comparison operator to use, e.g. 'LIKE' or '>'.
+ * @param mixed $value The value, or values, to compare with; the format will be set depending on the type of
+ * each value.
+ * @param string $where_relation The relation to join the WHERE clauses with, either 'OR' or 'AND'; default to 'OR'.
+ * @param string $value_relation The relation to join the value clauses in case the value is an array, either 'OR'
+ * or 'AND'; defaults to 'OR'.
+ *
+ * @return $this This repository instance to allow chain calls.
+ *
+ * @throws \Tribe__Repository__Usage_Error If the comparison operator or the relation are not valid.
+ */
+ public function where_multi( array $fields, $compare, $value, $where_relation = 'OR', $value_relation = 'OR' );
+
+ /**
+ * Sets the query instance the repository will use.
+ *
+ * Setting a query explicitly
+ *
+ * @since 4.9.9
+ *
+ * @param \WP_Query $query An query instance.
+ *
+ * @return \Tribe__Repository__Interface The repository instance, for chaining.
+ * @throws \Tribe__Repository__Usage_Error If trying to set the query after a fetching operation is done.
+ */
+ public function set_query( WP_Query $query );
+
+ /**
+ * Returns a cloned instance of the repository that will yield the next page results.
+ *
+ * Mind that this method will always return a Repository instance, no matter if a next page exists or not.
+ * If a next page does not exist then the instance returned by this method will yield no posts and a count of `0`.
+ *
+ * @since 4.9.11
+ *
+ * @return \Tribe__Repository__Interface The repository instance that will yield the next page results.
+ */
+ public function next();
+
+ /**
+ * Returns a cloned instance of the repository that will yield the previous page results.
+ *
+ * Mind that this method will always return a Repository instance, no matter if a previous page exists or not.
+ * If a previous page does not exist then the instance returned by this method will yield no posts and a count
+ * of `0`.
+ *
+ * @since 4.9.11
+ *
+ * @return \Tribe__Repository__Interface The repository instance that will yield the previous page results.
+ */
+ public function prev();
+
+ /**
+ * Sets the found rows calculation to be enabled for queries.
+ *
+ * @since 4.9.10
+ *
+ * @param bool $found_rows Whether found rows calculation should be enabled.
+ *
+ * @return \Tribe__Repository__Interface The repository instance, for chaining.
+ */
+ public function set_found_rows( $found_rows );
+
+ /**
+ * Voids the repositories queries preventing the repository from running any query.
+ *
+ * @since 4.9.14
+ *
+ * @param bool $void Whether to void the repository queries or not.
+ *
+ * @return Tribe__Repository__Interface $this The repository instance.
+ */
+ public function void_query( $void_query = true );
+
+ /**
+ * Returns the SQL code for the last query built and ran by the repository, if any.
+ *
+ * @since 5.0.1
+ *
+ * @return string|null The SQL code for the last query built and ran by the repository, if any.
+ */
+ public function get_last_sql(): ?string;
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Query_Filters.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Query_Filters.php
new file mode 100644
index 000000000..8f5ff8ad7
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Query_Filters.php
@@ -0,0 +1,1129 @@
+ [
+ 'post_title' => [],
+ 'post_content' => [],
+ 'post_excerpt' => [],
+ ],
+ 'status' => [],
+ 'join' => [],
+ 'where' => [],
+ ];
+
+ /**
+ * An array of the filters that can be set and unset by id.
+ *
+ * @since 4.9.14
+ *
+ * @var array
+ */
+ protected static $identifiable_filters = [ 'fields', 'join', 'where', 'orderby' ];
+
+ /**
+ * @var array
+ */
+ protected $query_vars;
+
+ /**
+ * @var WP_Query
+ */
+ protected $current_query;
+
+ /**
+ * @var int A reasonably large number for the LIMIT clause.
+ */
+ protected $really_large_number = 99999999;
+
+ /**
+ * @var array A list of the filters this class has added.
+ */
+ protected $active_filters = [];
+
+ /**
+ * @var bool
+ */
+ protected $buffer_where_clauses = false;
+
+ /**
+ * @var array
+ */
+ protected $buffered_where_clauses = [];
+
+ /**
+ * Stores the last request run by the current query.
+ *
+ * @var string
+ */
+ protected $last_request;
+
+ /**
+ * Tribe__Repository__Query_Filters constructor.
+ *
+ * @since 4.7.19
+ */
+ public function __construct() {
+ $this->query_vars = self::$initial_query_vars;
+ }
+
+ /**
+ * Builds an "not exists or is not in" media query.
+ *
+ * @since 4.7.19
+ *
+ * @param array|string $meta_keys On what meta_keys the check should be made.
+ * @param int|string|array $values A single value, an array of values or a CSV list of values.
+ * @param string $query_slug
+ *
+ * @return array
+ */
+ public static function meta_not_in( $meta_keys, $values, $query_slug ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
+ $values = Tribe__Utils__Array::list_to_array( $values );
+
+ if ( empty( $meta_keys ) || count( $values ) === 0 ) {
+ return [];
+ }
+
+ $args = [
+ 'meta_query' => [
+ $query_slug => [
+ 'relation' => 'AND',
+ ],
+ ],
+ ];
+
+ foreach ( $meta_keys as $key ) {
+ $args['meta_query'][ $query_slug ][ $key ] = [
+ 'not-exists' => [
+ 'key' => $key,
+ 'compare' => 'NOT EXISTS',
+ ],
+ 'relation' => 'OR',
+ ];
+
+ if ( count( $values ) > 1 ) {
+ $args['meta_query'][ $query_slug ][ $key ]['not-in'] = [
+ 'key' => $key,
+ 'compare' => 'NOT IN',
+ 'value' => $values,
+ ];
+ } else {
+ $args['meta_query'][ $query_slug ][ $key ]['not-equals'] = [
+ 'key' => $key,
+ 'value' => $values[0],
+ 'compare' => '!=',
+ ];
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Builds an "exists and is in" media query.
+ *
+ * @since 4.7.19
+ *
+ * @param array|string $meta_keys On what meta_keys the check should be made.
+ * @param int|string|array $values A single value, an array of values or a CSV list of values.
+ * @param string $query_slug
+ *
+ * @return array
+ */
+ public static function meta_in( $meta_keys, $values, $query_slug ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
+ $values = Tribe__Utils__Array::list_to_array( $values );
+
+ if ( empty( $meta_keys ) || count( $values ) === 0 ) {
+ return [];
+ }
+
+ $args = [
+ 'meta_query' => [
+ $query_slug => [
+ 'relation' => 'OR',
+ ],
+ ],
+ ];
+
+ foreach ( $meta_keys as $meta_key ) {
+ if ( count( $values ) > 1 ) {
+ $args['meta_query'][ $query_slug ][ $meta_key ] = [
+ 'key' => $meta_key,
+ 'compare' => 'IN',
+ 'value' => $values,
+ ];
+ } else {
+ $args['meta_query'][ $query_slug ][ $meta_key ] = [
+ 'key' => $meta_key,
+ 'compare' => '=',
+ 'value' => $values[0],
+ ];
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Builds a meta query to check that at least of the meta key exists.
+ *
+ * @since 4.7.19
+ *
+ * @param array|string $meta_keys
+ * @param string $query_slug
+ *
+ * @return array
+ */
+ public static function meta_exists( $meta_keys, $query_slug ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
+
+ if ( empty( $meta_keys ) ) {
+ return [];
+ }
+
+ $args = [
+ 'meta_query' => [
+ $query_slug => [
+ 'relation' => 'OR',
+ ],
+ ],
+ ];
+
+ foreach ( $meta_keys as $meta_key ) {
+ $args['meta_query'][ $query_slug ][ $meta_key ] = [
+ 'key' => $meta_key,
+ 'compare' => 'EXISTS',
+ ];
+ }
+
+ return $args;
+ }
+
+ /**
+ * Builds a meta query to check that a meta is either equal to a value or
+ * not exists.
+ *
+ * @since 4.7.19
+ *
+ * @param array|string $meta_keys
+ * @param array|string $values
+ * @param string $query_slug
+ *
+ * @return array
+ */
+ public static function meta_in_or_not_exists( $meta_keys, $values, $query_slug ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
+ $values = Tribe__Utils__Array::list_to_array( $values );
+
+ if ( empty( $meta_keys ) || count( $values ) === 0 ) {
+ return [];
+ }
+
+ $args = [
+ 'meta_query' => [
+ $query_slug => [
+ 'relation' => 'AND',
+ ],
+ ],
+ ];
+
+ foreach ( $meta_keys as $meta_key ) {
+ $args['meta_query'][ $query_slug ][ $meta_key ]['does-not-exist'] = [
+ 'key' => $meta_key,
+ 'compare' => 'NOT EXISTS',
+ ];
+ $args['meta_query'][ $query_slug ][ $meta_key ]['relation'] = 'OR';
+ if ( count( $values ) > 1 ) {
+ $args['meta_query'][ $query_slug ][ $meta_key ]['in'] = [
+ 'key' => $meta_key,
+ 'compare' => 'IN',
+ 'value' => $values,
+ ];
+ } else {
+ $args['meta_query'][ $query_slug ][ $meta_key ]['equals'] = [
+ 'key' => $meta_key,
+ 'compare' => '=',
+ 'value' => $values[0],
+ ];
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Builds a meta query to check that a meta is either not equal to a value or
+ * not exists.
+ *
+ * @since 4.7.19
+ *
+ * @param array|string $meta_keys
+ * @param array|string $values
+ * @param string $query_slug
+ *
+ * @return array
+ */
+ public static function meta_not_in_or_not_exists( $meta_keys, $values, $query_slug ) {
+ $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
+ $values = Tribe__Utils__Array::list_to_array( $values );
+
+ if ( empty( $meta_keys ) || count( $values ) === 0 ) {
+ return [];
+ }
+
+ $args = [
+ 'meta_query' => [
+ $query_slug => [
+ 'relation' => 'AND',
+ ],
+ ],
+ ];
+
+ foreach ( $meta_keys as $meta_key ) {
+ $args['meta_query'][ $query_slug ][ $meta_key ]['does-not-exist'] = [
+ 'key' => $meta_key,
+ 'compare' => 'NOT EXISTS',
+ ];
+ $args['meta_query'][ $query_slug ][ $meta_key ]['relation'] = 'OR';
+
+ if ( count( $values ) > 1 ) {
+ $args['meta_query'][ $query_slug ][ $meta_key ]['not-in'] = [
+ 'key' => $meta_key,
+ 'compare' => 'NOT IN',
+ 'value' => $values,
+ ];
+ } else {
+ $args['meta_query'][ $query_slug ][ $meta_key ]['not-equals'] = [
+ 'key' => $meta_key,
+ 'compare' => '!=',
+ 'value' => $values[0],
+ ];
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Filters the WHERE clause of the query to match posts with a field like.
+ *
+ * @since 4.7.19
+ *
+ * @param string $where
+ * @param WP_Query $query
+ *
+ * @return string
+ */
+ public function filter_by_like( $where, WP_Query $query ) {
+ if ( $query !== $this->current_query ) {
+ return $where;
+ }
+
+ if ( empty( $this->query_vars['like'] ) ) {
+ return $where;
+ }
+
+ foreach ( $this->query_vars['like'] as $field => $entries ) {
+ foreach ( $entries as $entry ) {
+ $where .= $this->and_field_like( $field, $entry );
+ }
+ }
+
+ return $where;
+ }
+
+ /**
+ * Builds the escaped WHERE entry to match a field like the entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $field
+ * @param string $entry
+ *
+ * @return string
+ */
+ protected function and_field_like( $field, $entry ) {
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ $like = $wpdb->esc_like( $entry );
+ $variations = [
+ $wpdb->prepare( "{$wpdb->posts}.{$field} LIKE %s ", "{$like}%" ),
+ $wpdb->prepare( "{$wpdb->posts}.{$field} LIKE %s ", "%{$like}%" ),
+ $wpdb->prepare( "{$wpdb->posts}.{$field} LIKE %s ", "%{$like}" ),
+ ];
+
+ return ' AND (' . implode( ' OR ', $variations ) . ')';
+ }
+
+ /**
+ * Filters the found posts value to apply filtering and selections on the PHP
+ * side of things.
+ *
+ * Here we perform, after the query did run, further filtering operations that would
+ * result in more JOIN and/or sub-SELECT clauses being added to the query.
+ *
+ * @since 4.7.19
+ *
+ * @param int $found_posts The number of found posts.
+ * @param WP_Query $query The current query object.
+ *
+ * @return string
+ */
+ public function filter_found_posts( $found_posts, WP_Query $query ) {
+ if ( $query !== $this->current_query ) {
+ return $found_posts;
+ }
+
+ if ( empty( $this->query_vars['found_posts_filters'] ) ) {
+ return $found_posts;
+ }
+
+ $filtered_found_posts = $found_posts;
+ $ids_only = $query->get( 'fields' ) === 'ids';
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ /**
+ * Handles meta-based relations between posts.
+ */
+ foreach ( $this->query_vars['found_posts_filters']['meta_related'] as $info ) {
+ list( $meta_keys, $field, $field_values, $compare ) = $info;
+ $post_ids = $ids_only ? $query->posts : wp_list_pluck( $query->posts, 'ID' );
+ $post_ids_interval = '(' . implode( ',', $post_ids ) . ')';
+ $meta_keys = "('" . implode( "','", array_map( 'esc_sql', $meta_keys ) ) . "')";
+ $field = esc_sql( $field );
+ $field_values = is_array( $field_values )
+ ? "('" . implode( "','", array_map( 'esc_sql', $field_values ) ) . "')"
+ : $wpdb->prepare( '%s', $field_values );
+
+ $relation_query = "
+ SELECT DISTINCT( pm.post_id )
+ FROM {$wpdb->posts} p
+ JOIN {$wpdb->postmeta} pm
+ ON pm.meta_value = p.ID
+ WHERE pm.post_id IN {$post_ids_interval}
+ AND pm.meta_key IN {$meta_keys}
+ AND p.{$field} {$compare} {$field_values}
+ ";
+
+ $matching_ids = $wpdb->get_col( $relation_query );
+
+ if ( empty( $matching_ids ) ) {
+ $query->posts = [];
+ $filtered_found_posts = 0;
+ break;
+ }
+
+ if ( $ids_only ) {
+ $query->posts = array_intersect( $query->posts, $matching_ids );
+ } else {
+ $updated_query_posts = [];
+ foreach ( $query->posts as $this_post ) {
+ if ( in_array( $this_post->ID, $matching_ids ) ) {
+ $updated_query_posts[] = $this_post;
+ }
+ }
+ $query->posts = $updated_query_posts;
+ }
+ $filtered_found_posts = count( $query->posts );
+ }
+
+ $query->post_count = $filtered_found_posts;
+
+ return $filtered_found_posts;
+ }
+
+ /**
+ * Sets the current query object.
+ *
+ * @since 4.7.19
+ *
+ * @param WP_Query $query
+ */
+ public function set_query( WP_Query $query ) {
+ $this->current_query = $query;
+ }
+
+ /**
+ * Sets up `posts_where` filtering to get posts with a title like the value.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ */
+ public function to_get_posts_with_title_like( $value ) {
+ $this->query_vars['like']['post_title'][] = $value;
+
+ if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
+ $this->add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Proxy method to add a filter calling the WordPress `add_filter` function
+ * and keep track of it.
+ *
+ * @since 4.7.19
+ *
+ * @param string $tag
+ * @param callable $function_to_add
+ * @param int $priority
+ * @param int $accepted_args
+ */
+ protected function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
+ $this->active_filters[] = [ $tag, $function_to_add, $priority ];
+ add_filter( $tag, $function_to_add, $priority, $accepted_args );
+ }
+
+ /**
+ * Sets up `posts_where` filtering to get posts with a content like the value.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ */
+ public function to_get_posts_with_content_like( $value ) {
+ $this->query_vars['like']['post_content'][] = $value;
+
+ if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
+ $this->add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Sets up `posts_where` filtering to get posts with an excerpt like the value.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ */
+ public function to_get_posts_with_excerpt_like( $value ) {
+ $this->query_vars['like']['post_excerpt'] = $value;
+
+ if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
+ add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Sets up `posts_where` filtering to get posts with a filtered content like the value.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ */
+ public function to_get_posts_with_filtered_content_like( $value ) {
+ $this->query_vars['like']['post_content_filtered'][] = $value;
+
+ if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
+ add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Sets up `posts_where` filtering to get posts with a guid that equals the value.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ */
+ public function to_get_posts_with_guid_like( $value ) {
+ $this->query_vars['like']['guid'][] = $value;
+
+ if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
+ add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Sets up `posts_where` filtering to get posts with a `to_ping` field equal to the value.
+ *
+ * @since 4.7.19
+ *
+ * @param string $value
+ */
+ public function to_get_posts_to_ping( $value ) {
+ $this->query_vars['to_ping'] = $value;
+
+ if ( ! has_filter( 'posts_where', [ $this, 'filter_by_to_ping' ] ) ) {
+ add_filter( 'posts_where', [ $this, 'filter_by_to_ping' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Filters the WHERE clause of the query to match posts with a specific `to_ping`
+ * entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $where
+ * @param WP_Query $query
+ *
+ * @return string
+ */
+ public function filter_by_to_ping( $where, WP_Query $query ) {
+ return $this->where_field_is( $where, $query, 'ping_status' );
+ }
+
+ /**
+ * Builds the escaped WHERE entry to match a field that equals the entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $where
+ * @param WP_Query $query
+ * @param string $field
+ * @param string $prepare
+ *
+ * @return string
+ */
+ protected function where_field_is( $where, WP_Query $query, $field, $prepare = '%s' ) {
+ if ( $query !== $this->current_query ) {
+ return $where;
+ }
+
+
+ if ( empty( $this->query_vars[ $field ] ) ) {
+ return $where;
+ }
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ $where .= $wpdb->prepare( " AND {$wpdb->posts}.{$field} = {$prepare} ", $this->query_vars[ $field ] );
+
+ return $where;
+ }
+
+ /**
+ * Removes all the filters this class applied.
+ *
+ * @since 4.7.19
+ */
+ public function remove_filters() {
+ foreach ( $this->active_filters as $filters ) {
+ list( $tag, $function_to_add, $priority ) = $filters;
+ remove_filter( $tag, $function_to_add, $priority );
+ }
+ }
+
+ /**
+ * Add a custom WHERE clause to the query.
+ *
+ * @since 4.7.19
+ * @since 4.9.14 Added the `$id` and `$override` parameters.
+ *
+ * @param string $where_clause
+ * @param null|string $id Optional WHERE ID to prevent duplicating clauses.
+ * @param boolean $override Whether to override the clause if a WHERE by the same ID exists or not.
+ */
+ public function where( $where_clause, $id = null, $override =false ) {
+ if ( $this->buffer_where_clauses ) {
+ if ( $id ) {
+ if ( $override || ! isset( $this->buffered_where_clauses[ $id ] ) ) {
+ $this->buffered_where_clauses[ $id ] = $where_clause;
+ }
+ } else {
+ $this->buffered_where_clauses[] = '(' . $where_clause . ')';
+ }
+ } else {
+ if ( $id ) {
+ if ( $override || ! isset( $this->query_vars['where'][ $id ] ) ) {
+ $this->query_vars['where'][ $id ] = '(' . $where_clause . ')';
+ }
+ } else {
+ $this->query_vars['where'][] = '(' . $where_clause . ')';
+ }
+
+ if ( ! has_filter( 'posts_where', [ $this, 'filter_posts_where' ] ) ) {
+ add_filter( 'posts_where', [ $this, 'filter_posts_where' ], 10, 2 );
+ }
+ }
+ }
+
+ /**
+ * Add a custom JOIN clause to the query.
+ *
+ * @since 4.7.19
+ *
+ * @param string $join_clause JOIN clause.
+ * @param null|string $id Optional JOIN ID to prevent duplicating joins.
+ * @param boolean $override Whether to override the clause if a JOIN by the same ID exists.
+ */
+ public function join( $join_clause, $id = null, $override = false ) {
+ if ( $id ) {
+ if ( $override || ! isset( $this->query_vars['join'][ $id ] ) ) {
+ $this->query_vars['join'][ $id ] = $join_clause;
+ }
+ } else {
+ $this->query_vars['join'][] = $join_clause;
+ }
+
+ if ( ! has_filter( 'posts_join', [ $this, 'filter_posts_join' ] ) ) {
+ add_filter( 'posts_join', [ $this, 'filter_posts_join' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Add a custom ORDER BY to the query.
+ *
+ * @since 4.9.5
+ * @since 4.9.14 Added the `$id` and `$override` parameters.
+ * @since 4.9.21 Added the `$order` and `$after` parameters.
+ *
+ * @param string|array $orderby The order by criteria; this argument can be specified in array form to specify
+ * multiple order by clauses and orders associated to each,
+ * e.g. `[ '_meta_1' => 'ASC', '_meta_2' => 'DESC' ]`. If a simple array is
+ * passed, then the order will be set to the default one for each entry.
+ * This arguments supports the same formats of the `WP_Query` `orderby` argument.
+ * @param null|string $id Optional ORDER ID to prevent duplicating order-by clauses.
+ * @param boolean $override Whether to override the clause if another by the same ID exists.
+ * @param bool $after Whether to append the order by clause to the ones managed by WordPress or not.
+ * Defaults to `false`,to prepend them to the ones managed by WordPress.
+ */
+ public function orderby( $orderby, $id = null, $override = false, $after = false ) {
+ $orderby_key = $after ? static::AFTER . 'orderby' : 'orderby';
+ $entries = [];
+
+ foreach ( (array) $orderby as $key => $value ) {
+ /*
+ * As WordPress does, we support "simple" entries, like `[ 'menu_order', 'post_date' ]` and entries in the
+ * shape `[ 'menu_order' => 'ASC', 'post_date' => 'DESC' ]`.
+ */
+ $the_orderby = is_numeric( $key ) ? $value : $key;
+ $the_order = is_numeric( $key ) ? 'DESC' : $value;
+
+ $entries[] = [ $the_orderby, $the_order ];
+ }
+
+ $id = $id ?: 'default';
+
+ // Use the `$id` parameter to allow later method calls to replace values set in previous calls.
+ if ( $id ) {
+ if ( $override || ! isset( $this->query_vars[ $orderby_key ][ $id ] ) ) {
+ $this->query_vars[ $orderby_key ][ $id ] = $entries;
+ }
+ } else {
+ $this->query_vars[ $orderby_key ][ $id ] = array_merge( $this->query_vars[ $orderby_key ][ $id ], $entries );
+ }
+
+ if ( ! has_filter( 'posts_orderby', [ $this, 'filter_posts_orderby' ] ) ) {
+ add_filter( 'posts_orderby', [ $this, 'filter_posts_orderby' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Add custom select fields to the query.
+ *
+ * @since 4.9.5
+ * @since 4.9.14 Added the `$id` and `$override` parameters.
+ *
+ * @param string $field The field to add to the result.
+ * @param null|string $id Optional ORDER ID to prevent duplicating order-by clauses..
+ * @param boolean $override Whether to override the clause if another by the same ID exists.
+ */
+ public function fields( $field, $id = null, $override = false ) {
+ if ( $id ) {
+ if ( $override || ! isset( $this->query_vars['fields'][ $id ] ) ) {
+ $this->query_vars['fields'][ $id ] = $field;
+ }
+ } else {
+ $this->query_vars['fields'][] = $field;
+ }
+
+ if ( ! has_filter( 'posts_fields', [ $this, 'filter_posts_fields' ] ) ) {
+ add_filter( 'posts_fields', [ $this, 'filter_posts_fields' ], 10, 2 );
+ }
+ }
+
+ /**
+ * Whether WHERE clauses should be buffered or not.
+ *
+ * @since 4.7.19
+ *
+ * @param bool $buffer_clauses
+ */
+ public function buffer_where_clauses( $buffer_clauses ) {
+ $this->buffer_where_clauses = (bool) $buffer_clauses;
+ }
+
+ /**
+ * Returns the buffered WHERE clause and, optionally, cleans
+ * and deactivates buffering.
+ *
+ * @since 4.7.19
+ *
+ * @param bool $get_clean Whether to clean the buffered WHERE
+ * clauses and deactivate buffering before
+ * returning them or not.
+ *
+ * @return array
+ */
+ public function get_buffered_where_clauses( $get_clean = false ) {
+ $clauses = $this->buffered_where_clauses;
+
+ if ( $get_clean ) {
+ $this->buffer_where_clauses = false;
+ $this->buffered_where_clauses = [];
+ }
+
+ return $clauses;
+ }
+
+ /**
+ * Builds the escaped WHERE entry to match a field not in the entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $where
+ * @param WP_Query $query
+ * @param string $field
+ *
+ * @return string
+ */
+ protected function where_field_not_in( $where, WP_Query $query, $field ) {
+ if ( $query !== $this->current_query ) {
+ return $where;
+ }
+
+ if ( empty( $this->query_vars[ $field ] ) ) {
+ return $where;
+ }
+
+ $input = $this->query_vars[ $field ];
+
+ $stati_interval = $this->create_interval_of_strings( $input );
+
+ $where .= $this->and_field_not_in_interval( $field, $stati_interval );
+
+ return $where;
+ }
+
+ /**
+ * Creates a SQL interval of strings.
+ *
+ * @since 4.7.19
+ *
+ * @param string|array $input
+ *
+ * @return string
+ */
+ public function create_interval_of_strings( $input ) {
+ $buffer = [];
+
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ foreach ( $input as $string ) {
+ $buffer[] = is_array( $string ) ? $string : [ $string ];
+ }
+
+ $buffer = array_unique( call_user_func_array( 'array_merge', $buffer ) );
+
+ $safe_strings = [];
+ foreach ( $buffer as $raw_status ) {
+ $safe_strings[] = $wpdb->prepare( '%s', $raw_status );
+ }
+
+ return implode( ',', $safe_strings );
+ }
+
+ /**
+ * Builds a WHERE clause where field is not in interval.
+ *
+ * @since 4.7.19
+ *
+ * @param string $field
+ * @param string $interval
+ *
+ * @return string
+ */
+ protected function and_field_not_in_interval( $field, $interval ) {
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ return " AND {$wpdb->posts}.{$field} NOT IN ('{$interval}') ";
+ }
+
+ /**
+ * Builds the escaped WHERE entry to match a field in the entry.
+ *
+ * @since 4.7.19
+ *
+ * @param string $where
+ * @param WP_Query $query
+ * @param string $field
+ *
+ * @return string
+ */
+ protected function where_field_in( $where, WP_Query $query, $field ) {
+ if ( $query !== $this->current_query ) {
+ return $where;
+ }
+
+ if ( empty( $this->query_vars[ $field ] ) ) {
+ return $where;
+ }
+
+ $interval = $this->create_interval_of_strings( $this->query_vars[ $field ] );
+
+ $where .= $this->and_field_in_interval( $field, $interval );
+
+ return $where;
+ }
+
+ /**
+ * Builds a AND WHERE clause.
+ *
+ * @since 4.7.19
+ *
+ * @param string $field
+ * @param string $interval
+ *
+ * @return string
+ */
+ protected function and_field_in_interval( $field, $interval ) {
+ /** @var wpdb $wpdb */
+ global $wpdb;
+
+ return " AND {$wpdb->posts}.{$field} IN ('{$interval}') ";
+ }
+
+ /**
+ * Filter the `posts_where` filter to add custom WHERE clauses.
+ *
+ * @since 4.7.19
+ *
+ * @param string $where
+ * @param WP_Query $query
+ *
+ * @return string
+ */
+ public function filter_posts_where( $where, WP_Query $query ) {
+ if ( $query !== $this->current_query ) {
+ return $where;
+ }
+
+ if ( empty( $this->query_vars['where'] ) ) {
+ return $where;
+ }
+
+ $where .= ' AND ' . implode( "\nAND ", $this->query_vars['where'] ) . ' ';
+
+ return $where;
+ }
+
+ /**
+ * Filter the `posts_join` filter to add custom JOIN clauses.
+ *
+ * @since 4.7.19
+ *
+ * @param string $join
+ * @param WP_Query $query
+ *
+ * @return string
+ */
+ public function filter_posts_join( $join, WP_Query $query ) {
+ if ( $query !== $this->current_query ) {
+ return $join;
+ }
+
+ if ( empty( $this->query_vars['join'] ) ) {
+ return $join;
+ }
+
+ $join .= "\n" . implode( "\n ", $this->query_vars['join'] ) . ' ';
+
+ return $join;
+ }
+
+ /**
+ * Filter the `posts_orderby` filter to add custom JOIN clauses.
+ *
+ * @since 4.9.5
+ *
+ * @param string $orderby The `ORDER BY` clause of the query being filtered.
+ * @param WP_Query $query The query object currently being filtered.
+ *
+ * @return string The filtered `ORDER BY` clause.
+ */
+ public function filter_posts_orderby( $orderby, WP_Query $query ) {
+ if ( $query !== $this->current_query ) {
+ return $orderby;
+ }
+
+ if ( empty( $this->query_vars['orderby'] ) && empty( $this->query_vars[ static::AFTER . 'orderby' ] ) ) {
+ return $orderby;
+ }
+
+ $frags = [ $orderby ];
+
+ /*
+ * Entries will be set, from the `orderby` method, to the `[ [ , ], [ , ] ]`
+ * format.
+ */
+ $build_entry = static function ( $entries ) {
+ $buffer = [];
+
+ foreach ( $entries as list( $orderby, $order ) ) {
+ $buffer[] = sprintf( '%s %s', $orderby, $order );
+ }
+
+ return implode( ', ', $buffer );
+ };
+
+ if ( ! empty( $this->query_vars['orderby'] ) ) {
+ $before = implode( ', ', array_map( $build_entry, $this->query_vars['orderby'] ) );
+ $frags = [ $before, $orderby ];
+ }
+
+ if ( ! empty( $this->query_vars[ static::AFTER . 'orderby' ] ) ) {
+ $frags[] = implode( ', ', array_map( $build_entry, $this->query_vars[ static::AFTER . 'orderby' ] ) );
+ }
+
+ return implode( ', ', array_filter( $frags ) );
+ }
+
+ /**
+ * Filter the `posts_fields` filter to amend fields to be selected.
+ *
+ * @since 4.9.5
+ *
+ * @param array $fields
+ * @param WP_Query $query
+ *
+ * @return string
+ */
+ public function filter_posts_fields( $fields, WP_Query $query ) {
+ if ( $query !== $this->current_query ) {
+ return $fields;
+ }
+
+ if ( empty( $this->query_vars['fields'] ) ) {
+ return $fields;
+ }
+
+ $fields .= ', ' . implode( ', ', $this->query_vars['fields'] );
+
+ return $fields;
+ }
+
+ /**
+ * Captures the request SQL as built from the query class.
+ *
+ * This happens on the `posts_pre_query` filter and
+ *
+ * @since 4.9.5
+ *
+ * @param null|array $posts A pre-filled array of post results.
+ * @param \WP_Query $query The current query object; this is used by the
+ * method to intercept only the request generated by
+ * its attached query.
+ *
+ * @return array|null An empty array to short-circuit the `get_posts` request; the input
+ * value, if the query is not the one attached to this filter or the method
+ * is called not in the context of the `posts_pre_query` filter;
+ */
+ public function capture_request( $posts = null, WP_Query $query = null ) {
+ if ( ! doing_filter( 'posts_pre_query' ) ) {
+ // Let's make sure nothing bad happens if this runs outside of its natural context.
+ return null;
+ }
+
+ if ( $query !== $this->current_query ) {
+ return $posts;
+ }
+
+ $this->last_request = $query->request;
+
+ remove_filter( 'posts_pre_query', [ $this, 'capture_request' ] );
+
+ // This will short-circuit the query not running it.
+ return [];
+ }
+
+ /**
+ * Returns the controlled query request SQL.
+ *
+ * It's not possible to build the SQL for a query outside of a request to `get_posts`
+ * so what this class does is fire such a request intercepting it before it actually
+ * runs and returning an empty post array.
+ * To really run the query it's sufficien to run `get_posts` again on it.
+ *
+ * @since 4.9.5
+ *
+ * @return string The request SQL, as built from the `WP_Query` class including all the
+ * possible filtering applied by this class and other classes.
+ */
+ public function get_request() {
+ add_filter( 'posts_pre_query', [ $this, 'capture_request' ], 10, 2 );
+
+ $this->current_query->get_posts();
+
+ return $this->last_request;
+ }
+
+ /**
+ * Returns the fields, join, where and orderby clauses for an id.
+ *
+ * @since 4.9.14
+ *
+ * @param string $id The identifier of the group to remove.
+ *
+ * @return array An associative array of identifiable filters and their values, if any.
+ *
+ * @see Tribe__Repository__Query_Filters::$identifiable_filters
+ */
+ public function get_filters_by_id( $id ) {
+ $entries = [];
+
+ foreach ( static::$identifiable_filters as $key ) {
+ if ( empty( $this->query_vars[ $key ][ $id ] ) ) {
+ continue;
+ }
+ $entries[ $key ] = $this->query_vars[ $key ][ $id ];
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Removes fields, join, where and orderby clauses for an id.
+ *
+ * @since 4.9.14
+ *
+ * @param string $id The identifier of the group to remove.
+ */
+ public function remove_filters_by_id( $id ) {
+ array_walk(
+ $this->query_vars,
+ static function ( array &$filters, $key ) use ( $id ) {
+ if ( ! in_array( $key, static::$identifiable_filters, true ) ) {
+ return;
+ }
+ unset( $filters[ $id ] );
+ }
+ );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Read_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Read_Interface.php
new file mode 100644
index 000000000..57dc581cd
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Read_Interface.php
@@ -0,0 +1,173 @@
+where( 'post__in' , $ids )->get_query()` is
+ * to avoid all the overhead of a query that, probably did run already.
+ *
+ * @since 4.9.5
+ *
+ * @param array $posts An array of post objects or post IDs the query should return as if fetched.
+ *
+ * @return WP_Query A query object ready to return, and operate, on the posts.
+ */
+ public function get_query_for_posts( array $posts );
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Setter_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Setter_Interface.php
new file mode 100644
index 000000000..c57ca92d0
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Setter_Interface.php
@@ -0,0 +1,46 @@
+`.
+ *
+ * @since 4.11.5
+ *
+ * @var string
+ */
+ public static $localized_matcher_delimiter = '~';
+
+ /**
+ * WP_Rewrite Instance
+ *
+ * @var WP_Rewrite
+ */
+ public $rewrite;
+
+ /**
+ * Rewrite rules Holder
+ *
+ * @var array
+ */
+ public $rules = [];
+
+ /**
+ * Base slugs for rewrite urls
+ *
+ * @var array
+ */
+ public $bases = [];
+
+ /**
+ * After creating the Hooks on WordPress we lock the usage of the function.
+ *
+ * @var boolean
+ */
+ protected $hook_lock = false;
+
+ /**
+ * An array cache of resolved canonical URLs in the shape `[ => ]`.
+ *
+ * @since 4.9.11
+ *
+ * @var array
+ */
+ protected $canonical_url_cache = null;
+
+ /**
+ * An array cache of parsed URLs in the shape `[ => ]`.
+ *
+ * @since 4.9.11
+ *
+ * @var array
+ */
+ protected $parse_request_cache = null;
+
+ /**
+ * And array cache of cleaned URLs.
+ *
+ * @since 4.9.11
+ *
+ * @var array
+ */
+ protected $clean_url_cache = null;
+ /**
+ * A reference to the Locale Switcher instance.
+ *
+ * @since 5.0.8
+ *
+ * @var Translations_Loader
+ */
+ protected $translations_loader;
+
+ /**
+ * Static Singleton Factory Method
+ *
+ * @return self
+ */
+ public static function instance() {
+ if ( ! static::$instance ) {
+ static::$instance = new static;
+ }
+
+ return static::$instance;
+ }
+
+ /**
+ * Tribe__Rewrite constructor.
+ *
+ * @param WP_Rewrite|null $wp_rewrite An instance of the `WP_Rewrite` class.
+ * @param Translations_Loader|null $translations_loader An instance of the translations loader.
+ */
+ public function __construct( WP_Rewrite $wp_rewrite = null, Translations_Loader $translations_loader = null ) {
+ $this->rewrite = $wp_rewrite;
+ $this->translations_loader = $translations_loader ?? tribe( Translations_Loader::class );
+ }
+
+ /**
+ * When you are going to use any of the functions to create new rewrite rules you need to setup first
+ *
+ * @param WP_Rewrite|null $wp_rewrite Pass the WP_Rewrite if you have it
+ *
+ * @return Tribe__Rewrite The modified version of the class with the required variables in place
+ */
+ public function setup( $wp_rewrite = null ) {
+ if ( ! $wp_rewrite instanceof WP_Rewrite ) {
+ global $wp_rewrite;
+ }
+
+ $this->rewrite = $wp_rewrite;
+ $this->bases = $this->get_bases( 'regex' );
+
+ return $this;
+ }
+
+ /**
+ * Generate the Rewrite Rules
+ *
+ * @param WP_Rewrite $wp_rewrite WordPress Rewrite that will be modified, pass it by reference (&$wp_rewrite)
+ */
+ public function filter_generate( WP_Rewrite $wp_rewrite ) {
+ // Gets the rewrite bases and completes any other required setup work
+ $this->setup( $wp_rewrite );
+
+ /**
+ * Use this to change the Tribe__Rewrite instance before new rules
+ * are committed.
+ *
+ * Should be used when you want to add more rewrite rules without having to
+ * deal with the array merge, noting that rules for The Events Calendar are
+ * themselves added via this hook (default priority).
+ *
+ * @var Tribe__Rewrite $rewrite
+ */
+ do_action( 'tribe_pre_rewrite', $this );
+ }
+
+
+ /**
+ * Do not allow people to Hook methods twice by mistake
+ */
+ public function hooks( $remove = false ) {
+ if ( false === $this->hook_lock ) {
+ // Don't allow people do Double the hooks
+ $this->hook_lock = true;
+
+ $this->add_hooks();
+ } elseif ( true === $remove ) {
+ $this->remove_hooks();
+ }
+ }
+
+ /**
+ * Converts any percentage placeholders in the array keys back to % symbols.
+ *
+ * @param array $rules
+ *
+ * @return array
+ */
+ public function remove_percent_placeholders( array $rules ) {
+ foreach ( $rules as $key => $value ) {
+ $this->replace_array_key( $rules, $key, str_replace( self::PERCENT_PLACEHOLDER, '%', $key ) );
+ }
+
+ return $rules;
+ }
+
+ /**
+ * Filter for the `rewrite_rules_array` hook.
+ *
+ * @since 5.0.10
+ *
+ * @param array|mixed $rules The rules to be filtered.
+ *
+ * @return array|mixed Rules after filtering.
+ */
+ public function filter_rewrite_rules_array( $rules ) {
+ if ( ! is_array( $rules ) ) {
+ return $rules;
+ }
+
+ return $this->remove_percent_placeholders( $rules );
+ }
+
+ protected function add_hooks() {
+ add_filter( 'generate_rewrite_rules', [ $this, 'filter_generate' ] );
+
+ // Remove percent Placeholders on all items
+ add_filter( 'rewrite_rules_array', [ $this, 'filter_rewrite_rules_array' ], 25 );
+
+ add_action( 'shutdown', [ $this, 'dump_cache' ] );
+ }
+
+ protected function remove_hooks() {
+ remove_filter( 'generate_rewrite_rules', [ $this, 'filter_generate' ] );
+ remove_filter( 'rewrite_rules_array', [ $this, 'filter_rewrite_rules_array' ], 25 );
+
+ remove_action( 'shutdown', [ $this, 'dump_cache' ] );
+ }
+
+ /**
+ * Determines if we have plain permalink.
+ *
+ * @since 4.11.2
+ *
+ * @return bool If we use plain permalink or not.
+ */
+ public static function is_plain_permalink() {
+ return tribe_context()->is( 'plain_permalink' );
+ }
+
+ /**
+ * Get the base slugs for the rewrite rules.
+ *
+ * WARNING: Don't mess with the filters below if you don't know what you are doing
+ *
+ * @param string $method Use "regex" to return a Regular Expression with the possible Base Slugs using l10n
+ *
+ * @return object Return Base Slugs with l10n variations
+ */
+ public function get_bases( $method = 'regex' ) {
+ return new stdClass();
+ }
+
+ /**
+ * The base method for creating a new Rewrite rule
+ *
+ * @param array|string $regex The regular expression to catch the URL
+ * @param array $args The arguments in which the regular expression "alias" to
+ *
+ * @return Tribe__Events__Rewrite
+ */
+ public function add( $regex, $args = [] ) {
+ $regex = (array) $regex;
+
+ $default = [];
+ $args = array_filter( wp_parse_args( $args, $default ) );
+
+ $url = add_query_arg( $args, 'index.php' );
+
+ // Optional Trailing Slash
+ $regex[] = '?$';
+
+ // Glue the pieces with slashes
+ $regex = implode( '/', array_filter( $regex ) );
+
+ // Add the Bases to the regex
+ foreach ( $this->bases as $key => $value ) {
+ $regex = str_replace( [ '{{ ' . $key . ' }}', '{{' . $key . '}}' ], $value, $regex );
+ }
+
+ // Apply the Preg Indexes to the URL
+ preg_match_all( '/%([0-9])/', $url, $matches );
+ foreach ( end( $matches ) as $index ) {
+ $url = str_replace( '%' . $index, $this->rewrite->preg_index( $index ), $url );
+ }
+
+ // Add the rule
+ $this->rules[ $regex ] = $url;
+
+ return $this;
+ }
+
+ /**
+ * Returns a sanitized version of $slug that can be used in rewrite rules.
+ *
+ * This is ideal for those times where we wish to support internationalized
+ * URLs (ie, where "venue" in "venue/some-slug" may be rendered in non-ascii
+ * characters).
+ *
+ * In the case of registering new post types, $permastruct_name should
+ * generally match the CPT name itself.
+ *
+ * @param string $slug
+ * @param string $permastruct_name
+ * @param string $is_regular_exp
+ *
+ * @return string
+ */
+ public function prepare_slug( $slug, $permastruct_name, $is_regular_exp = true ) {
+ $needs_handling = false;
+ $sanitized_slug = sanitize_title( $slug );
+
+ // Was UTF8 encoding required for the slug? %a0 type entities are a tell-tale of this
+ if ( preg_match( '/(%[0-9a-f]{2})+/', $sanitized_slug ) ) {
+ /**
+ * Controls whether special UTF8 URL handling is setup for the set of
+ * rules described by $permastruct_name.
+ *
+ * This only fires if Tribe__Events__Rewrite::prepare_slug() believes
+ * handling is required.
+ *
+ * @var string $permastruct_name
+ * @var string $slug
+ */
+ $needs_handling = apply_filters(
+ 'tribe_events_rewrite_utf8_handling', true, $permastruct_name, $slug
+ );
+ }
+
+ if ( $needs_handling ) {
+ // User agents encode things the same way but in uppercase
+ $sanitized_slug = strtoupper( $sanitized_slug );
+
+ // UTF8 encoding results in lots of "%" chars in our string which play havoc
+ // with WP_Rewrite::generate_rewrite_rules(), so we swap them out temporarily
+ $sanitized_slug = str_replace( '%', Tribe__Rewrite::PERCENT_PLACEHOLDER, $sanitized_slug );
+ }
+
+ $prepared_slug = $is_regular_exp ? preg_quote( $sanitized_slug ) : $sanitized_slug;
+
+ /**
+ * Provides an opportunity to modify the sanitized slug which will be used
+ * in rewrite rules relating to $permastruct_name.
+ *
+ * @var string $prepared_slug
+ * @var string $permastruct_name
+ * @var string $original_slug
+ */
+ return apply_filters( 'tribe_rewrite_prepared_slug', $prepared_slug, $permastruct_name, $slug );
+ }
+
+ /**
+ * A way to replace an Array key without destroying the array ordering
+ *
+ * @since 4.0.6
+ *
+ * @param array &$array The Rules Array should be used here
+ * @param string $search Search for this Key
+ * @param string $replace Replace with this key]
+ *
+ * @return bool Did we replace anything?
+ */
+ protected function replace_array_key( &$array, $search, $replace ) {
+ $keys = array_keys( $array );
+ $index = array_search( $search, $keys );
+
+ if ( false !== $index ) {
+ $keys[ $index ] = $replace;
+ $array = array_combine( $keys, $array );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the canonical URLs associated with a ugly link.
+ *
+ * This method will handle "our" URLs to go from their ugly form, filled with query vars, to the "pretty" one, if
+ * possible.
+ *
+ * @since 4.9.11
+ *
+ * @param string $url The URL to try and translate into its canonical form.
+ * @param bool $force Whether to try and use the cache or force a new canonical URL conversion.
+ *
+ * @return string The canonical URL, or the input URL if it could not be resolved to a canonical one.
+ */
+ public function get_canonical_url( $url, $force = false ) {
+ if ( get_class( $this ) === Tribe__Rewrite::class ) {
+ throw new BadMethodCallException(
+ 'Method get_canonical_url should only be called on extending classes.'
+ );
+ }
+
+ if ( null === $this->rewrite ) {
+ // We re-do this check here as the object might have been initialized before the global rewrite was set.
+ $this->setup();
+ }
+
+ /**
+ * Filters the canonical URL for an input URL before any kind of logic runs.
+ *
+ * @since 4.9.11
+ *
+ * @param string|null $canonical_url The canonical URL, defaults to `null`; returning a non `null` value will
+ * make the logic bail and return the value.
+ * @param string $url The input URL to resolve to a canonical one.
+ * @param Tribe__Rewrite $this This rewrite object.
+ */
+ $canonical_url = apply_filters( 'tribe_rewrite_pre_canonical_url', null, $url );
+ if ( null !== $canonical_url ) {
+ return $canonical_url;
+ }
+
+ $home_url = home_url();
+
+ // It's not a path we, or WP, could possibly handle.
+ $has_http_scheme = (bool) parse_url( $url, PHP_URL_SCHEME );
+ if (
+ $home_url === $url
+ || ( $has_http_scheme && false === strpos( $url, $home_url ) )
+ ) {
+ return $url;
+ }
+
+ $canonical_url = $url;
+ // To avoid issues with missing `path` component let's always add a trailing '/'.
+ if ( false !== strpos( $url, '?' ) ) {
+ $canonical_url = preg_replace( '~(\\/)*\\?~', '/?', $canonical_url );
+ } elseif ( false !== strpos( $url, '#' ) ) {
+ $canonical_url = preg_replace( '~(\\/)*#~', '/#', $canonical_url );
+ }
+
+ // Canonical URLs are supposed to contain the home URL.
+ if ( false === strpos( $canonical_url, $home_url ) ) {
+ $canonical_url = home_url( $canonical_url );
+ }
+
+ if ( empty( $canonical_url ) ) {
+ return $home_url;
+ }
+
+ // Passthru vars are additional salts for the cache that would render it useless: parse them here.
+ $query = (string) parse_url( $url, PHP_URL_QUERY );
+ wp_parse_str( $query, $query_vars );
+ // Non-scalar value query vars should not be handled, but they should survive the resolution and not be cached.
+ $scalar_query_vars = array_filter( $query_vars, 'is_scalar' );
+ $passthru_vars = array_diff_key( $query_vars, $scalar_query_vars );
+ // Remove the passthru query vars from the URL to match the correct cache.
+ $url = remove_query_arg( array_keys( $passthru_vars ), $url );
+ // Normalize the URL to make sure there's a trailing slash at the end of the path, before the query or fragment.
+ $url = preg_replace( '~(?warmup_cache(
+ 'canonical_url',
+ WEEK_IN_SECONDS,
+ Listener::TRIGGER_GENERATE_REWRITE_RULES
+ );
+ if ( isset( $this->canonical_url_cache[ $url ] ) ) {
+ // Re-apply passthru vars now, if any.
+ return add_query_arg( $passthru_vars, $this->canonical_url_cache[ $url ] );
+ }
+ }
+
+ $query_vars = array_intersect_key( $query_vars, $scalar_query_vars );
+
+ if ( isset( $query_vars['paged'] ) && 1 === (int) $query_vars['paged'] ) {
+ // Remove the `paged` query var if it's 1.
+ unset( $query_vars['paged'] );
+ }
+
+ ksort( $query_vars );
+
+ $our_rules = $this->get_handled_rewrite_rules();
+ $handled_query_vars = $this->get_rules_query_vars( $our_rules );
+ $handled_post_types = $this->get_post_types();
+
+ if (
+ // The rules we handle should not be empty.
+ empty( $our_rules )
+ || ! (
+ // Supported post types should be either keys or values, of the `post_type` argument, in the query vars.
+ count( array_intersect_key( array_flip( $handled_post_types ), $query_vars ) )
+ || in_array( Arr::get( $query_vars, 'post_type', 'post' ), $handled_post_types, true )
+ )
+ ) {
+ $wp_canonical = redirect_canonical( $canonical_url, false );
+ if ( empty( $wp_canonical ) ) {
+ $wp_canonical = $canonical_url;
+ }
+
+ $this->canonical_url_cache[ $url ] = $wp_canonical;
+
+ return $wp_canonical;
+ }
+
+ $bases = (array) $this->get_bases();
+ ksort( $bases );
+
+ $localized_matchers = $this->get_localized_matchers();
+ $dynamic_matchers = $this->get_dynamic_matchers( $query_vars );
+
+ // Try to match only on the query vars we're actually handling.
+ $matched_vars = array_intersect_key( $query_vars, array_combine( $handled_query_vars, $handled_query_vars ) );
+ $unmatched_vars = array_diff_key( $query_vars, array_combine( $handled_query_vars, $handled_query_vars ) );
+
+ if ( empty( $matched_vars ) ) {
+ // The URL does contain query vars, but none we handle.
+ $wp_canonical = trailingslashit( redirect_canonical( $url, false ) );
+ $this->canonical_url_cache[ $url ] = $wp_canonical;
+
+ return $wp_canonical;
+ }
+
+ $found = false;
+
+ foreach ( $our_rules as $link_template => $index_path ) {
+ wp_parse_str( (string) parse_url( $index_path, PHP_URL_QUERY ), $link_vars );
+ ksort( $link_vars );
+
+ if ( array_keys( $link_vars ) !== array_keys( $matched_vars ) ) {
+ continue;
+ }
+
+ if ( ! (
+ Arr::get( $matched_vars, 'post_type', '' ) === Arr::get( $link_vars, 'post_type', '' )
+ && Arr::get( $matched_vars, 'eventDisplay', '' ) === Arr::get( $link_vars, 'eventDisplay', '' )
+ ) ) {
+ continue;
+ }
+
+ $replace = array_map( function ( $localized_matcher ) use ( $matched_vars ) {
+ if ( ! is_array( $localized_matcher ) ) {
+ // For the dates.
+ return isset( $matched_vars[ $localized_matcher ] )
+ ? $matched_vars[ $localized_matcher ]
+ : '';
+ }
+
+ $query_var = $localized_matcher['query_var'];
+ $query_vars = [ $query_var ];
+
+ if ( $query_var === 'name' ) {
+ $query_vars = array_merge( $query_vars, $this->get_post_types() );
+ }
+
+ if ( ! array_intersect( array_keys( $matched_vars ), $query_vars ) ) {
+ return '';
+ }
+
+ if ( isset( $localized_matcher['localized_slug'] ) ) {
+ // If available, then return the localized slug instead of inferring it as we do below.
+ return $localized_matcher['localized_slug'];
+ }
+
+ /*
+ * We use `end` as, by default, the localized version of the slug in the current language will be at the
+ * end of the array.
+ */
+ return end( $localized_matcher['localized_slugs'] );
+ }, $localized_matchers );
+
+ // Include dynamic matchers now.
+ $replace = array_merge( $dynamic_matchers, $replace );
+
+ /*
+ * Prune from the replacements the empty values. This will resolve conflicts (e.g. single and archive w/
+ * same slug) as no two can be true at the same time.
+ * Remove the `` prefix added to localized matchers, if any.
+ */
+ $replace = array_filter( $replace );
+ $replace = array_combine(
+ array_map( static function ( $key ) {
+ return preg_replace(
+ '/' . preg_quote( Tribe__Rewrite::$localized_matcher_delimiter, '/' ) . '\\w*$/',
+ '',
+ $key
+ );
+ }, array_keys( $replace ) ),
+ $replace
+ );
+
+ // Use case-insensitive replace to make sure to work with some decoding using uppercase escaped chars.
+ $replaced = str_ireplace( array_keys( $replace ), $replace, $link_template );
+
+ // Remove trailing chars.
+ $path = rtrim( $replaced, '?$' );
+ $resolved = trailingslashit( home_url( $path ) );
+ $found = true;
+
+ break;
+ }
+
+ if ( empty( $resolved ) ) {
+ $wp_canonical = redirect_canonical( $canonical_url, false );
+ $resolved = empty( $wp_canonical ) ? $canonical_url : $wp_canonical;
+ }
+
+ if ( $canonical_url !== $resolved ) {
+ // Be sure to add a trailing slash to the URL; before `?` or `#`.
+ $resolved = preg_replace( '/(?canonical_url_cache[ $url ] = $resolved;
+ }
+
+ // Re-apply passthru vars now, if any. After the caching to allow salting the cache key too much.
+ $resolved = add_query_arg( $passthru_vars, $resolved );
+
+ return $resolved;
+ }
+
+ /**
+ * Returns an array of rewrite rules handled by the implementation.
+ *
+ * @since 4.9.11
+ *
+ * @return array An array of rewrite rules handled by the implementation in the shape `[ => ]`.
+ */
+ protected function get_handled_rewrite_rules() {
+ // Try and pull it from memoized values.
+ $cache = tribe_cache();
+ $our_rules = $cache['handled_rewrite_rules'] ?? null;
+
+ // We need to make sure we are have WP_Rewrite setup
+ if ( ! $this->rewrite || empty( $this->rewrite->rules ) ) {
+ $this->setup();
+ }
+
+ $all_rules = isset( $this->rewrite->rules ) ? (array) $this->rewrite->rules : [];
+
+ if ( ! is_array( $our_rules ) ) {
+ // While this is specific to The Events Calendar we're handling a small enough post type base to keep it here.
+ $pattern = '/post_type=tribe_(events|venue|organizer)/';
+ // Reverse the rules to try and match the most complex first.
+ $our_rules = array_filter( $all_rules,
+ static function ( $rule_query_string ) use ( $pattern ) {
+ return is_string( $rule_query_string ) && preg_match( $pattern, $rule_query_string );
+ }
+ );
+
+ $cache['handled_rewrite_rules'] = $our_rules;
+ }
+
+ /**
+ * Filters the list of rewrite rules handled by our code to add or remove some as required.
+ *
+ * @since 4.9.18
+ *
+ * @param array $our_rules An array of rewrite rules handled by our code, in the shape
+ * `[ => ]`.
+ * E.g. `[ '(?:events)/(?:list)/?$' => 'index.php?post_type=tribe_events&eventDisplay=list' ]`.
+ * @param array $all_rules All the current rewrite rules, before any filtering is applied; these
+ * have the same ` rewrite >` format as the previous argument,
+ * which is the format used by WordPress rewrite rules.
+ */
+ $our_rules = apply_filters( 'tribe_rewrite_handled_rewrite_rules', $our_rules, $all_rules );
+
+ return $our_rules;
+ }
+
+ /**
+ * Returns a map relating localized regex matchers to query vars.
+ *
+ * @since 4.9.11
+ *
+ * @return array A map of localized regex matchers in the shape `[ => ]`.
+ */
+ protected function get_localized_matchers() {
+ $cache_key = __METHOD__;
+ $cache = tribe_cache();
+
+ $localized_matchers = $cache[ $cache_key ];
+
+ if ( ! empty( $localized_matchers ) ) {
+ return $localized_matchers;
+ }
+
+ $bases = (array) $this->get_bases();
+ $query_var_map = $this->get_matcher_to_query_var_map();
+ $localized_matchers = [];
+
+ foreach ( $bases as $base => $localized_matcher ) {
+ // Use the base too to allow possible conflicts if the slugs are the same for single and archive.
+ $localized_matcher_key = $localized_matcher . static::$localized_matcher_delimiter . $base;
+
+ if ( isset( $localized_matchers[ $localized_matcher_key ] ) ) {
+ continue;
+ }
+
+ if ( isset( $query_var_map[ $base ] ) ) {
+ $localized_matchers[ $localized_matcher_key ] = [
+ 'base' => $base,
+ 'query_var' => $query_var_map[ $base ],
+ 'en_slug' => $base,
+ 'localized_slugs' => [ $base ],
+ ];
+ // If we have the localized slug version then let's parse it.
+ preg_match( '/^\\(\\?:(?[^\\)]+)\\)$/u', $localized_matcher, $buffer );
+ if ( ! empty( $buffer['slugs'] ) ) {
+ $slugs = explode( '|', $buffer['slugs'] );
+
+ $localized_matchers[ $localized_matcher_key ]['localized_slugs'] = array_map(
+ static function ( $localized_slug ) {
+ return str_replace( '\-', '-', $localized_slug );
+ },
+ $slugs
+ );
+
+ // The English version is the first.
+ $localized_matchers[ $localized_matcher_key ]['en_slug'] = reset( $slugs );
+
+ $localized_slug = $this->filter_matcher( null, $base );
+
+ if ( $localized_slug ) {
+ $localized_matchers[ $localized_matcher_key ]['localized_slug'] = $localized_slug;
+ }
+ }
+ }
+ }
+
+ $cache[ $cache_key ] = $localized_matchers;
+
+ return $localized_matchers;
+ }
+
+ /**
+ * Returns a map relating localized matcher slugs to the corresponding query var.
+ *
+ * @since 4.9.11
+ *
+ * @return array A map relating localized matcher slugs to the corresponding query var.
+ */
+ protected function get_matcher_to_query_var_map() {
+ throw new BadMethodCallException(
+ 'This method should not be called on the base class (' . __CLASS__ . '); only on extending classes.'
+ );
+ }
+
+ /**
+ * Return a list of the query vars handled in the input rewrite rules.
+ *
+ * @since 4.9.11
+ *
+ * @param array $rules A set of rewrite rules in the shape `[ => ]`.
+ *
+ * @return array A list of all the query vars handled in the rules.
+ */
+ protected function get_rules_query_vars( array $rules ) {
+ static $cache_var_name = __METHOD__;
+
+ $cached_rules = tribe_get_var( $cache_var_name, [] );
+ $cache_key = md5( json_encode( $rules ) );
+
+ if ( ! isset( $cached_rules[ $cache_key ] ) ) {
+ $cached_rules[ $cache_key ] = array_unique(
+ array_filter(
+ array_merge(
+ [],
+ ...array_values(
+ array_map(
+ static function ( $rule_string ) {
+ wp_parse_str( parse_url( $rule_string, PHP_URL_QUERY ), $vars );
+
+ return array_keys( $vars );
+ },
+ array_filter( $rules, 'is_string' )
+ )
+ )
+ )
+ )
+ );
+
+ tribe_set_var( $cache_var_name, $cached_rules );
+ }
+
+ return $cached_rules[ $cache_key ];
+ }
+
+ /**
+ * Sets up the dynamic matchers based on the link query vars.
+ *
+ * @since 4.9.11
+ *
+ * @param array $query_vars An map of query vars and their values.
+ *
+ * @return array A map of dynamic matchers in the shape `[ => ]`.
+ */
+ protected function get_dynamic_matchers( array $query_vars ) {
+ $bases = (array) $this->get_bases();
+ $dynamic_matchers = [];
+
+ /*
+ * In some instance we use the `page` (w/o `d`) to paginate a dynamic archive.
+ * Let's support that too.
+ * It's important to add `page` after `paged` to try and match the longest (`paged`) first.
+ */
+ foreach ( [ 'paged', 'page' ] as $page_var ) {
+ if ( isset( $query_vars[ $page_var ] ) ) {
+ $page_regex = $bases['page'];
+ preg_match( '/^\(\?:(?[^\\)]+)\)/', $page_regex, $matches );
+ if ( isset( $matches['slugs'] ) ) {
+ $slugs = explode( '|', $matches['slugs'] );
+ // The localized version is the last, by default.
+ $en_slug = end( $slugs );
+ $localized_slug = $this->filter_matcher( null, 'page' );
+
+ // We use two different regular expressions to read pages, let's add both.
+ if ( $localized_slug ) {
+ $dynamic_matchers["{$page_regex}/(\d+)"] = "{$localized_slug}/{$query_vars[$page_var]}";
+ $dynamic_matchers["{$page_regex}/([0-9]{1,})"] = "{$localized_slug}/{$query_vars[$page_var]}";
+ } else {
+ $dynamic_matchers["{$page_regex}/(\d+)"] = "{$en_slug}/{$query_vars[$page_var]}";
+ $dynamic_matchers["{$page_regex}/([0-9]{1,})"] = "{$en_slug}/{$query_vars[$page_var]}";
+ }
+ }
+ }
+ }
+
+ if ( isset( $query_vars['tag'] ) ) {
+ $tag = $query_vars['tag'];
+ $tag_term = get_term_by( 'slug', $tag, 'post_tag' );
+
+ if ( $tag_term instanceof WP_Term ) {
+ // Let's actually add the matcher only if the tag exists.
+ $tag_regex = $bases['tag'];
+ preg_match( '/^\(\?:(?[^\\)]+)\)/', $tag_regex, $matches );
+ if ( isset( $matches['slugs'] ) ) {
+ $slugs = explode( '|', $matches['slugs'] );
+ // The localized version is the last, by default.
+ $en_slug = end( $slugs );
+ $localized_slug = $this->filter_matcher( null, 'tag' );
+
+ if ( $localized_slug ) {
+ $dynamic_matchers["{$tag_regex}/([^/]+)"] = "{$localized_slug}/{$tag}";
+ } else {
+ $dynamic_matchers["{$tag_regex}/([^/]+)"] = "{$en_slug}/{$tag}";
+ }
+ }
+ }
+ }
+
+ if ( isset( $query_vars['feed'] ) ) {
+ $feed_regex = 'feed/(feed|rdf|rss|rss2|atom)';
+ $dynamic_matchers[ $feed_regex ] = "feed/{$query_vars['feed']}";
+ }
+
+ return $dynamic_matchers;
+ }
+
+ /**
+ * Returns a list of post types supported by the implementation.
+ *
+ * @since 4.9.11
+ *
+ * @return array An array of post types supported and handled by the rewrite implementation.
+ */
+ protected function get_post_types() {
+ throw new BadMethodCallException( 'Method get_post_types should be implemented by extending classes.' );
+ }
+
+ /**
+ * Parses a URL to produce an array of query variables.
+ *
+ * Most of this functionality was copied from `WP::parse_request()` method
+ * with some changes to avoid conflicts and removing non-required behaviors.
+ *
+ * @since 4.9.11
+ *
+ * @param string $url The URLto parse.
+ * @param array $extra_query_vars An associative array of extra query vars to use for the parsing. These vars will
+ * be read before the WordPress defined ones overriding them.
+ * @param bool $force Whether to try and use the cache or force a new canonical URL conversion.
+ *
+ * @return array An array of query vars, as parsed from the input URL.
+ */
+ public function parse_request( $url, array $extra_query_vars = [], $force = false ) {
+ if ( null === $this->rewrite ) {
+ // We re-do this check here as the object might have been initialized before the global rewrite was set.
+ $this->setup();
+ }
+
+ /**
+ * Allows short-circuiting the URL parsing.
+ *
+ * This filter will run before any logic runs, its result will not be cached and this filter will be called on
+ * each call to this method.
+ * Returning a non `null` value here will short-circuit this logic.
+ *
+ * @since 4.9.11
+ *
+ * @param array $query_vars The parsed query vars array.
+ * @param array $extra_query_vars An associative array of extra query vars that will be processed before the
+ * WordPress defined ones.
+ * @param string $url The URL to parse.
+ */
+ $parsed = apply_filters( 'tribe_rewrite_pre_parse_query_vars', null, $extra_query_vars, $url );
+ if ( null !== $parsed ) {
+ return $parsed;
+ }
+
+ if ( ! $force ) {
+ $this->warmup_cache(
+ 'parse_request',
+ WEEK_IN_SECONDS,
+ Listener::TRIGGER_GENERATE_REWRITE_RULES
+ );
+ if ( isset( $this->parse_request_cache[ $url ] ) ) {
+ return $this->parse_request_cache[ $url ];
+ }
+ }
+
+ $query_vars = [];
+ $post_type_query_vars = [];
+ $perma_query_vars = [];
+ $url_components = parse_url($url);
+ $url_path = Arr::get( $url_components, 'path', '/' );
+ $site_path = parse_url( home_url(), PHP_URL_PATH );
+ if ( ! empty( $site_path ) && '/' !== $site_path ) {
+ // The current site is in a sub-directory: the site path should be dropped from the request path.
+ $url_path = str_replace( $site_path, '', $url_path );
+ }
+ $url_query = Arr::get( $url_components, 'query', '' );
+ parse_str( $url_query, $url_query_vars );
+ // Look for matches, removing leading `/` char.
+ $request_match = ltrim( $url_path, '/' );
+ $decoded_request_match = urldecode( $request_match );
+
+ // Fetch the rewrite rules.
+ $rewrite_rules = $this->rewrite->wp_rewrite_rules();
+ $matched_rule = false;
+
+ if ( ! empty( $rewrite_rules ) ) {
+ foreach ( (array) $rewrite_rules as $match => $query ) {
+ $matches_regex = preg_match( "#^$match#", $request_match, $matches )
+ || preg_match( "#^$match#", $decoded_request_match, $matches );
+
+ if ( ! $matches_regex ) {
+ continue;
+ }
+
+ if (
+ $this->rewrite->use_verbose_page_rules
+ && preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch )
+ ) {
+ // This is a verbose page match, let's check to be sure about it.
+ $page = get_page_by_path( $matches[ $varmatch[1] ] );
+ if ( ! $page ) {
+ continue;
+ }
+ $post_status_obj = get_post_status_object( $page->post_status );
+ if (
+ ! $post_status_obj->public
+ && ! $post_status_obj->protected
+ && ! $post_status_obj->private
+ && $post_status_obj->exclude_from_search
+ ) {
+ continue;
+ }
+ }
+
+ // Got a match.
+ $matched_rule = $match;
+ break;
+ }
+
+ if ( false !== $matched_rule ) {
+ // Trim the query of everything up to the '?'.
+ $query = preg_replace( '!^.+\?!', '', $query );
+ // Substitute the substring matches into the query.
+ $query = addslashes( WP_MatchesMapRegex::apply( $query, $matches ) );
+ // Parse the query.
+ parse_str( $query, $perma_query_vars );
+ }
+ }
+
+ foreach ( get_post_types( [], 'objects' ) as $post_type => $t ) {
+ if (
+ is_post_type_viewable( $t )
+ && $t->query_var
+ ) {
+ $post_type_query_vars[ $t->query_var ] = $post_type;
+ }
+ }
+
+ global $wp;
+
+ /*
+ * WordPress would apply this filter in the `parse_request` method to allow the registration of additional query
+ * vars. They might not have been registered at this point so we do this again making sure to avoid duplicates.
+ */
+ $public_query_vars = array_unique( apply_filters( 'query_vars', $wp->public_query_vars ) );
+
+ foreach ( $public_query_vars as $wpvar ) {
+ if ( isset( $extra_query_vars[ $wpvar ] ) ) {
+ $query_vars[ $wpvar ] = $extra_query_vars[ $wpvar ];
+ } elseif ( isset( $perma_query_vars[ $wpvar ] ) ) {
+ $query_vars[ $wpvar ] = $perma_query_vars[ $wpvar ];
+ }
+ if ( ! empty( $query_vars[ $wpvar ] ) ) {
+ if ( ! is_array( $query_vars[ $wpvar ] ) ) {
+ $query_vars[ $wpvar ] = (string) $query_vars[ $wpvar ];
+ } else {
+ foreach ( $query_vars[ $wpvar ] as $vkey => $v ) {
+ if ( is_scalar( $v ) ) {
+ $query_vars[ $wpvar ][ $vkey ] = (string) $v;
+ }
+ }
+ }
+ if ( isset( $post_type_query_vars[ $wpvar ] ) ) {
+ $query_vars['post_type'] = $post_type_query_vars[ $wpvar ];
+ $query_vars['name'] = $query_vars[ $wpvar ];
+ }
+ }
+ }
+
+ // Convert urldecoded spaces back into `+`.
+ foreach ( get_taxonomies( [], 'objects' ) as $taxonomy => $t ) {
+ if ( $t->query_var && isset( $query_vars[ $t->query_var ] ) ) {
+ $query_vars[ $t->query_var ] = str_replace( ' ', '+', $query_vars[ $t->query_var ] );
+ }
+ }
+
+ // Don't allow non-publicly queryable taxonomies to be queried from the front end.
+ if ( ! is_admin() ) {
+ foreach ( get_taxonomies( [ 'publicly_queryable' => false ], 'objects' ) as $taxonomy => $t ) {
+ /*
+ * Disallow when set to the 'taxonomy' query var.
+ * Non-publicly queryable taxonomies cannot register custom query vars. See register_taxonomy().
+ */
+ if ( isset( $query_vars['taxonomy'] ) && $taxonomy === $query_vars['taxonomy'] ) {
+ unset( $query_vars['taxonomy'], $query_vars['term'] );
+ }
+ }
+ }
+
+ // Limit publicly queried post_types to those that are publicly_queryable
+ if ( isset( $query_vars['post_type'] ) ) {
+ $queryable_post_types = get_post_types( [ 'publicly_queryable' => true ] );
+ if ( ! is_array( $query_vars['post_type'] ) ) {
+ if ( ! in_array( $query_vars['post_type'], $queryable_post_types ) ) {
+ unset( $query_vars['post_type'] );
+ }
+ } else {
+ $query_vars['post_type'] = array_intersect( $query_vars['post_type'], $queryable_post_types );
+ }
+ }
+
+ // Resolve conflicts between posts with numeric slugs and date archive queries.
+ $query_vars = wp_resolve_numeric_slug_conflicts( $query_vars );
+
+ foreach ( (array) $wp->private_query_vars as $var ) {
+ if ( isset( $extra_query_vars[ $var ] ) ) {
+ $query_vars[ $var ] = $extra_query_vars[ $var ];
+ }
+ }
+
+ /*
+ * If we have both the `name` query var and the post type one, then let's remove the `name` one.
+ */
+ if ( array_intersect( array_keys( $query_vars ), $this->get_post_types() ) ) {
+ unset( $query_vars['name'] );
+ }
+
+ if ( ! empty( $url_query_vars ) ) {
+ // If the URL did have query vars keep them if not overridden by our resolution.
+ $query_vars = array_merge( $url_query_vars, $query_vars );
+ }
+
+ // Prune the query vars to drop the empty `page` or `paged` ones.
+ $query_vars = array_filter( $query_vars, static function ( $value, $key ) {
+ return ! in_array( $key, [ 'paged', 'page' ] ) || (int) $value !== 0;
+ }, ARRAY_FILTER_USE_BOTH );
+
+ /**
+ * Filters the array of parsed query variables after the class logic has been applied to it.
+ *
+ * Due to the costly nature of this operation the results will be cached. The logic, and this filter, will
+ * not run a second time for the same URL in the context of the same request.
+ *
+ * @since 4.9.11
+ *
+ * @param array $query_vars The parsed query vars array.
+ * @param array $extra_query_vars An associative array of extra query vars that will be processed before the
+ * WordPress defined ones.
+ * @param string $url The URL to parse.
+ */
+ $query_vars = apply_filters( 'tribe_rewrite_parse_query_vars', $query_vars, $extra_query_vars, $url );
+
+ if ( $matched_rule ) {
+ // Since we're caching let's not cache unmatchec URLs to allow for their later, valid matching.
+ $this->parse_request_cache[ $url ] = $query_vars;
+ }
+
+ return $query_vars;
+ }
+
+ /**
+ * Returns the "clean" version of a URL.
+ *
+ * The URL is first parsed then resolved to a canonical URL.
+ * As an example the URL `/events/list/?post_type=tribe_events` is "dirty" in that the `post_type` query variable
+ * is redundant. The clean version of the URL is `/events/list/`, where the query variable is removed.
+ *
+ * @since 4.9.11
+ *
+ * @param string $url The URL to clean.
+ * @param bool $force Whether to try and use the cache or force a new URL cleaning run.
+ *
+ * @return string The cleaned URL, or the input URL if it could not be resolved to a clean one.
+ */
+ public function get_clean_url( $url, $force = false ) {
+ if ( ! $force ) {
+ $this->warmup_cache(
+ 'clean_url',
+ WEEK_IN_SECONDS,
+ Listener::TRIGGER_GENERATE_REWRITE_RULES
+ );
+ if ( isset( $this->clean_url_cache[ $url ] ) ) {
+ return $this->clean_url_cache[ $url ];
+ }
+ }
+
+ $parsed_vars = $this->parse_request( $url );
+
+ if ( empty( $parsed_vars ) ) {
+ return home_url();
+ }
+
+ $clean = $this->get_canonical_url( add_query_arg( $parsed_vars, home_url( '/' ) ), $force );
+
+ $this->clean_url_cache[ $url ] = $clean;
+
+ return $clean;
+ }
+
+ /**
+ * Filters the localized matcher to allow integrations to provider contextual translations of the matcher.
+ *
+ * @since 5.0.17
+ *
+ * @param string|null $localized_matcher The localized matcher.
+ * @param string $base The base the localized matcher is for.
+ *
+ * @return string The localized matcher.
+ */
+ protected function filter_matcher( ?string $localized_matcher, string $base ): string {
+ return (string) apply_filters( 'tec_common_rewrite_localize_matcher', $localized_matcher, $base );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Body_Classes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Body_Classes.php
new file mode 100644
index 000000000..52c3b496f
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Body_Classes.php
@@ -0,0 +1,88 @@
+hooks();
+ }
+
+ /**
+ * Set up hooks for classes.
+ *
+ * @since 4.12.6
+ */
+ private function hooks() {
+ add_filter( 'body_class', [ $this, 'add_body_classes' ] );
+ add_filter( 'admin_body_class', [ $this, 'add_admin_body_classes' ] );
+
+ /**
+ * Allows plugins to hook into the hooks action to register their own hooks.
+ *
+ * @since 4.12.6
+ *
+ * @param Tribe\Service_Providers\Dialog $dialog
+ */
+ do_action( 'tribe_body_classes_hooks', $this );
+ }
+
+ /**
+ * Hook in and add FE body classes.
+ *
+ * @since 4.12.6
+ *
+ * @param array $classes An array of body class names.
+ * @return array The modified array of body class names.
+ */
+ public function add_body_classes( $classes = [] ) {
+ /** @var Body_Class_Object $body_classes */
+ $body_classes = tribe( Body_Class_Object::class );
+
+ return $body_classes->add_body_classes( $classes );
+ }
+
+ /**
+ * Hook in and add admin body classes.
+ *
+ * @since 4.12.6
+ *
+ * @param array $classes An array of body class names.
+ * @return array The modified array of body class names.
+ */
+ public function add_admin_body_classes( $classes = [] ) {
+ /** @var Body_Class_Object $body_classes */
+ $body_classes = tribe( Body_Class_Object::class );
+
+ return $body_classes->add_admin_body_classes( $classes );
+ }
+
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Crons.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Crons.php
new file mode 100644
index 000000000..1055ca954
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Crons.php
@@ -0,0 +1,39 @@
+ 0 ) {
+ $panels = array_merge( $panels, $tribe_panels );
+ }
+
+ return $panels;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Dialog.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Dialog.php
new file mode 100644
index 000000000..df6391c8f
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Dialog.php
@@ -0,0 +1,113 @@
+hooks();
+ }
+
+ /**
+ * Set up hooks for classes.
+ *
+ * @since 4.10.0
+ */
+ private function hooks() {
+ add_action( 'tribe_common_loaded', [ $this, 'register_dialog_assets' ] );
+ add_filter( 'tribe_template_public_namespace', [ $this, 'template_public_namespace' ], 10, 2 );
+
+ /**
+ * Allows plugins to hook into the hooks action to register their own hooks
+ *
+ * @since 4.10.0
+ *
+ * @param Tribe\Service_Providers\Dialog $dialog
+ */
+ do_action( 'tribe_dialog_hooks', $this );
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @since 4.10.0
+ */
+ public function template_public_namespace( $namespace, $obj ) {
+ if ( ! empty( $obj->template_namespace ) && 'dialog' === $obj->template_namespace ) {
+ array_push( $namespace, 'dialog' );
+ }
+
+ return $namespace;
+ }
+
+ /**
+ * Register assets associated with dialog
+ *
+ * @since 4.10.0
+ */
+ public function register_dialog_assets() {
+ $main = \Tribe__Main::instance();
+
+ tribe_asset(
+ $main,
+ 'tribe-dialog',
+ 'dialog.css',
+ [ 'tec-variables-full' ],
+ [],
+ [ 'groups' => 'tribe-dialog' ]
+ );
+
+ tribe_asset(
+ $main,
+ 'tec-a11y-dialog',
+ 'tec-a11y-dialog.js',
+ [ 'underscore', 'tribe-common' ],
+ [],
+ [ 'groups' => 'tribe-dialog' ]
+ );
+
+ tribe_asset(
+ $main,
+ 'tribe-dialog-js',
+ 'dialog.js',
+ [ 'tec-a11y-dialog' ],
+ [],
+ [ 'groups' => 'tribe-dialog' ]
+ );
+
+ /**
+ * Allows plugins to hook into the assets action to register their own assets
+ *
+ * @since 4.10.0
+ *
+ * @param Tribe\Service_Providers\Dialog $dialog
+ */
+ do_action( 'tribe_dialog_assets_registered', $this );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Onboarding.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Onboarding.php
new file mode 100644
index 000000000..b26b29fa7
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Onboarding.php
@@ -0,0 +1,149 @@
+is_enabled() ) {
+ return;
+ }
+
+ $this->container->singleton( Onboarding_Main::class, Onboarding_Main::class );
+ $this->container->singleton( static::class, static::class );
+
+ $this->hooks();
+ }
+
+ /**
+ * Set up hooks for classes.
+ *
+ * @since 4.14.9
+ */
+ protected function hooks() {
+ add_action( 'tribe_common_loaded', [ $this, 'register_assets' ] );
+
+ add_action( 'admin_enqueue_scripts', tribe_callback( Onboarding_Main::class, 'localize_tour' ) );
+ add_action( 'admin_enqueue_scripts', tribe_callback( Onboarding_Main::class, 'localize_hints' ) );
+ }
+
+ /**
+ * Register assets associated with onboarding.
+ *
+ * @since 4.14.9
+ */
+ public function register_assets() {
+ $main = \Tribe__Main::instance();
+
+ tribe_asset(
+ $main,
+ 'tec-intro-js',
+ 'node_modules/intro.js/intro.js',
+ [],
+ [ 'admin_enqueue_scripts' ],
+ [
+ 'groups' => self::$group_key,
+ 'conditionals' => [ $this, 'should_enqueue_assets' ],
+ ]
+ );
+
+ tribe_asset(
+ $main,
+ 'tec-intro-styles',
+ 'node_modules/intro.js/introjs.css',
+ [],
+ [ 'admin_enqueue_scripts' ],
+ [
+ 'groups' => self::$group_key,
+ 'conditionals' => [ $this, 'should_enqueue_assets' ],
+ ]
+ );
+
+ tribe_asset(
+ $main,
+ 'tec-onboarding-styles',
+ 'onboarding.css',
+ [ 'tec-intro-styles', 'tec-variables-skeleton', 'tec-variables-full' ],
+ [ 'admin_enqueue_scripts' ],
+ [
+ 'groups' => self::$group_key,
+ 'conditionals' => [ $this, 'should_enqueue_assets' ],
+ ]
+ );
+
+ tribe_asset(
+ $main,
+ 'tec-onboarding-js',
+ 'onboarding.js',
+ [
+ 'tribe-common',
+ 'tec-intro-js',
+ ],
+ [ 'admin_enqueue_scripts' ],
+ [
+ 'groups' => self::$group_key,
+ 'in_footer' => false,
+ 'localize' => [
+ 'name' => 'TribeOnboarding',
+ 'data' => [
+ 'hintButtonLabel' => __( 'Got it', 'tribe-common' ),
+ ],
+ ],
+ 'conditionals' => [ $this, 'should_enqueue_assets' ],
+ ]
+ );
+ }
+
+ /**
+ * Define if the assets for `Onboarding` should be enqueued or not.
+ *
+ * @since 4.14.9
+ *
+ * @return bool If the Onboarding assets should be enqueued or not.
+ */
+ public function should_enqueue_assets() {
+ return $this->is_enabled();
+ }
+
+ /**
+ * Check if the onboarding is enabled or not.
+ *
+ * @since 4.14.9
+ *
+ * @return bool
+ */
+ public function is_enabled() {
+ /**
+ * Filter to disable tribe onboarding
+ *
+ * @since 4.14.9
+ *
+ * @param bool $disabled If we want to disable the on boarding.
+ */
+ $is_enabled = (bool) apply_filters( 'tec_onboarding_enabled', false );
+
+ return $is_enabled && is_admin();
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/PUE.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/PUE.php
new file mode 100644
index 000000000..25eb50b14
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/PUE.php
@@ -0,0 +1,52 @@
+container->singleton( Update_Prevention::class, Update_Prevention::class );
+
+ // Setup all of WP hooks associated with PUE.
+ $this->register_hooks();
+ }
+
+ /**
+ * Registers the provider handling all the 1st level filters and actions for PUE.
+ *
+ * @since 4.9.2
+ */
+ protected function register_hooks() {
+ add_filter( 'upgrader_source_selection', [ $this, 'filter_upgrader_source_selection' ], 15, 4 );
+ }
+
+ /**
+ * Filters the source file location for the upgrade package for the PUE Update_Prevention engine.
+ *
+ * @since 4.9.12
+ *
+ * @param string $source File source location.
+ * @param string $remote_source Remote file source location.
+ * @param WP_Upgrader $upgrader WP_Upgrader instance.
+ * @param array $extra Extra arguments passed to hooked filters.
+ */
+ public function filter_upgrader_source_selection( $source, $remote_source, $upgrader, $extras ) {
+ return $this->container->make( Update_Prevention::class )->filter_upgrader_source_selection( $source, $remote_source, $upgrader, $extras );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Processes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Processes.php
new file mode 100644
index 000000000..f7d929a48
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Processes.php
@@ -0,0 +1,281 @@
+context = tribe( 'context' );
+
+ // If the context of this request is neither AJAX or Cron bail.
+ if ( ! ( $this->context->doing_ajax() || $this->context->doing_cron() ) ) {
+ return;
+ }
+
+ /** @var Tribe__Feature_Detection $feature_detection */
+ $feature_detection = tribe( 'feature-detection' );
+ $action = tribe_get_request_var( 'action', false );
+ $testing_for_async_support = $action === $this->get_handler_action( 'Tribe__Process__Tester' );
+
+ // Dispatch in async mode if testing for it (w/o re-checking) or if async processes are supported.
+ if ( $testing_for_async_support || $feature_detection->supports_async_process() ) {
+ $this->dispatch_async();
+
+ return;
+ }
+
+ $this->dispatch_cron();
+ }
+
+ /**
+ * Hooks the correct handler for the action.
+ *
+ * @since 4.7.12
+ *
+ * @param string $action
+ */
+ protected function hook_handler_for( $action ) {
+ if ( null === $this->handler_actions ) {
+ $handlers = [
+ 'Tribe__Process__Tester',
+ 'Tribe__Process__Post_Thumbnail_Setter',
+ ];
+
+ /**
+ * Filters the process handler classes the Service Provider should handle.
+ *
+ * All handlers should extend the `Tribe__Process__Handler` base class.
+ *
+ * @since 4.7.12
+ *
+ * @param array $handlers
+ */
+ $handlers = array_unique( apply_filters( 'tribe_process_handlers', $handlers ) );
+
+ $this->handler_actions = array_combine(
+ $handlers,
+ array_map( [ $this, 'get_handler_action' ], $handlers )
+ );
+ }
+
+ $array_search = array_search( $action, $this->handler_actions, true );
+
+ if ( false === $handler_class = $array_search ) {
+ return;
+ }
+
+ // the handler will handle the hooking
+ $this->container->make( $handler_class );
+ }
+
+ /**
+ * Hooks the correct queue for the action.
+ *
+ * @since 4.7.12
+ *
+ * @param string $action
+ */
+ protected function hook_queue_for( $action ) {
+ if ( null === $this->queue_actions ) {
+ $queues = [
+ 'Tribe__Promise',
+ ];
+
+ /**
+ * Filters the queue processing classes the Service Provider should handle.
+ *
+ * All queues should extend the `Tribe__Process__Queue` base class.
+ *
+ * @since 4.7.12
+ *
+ * @param array $queues An array of class names, each extending the `Tribe__Process__Queue` base class.
+ */
+ $queues = array_unique( apply_filters( 'tribe_process_queues', $queues ) );
+
+ $all_queues_actions = array_combine(
+ $queues,
+ array_map( [ $this, 'get_queue_action' ], $queues )
+ );
+ }
+
+ $array_search = array_search( $action, $all_queues_actions, true );
+
+ if ( false === $queue_class = $array_search ) {
+ return;
+ }
+
+ // the queue will handle the hooking
+ $this->container->make( $queue_class );
+ }
+
+ /**
+ * Returns the action for the handler.
+ *
+ * @since 4.7.12
+ *
+ * @param string $handler_class
+ *
+ * @return string
+ */
+ protected function get_handler_action( $handler_class ) {
+ /** @var Tribe__Process__Handler handler_class */
+ return 'tribe_process_' . call_user_func( [ $handler_class, 'action' ] );
+ }
+
+ /**
+ * Returns the action for the queue.
+ *
+ * @since 4.7.12
+ *
+ * @param string $queue_class
+ *
+ * @return string
+ */
+ protected function get_queue_action( $queue_class ) {
+ /** @var Tribe__Process__Queue queue_class */
+ return 'tribe_queue_' . call_user_func( [ $queue_class, 'action' ] );
+ }
+
+ /**
+ * Dispatches the request, if in AJAX context of a valid queue processing request,
+ * to the correct handler.
+ *
+ * @since 4.7.23
+ */
+ protected function dispatch_async() {
+ if ( ! (
+ $this->context->doing_ajax()
+ && false !== $action = tribe_get_request_var( 'action', false )
+ ) ) {
+ return;
+ }
+
+ $this->hook_handler_for_action( $action );
+ }
+
+ /**
+ * Start the process handlers if in the context of a cron process and
+ * if any is registered.
+ *
+ * @since 4.7.23
+ */
+ protected function dispatch_cron() {
+ if ( ! $this->context->doing_cron() ) {
+ return;
+ }
+
+ /*
+ * Here we parse the scheduled cron events to get those scheduled by a queue
+ * or process handler.
+ */
+ $hooks = $this->get_scheduled_like( [ 'tribe_process_', 'tribe_queue_' ] );
+
+ if ( empty( $hooks ) ) {
+ return;
+ }
+
+ foreach ( $hooks as $action ) {
+ /*
+ * Building the queue or process handler for an action will make it
+ * so the handler, in its `__construct` method, will hook on the action
+ * triggered by its cron event.
+ */
+ $this->hook_handler_for_action( $action );
+ }
+ }
+
+ /**
+ * Hooks the correct queue or process handler for an action if any.
+ *
+ * @since 4.7.23
+ *
+ * @param string $action The action to hook the handler, or queue, for.
+ */
+ protected function hook_handler_for_action( $action ) {
+ if (
+ 0 !== strpos( $action, 'tribe_process_' )
+ && 0 !== strpos( $action, 'tribe_queue_' )
+ ) {
+ return;
+ }
+
+ if ( 0 === strpos( $action, 'tribe_process_' ) ) {
+ $this->hook_handler_for( $action );
+ } else {
+ $this->hook_queue_for( $action );
+ }
+ }
+
+ /**
+ * Parses the `cron` array to return the hook names starting with a pattern.
+ *
+ * @since 4.7.23
+ *
+ * @param string|array $needles A pattern to look for or an array of patterns; if
+ * this is an array then a match will be an hook that
+ * matches at least one pattern.
+ *
+ * @return array An array of hook names matching the pattern.
+ */
+ protected function get_scheduled_like( $needles ) {
+ $cron = get_option( 'cron', false );
+
+ if ( empty( $cron ) ) {
+ return [];
+ }
+
+ $needles = (array) $needles;
+ $matching = [];
+
+ foreach ( $cron as $time ) {
+ if ( ! is_array( $time ) ) {
+ continue;
+ }
+ foreach ( $time as $hook => $entry ) {
+ foreach ( $needles as $needle ) {
+ if ( false !== strpos( $hook, $needle ) ) {
+ $matching[] = $hook;
+ }
+ }
+ }
+ }
+
+ return $matching;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Promoter.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Promoter.php
new file mode 100644
index 000000000..f170958df
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Promoter.php
@@ -0,0 +1,119 @@
+hook();
+ }
+
+ /**
+ * Setup hooks for classes.
+ */
+ private function hook() {
+ add_action( 'template_redirect', tribe_callback( 'promoter.view', 'display_auth_check_view' ), 10, 0 );
+ add_action( 'init', tribe_callback( 'promoter.view', 'add_rewrites' ) );
+
+ /** @var Tribe__Promoter__PUE $pue */
+ $pue = tribe( 'promoter.pue' );
+
+ // Only add the setting if a promoter key is present.
+ if ( $pue->has_license_key() ) {
+ add_action(
+ 'init',
+ tribe_callback( 'promoter.auth', 'register_setting' )
+ );
+ }
+
+ // The usage of a high priority so we can push the icon to the end
+ add_action( 'admin_bar_menu', [ $this, 'add_promoter_logo_on_admin_bar' ], 1000 );
+ add_action( 'tribe_common_loaded', [ $this, 'add_promoter_assets' ] );
+ }
+
+ /**
+ * Add Admin Bar link to the promoter website
+ *
+ * @since 4.9.2
+ * @param $wp_admin_bar
+ */
+ public function add_promoter_logo_on_admin_bar( $wp_admin_bar ) {
+ /** @var Tribe__Promoter__PUE $pue */
+ $pue = tribe( 'promoter.pue' );
+ if ( ! $pue->has_license_key() ) {
+ return;
+ }
+
+ /**
+ * It uses and inline SVG as will provider more flexibility for styling so we can change
+ * the fill of the path property of the SVG so we can match the WP installations.
+ */
+ $args = [
+ 'id' => 'promoter-admin-bar',
+ 'title' => sprintf(
+ "%s%s",
+ '',
+ 'Promoter'
+ ),
+ 'href' => 'https://promoter.theeventscalendar.com/',
+ 'meta' => [
+ 'target' => '_blank',
+ 'class' => 'promoter-admin-bar-link',
+ ],
+ ];
+ $wp_admin_bar->add_node( $args );
+ }
+
+ /**
+ * Register assets associated with promoter
+ *
+ * @since 4.9.2
+ */
+ public function add_promoter_assets() {
+ tribe_asset(
+ Tribe__Main::instance(),
+ 'promoter',
+ 'promoter.css',
+ [],
+ [ 'wp_enqueue_scripts', 'admin_enqueue_scripts' ],
+ [
+ 'conditionals' => [ $this, 'should_load_promoter_styles' ],
+ ]
+ );
+ }
+
+ /**
+ * Only load the styles related to promoter if user is logged in and there's a valid license
+ * for promoter
+ *
+ * @since 4.9.2
+ *
+ * @return bool
+ */
+ public function should_load_promoter_styles() {
+ return is_user_logged_in() && tribe( 'promoter.pue' )->has_license_key();
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Shortcodes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Shortcodes.php
new file mode 100644
index 000000000..b952ddaa0
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Shortcodes.php
@@ -0,0 +1,123 @@
+container->singleton( Manager::class, Manager::class );
+ $this->container->singleton(
+ 'shortcode.manager',
+ function() {
+ return $this->container->make( Manager::class );
+ }
+ );
+
+ $this->register_hooks();
+ $this->register_assets();
+
+ $this->container->singleton( static::class, $this );
+
+ }
+
+ /**
+ * Static method wrapper around a filter to allow full deactivation of this provider
+ *
+ * @since 4.12.0
+ *
+ * @return boolean If this service provider is active.
+ */
+ public static function is_active() {
+ /**
+ * Allows filtering to deactivate all shortcodes loading.
+ *
+ * @since 4.12.0
+ *
+ * @param boolean $is_active If shortcodes should be loaded or not.
+ */
+ return apply_filters( 'tribe_shortcodes_is_active', true );
+ }
+
+ /**
+ * Register all the assets associated with this service provider.
+ *
+ * @since 4.12.0
+ */
+ protected function register_assets() {
+
+ }
+
+ /**
+ * Registers the provider handling all the 1st level filters and actions for this service provider.
+ *
+ * @since 4.12.0
+ */
+ protected function register_hooks() {
+ add_action( 'init', [ $this, 'action_add_shortcodes' ], 20 );
+ add_filter( 'pre_do_shortcode_tag', [ $this, 'filter_pre_do_shortcode_tag' ], 10, 4 );
+ add_filter( 'do_shortcode_tag', [ $this, 'filter_do_shortcode_tag' ], 10, 4 );
+ }
+
+ /**
+ * Adds the new shortcodes, this normally will trigger on `init@P20` due to how we the
+ * v1 is added on `init@P10` and we remove them on `init@P15`.
+ *
+ * It's important to leave gaps on priority for better injection.
+ *
+ * @since 4.12.0
+ */
+ public function action_add_shortcodes() {
+ $this->container->make( Manager::class )->add_shortcodes();
+ }
+
+ /**
+ * Filters `pre_do_shortcode_tag` to mark that a tribe shortcode is currently being done.
+ *
+ * @since 4.12.9
+ *
+ * @param bool|string $return Short-circuit return value. Either false or the value to replace the shortcode with.
+ * @param string $tag Shortcode name.
+ * @param array $attr Shortcode attributes array,
+ * @param array $m Regular expression match array.
+ *
+ * @return bool|string Short-circuit return value.
+ */
+ public function filter_pre_do_shortcode_tag( $false, $tag, $attr, $m ) {
+ return $this->container->make( Manager::class )->filter_pre_do_shortcode_tag( $false, $tag, $attr, $m );
+ }
+
+ /**
+ * * Filters `do_shortcode_tag` to mark that a tribe shortcode is complete, and remove it from the current list.
+ *
+ * @since 4.12.9
+ *
+ * @param string $output Shortcode output.
+ * @param string $tag Shortcode name.
+ * @param array|string $attr Shortcode attributes array or empty string.
+ * @param array $m Regular expression match array.
+ *
+ * @return string Shortcode output.
+ */
+ public function filter_do_shortcode_tag( $output, $tag, $attr, $m ) {
+ return $this->container->make( Manager::class )->filter_do_shortcode_tag( $output, $tag, $attr, $m );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Tooltip.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Tooltip.php
new file mode 100644
index 000000000..dd5ef6d19
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Tooltip.php
@@ -0,0 +1,67 @@
+hook();
+ }
+
+ /**
+ * Setup hooks for classes.
+ *
+ * @since 4.9.8
+ */
+ private function hook() {
+ add_action( 'tribe_common_loaded', [ $this, 'add_tooltip_assets' ] );
+ }
+
+ /**
+ * Register assets associated with tooltip
+ *
+ * @since 4.9.8
+ */
+ public function add_tooltip_assets() {
+ $main = \Tribe__Main::instance();
+
+ tribe_asset(
+ $main,
+ 'tribe-tooltip',
+ 'tooltip.css',
+ [ 'tribe-common-skeleton-style' ],
+ null,
+ [
+ 'groups' => 'tribe-tooltip',
+ ]
+ );
+
+ tribe_asset(
+ $main,
+ 'tribe-tooltip-js',
+ 'tooltip.js',
+ [ 'jquery', 'tribe-common' ],
+ null,
+ [
+ 'groups' => 'tribe-tooltip'
+ ]
+ );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Widgets.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Widgets.php
new file mode 100644
index 000000000..2a36d8f63
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Widgets.php
@@ -0,0 +1,79 @@
+container->singleton( Manager::class, Manager::class );
+ $this->container->singleton(
+ 'widget.manager',
+ function() {
+ return $this->container->make( Manager::class );
+ }
+ );
+
+ $this->register_hooks();
+
+ $this->container->singleton( static::class, $this );
+ $this->container->singleton( 'widgets', $this );
+ }
+
+ /**
+ * Static method wrapper around a filter to allow full deactivation of this provider.
+ *
+ * @since 4.12.12
+ *
+ * @return boolean If this service provider is active.
+ */
+ public static function is_active() {
+ /**
+ * Allows filtering to prevent all Tribe widgets from loading.
+ *
+ * @since 4.12.12
+ *
+ * @param boolean $is_active If widgets should be loaded or not.
+ */
+ return apply_filters( 'tribe_widgets_is_active', true );
+ }
+
+ /**
+ * Registers the provider handling all the 1st level filters and actions for this service provider.
+ *
+ * @since 4.12.12
+ */
+ protected function register_hooks() {
+ add_action( 'widgets_init', [ $this, 'register_widgets_with_wp' ], 20 );
+ }
+
+ /**
+ * Adds the new widgets.
+ *
+ * This triggers on `init@P20` due to how v1 is added on `init@P10` and removed on `init@P15`,
+ * as it's important to leave gaps on priority for future flexibility.
+ *
+ * @since 4.12.12
+ */
+ public function register_widgets_with_wp() {
+ $this->container->make( Manager::class )->register_widgets_with_wp();
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings.php
new file mode 100644
index 000000000..d5880cfd1
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings.php
@@ -0,0 +1,819 @@
+ => array(...) ]
+ *
+ * @var array
+ */
+ protected $fields_for_save = [];
+
+ /**
+ * An array that contains the fields that are currently being validated.
+ *
+ * @var array
+ */
+ protected $current_fields = [];
+
+ /**
+ * Static Singleton Factory Method.
+ *
+ * @return Tribe__Settings
+ */
+ public static function instance() {
+ return tribe( 'settings' );
+ }
+
+ /**
+ * Class constructor.
+ *
+ * @return void
+ */
+ public function __construct() {
+
+ // Set instance variables.
+ $this->menuName = apply_filters( 'tribe_settings_menu_name', esc_html__( 'Events', 'tribe-common' ) );
+ $this->requiredCap = apply_filters( 'tribe_settings_req_cap', 'manage_options' );
+ $this->adminSlug = apply_filters( 'tribe_settings_admin_slug', 'tribe-common' );
+ $this->help_slug = apply_filters( 'tribe_settings_help_slug', 'tribe-common-help' );
+ $this->errors = get_option( 'tribe_settings_errors', [] );
+ $this->major_error = get_option( 'tribe_settings_major_error', false );
+ $this->sent_data = get_option( 'tribe_settings_sent_data', [] );
+ $this->validated = [];
+ $this->defaultTab = null;
+ $this->currentTab = null;
+
+ $this->hook();
+ }
+
+ /**
+ * Hooks the actions and filters required for the class to work.
+ */
+ public function hook() {
+ // Run actions & filters.
+ add_action( 'admin_init', [ $this, 'initTabs' ] );
+ add_action( 'tribe_settings_below_tabs', [ $this, 'displayErrors' ] );
+ add_action( 'tribe_settings_below_tabs', [ $this, 'displaySuccess' ] );
+ }
+
+ /**
+ * Determines whether or not the full admin pages should be initialized.
+ *
+ * @return boolean
+ */
+ public function should_setup_pages() {
+ // @todo: Deprecate this and update where needed.
+ return true;
+ }
+
+ /**
+ * create the main option page
+ *
+ * @return void
+ */
+ public function addPage() {
+ _deprecated_function( __METHOD__, '4.15.0' );
+ }
+
+ /**
+ * create the network options page
+ *
+ * @return void
+ */
+ public function addNetworkPage() {
+ _deprecated_function( __METHOD__, '4.15.0' );
+ }
+
+ /**
+ * Init all the tabs.
+ *
+ * @return void
+ */
+ public function initTabs() {
+ $admin_pages = tribe( 'admin.pages' );
+ $admin_page = $admin_pages->get_current_page();
+
+ if ( empty( $admin_pages->has_tabs( $admin_page ) ) ) {
+ return;
+ }
+
+ // Load settings tab-specific helpers and enhancements.
+ Tribe__Admin__Live_Date_Preview::instance();
+
+ do_action( 'tribe_settings_do_tabs', $admin_page ); // This is the hook to use to add new tabs.
+
+ $this->tabs = (array) apply_filters( 'tribe_settings_tabs', [], $admin_page );
+ $this->allTabs = (array) apply_filters( 'tribe_settings_all_tabs', [], $admin_page );
+ $this->noSaveTabs = (array) apply_filters( 'tribe_settings_no_save_tabs', [], $admin_page );
+
+ if ( is_network_admin() ) {
+ $this->defaultTab = apply_filters( 'tribe_settings_default_tab_network', 'network', $admin_page );
+ $current_tab = ( isset( $_GET['tab'] ) && $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : $this->defaultTab;
+ $this->currentTab = apply_filters( 'tribe_settings_current_tab', $current_tab, $admin_page );
+ $this->url = $this->get_tab_url( $this->currentTab );
+ } else {
+ $tabs_keys = array_keys( $this->tabs );
+ $default_tab = apply_filters( 'tribe_settings_default_tab', 'general', $admin_page );
+ $this->defaultTab = in_array( $default_tab, $tabs_keys ) ? $default_tab : $tabs_keys[0];
+ $this->currentTab = apply_filters( 'tribe_settings_current_tab', ( isset( $_GET['tab'] ) && $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : $this->defaultTab );
+ $this->url = $this->get_tab_url( $this->currentTab );
+ }
+
+ $this->fields_for_save = (array) apply_filters( 'tribe_settings_fields', [], $admin_page );
+ do_action( 'tribe_settings_after_do_tabs', $admin_page );
+ $this->fields = (array) apply_filters( 'tribe_settings_fields', [], $admin_page );
+ $this->validate();
+ }
+
+ /**
+ * Get the current settings page URL
+ *
+ * @since 4.15.0
+ *
+ * @return string The current settings page URL.
+ */
+ public function get_settings_page_url( array $args = [] ) {
+ $admin_pages = tribe( 'admin.pages' );
+ $page = $admin_pages->get_current_page();
+ $tab = tribe_get_request_var( 'tab', $this->defaultTab );
+ $defaults = [
+ 'page' => $page,
+ 'tab' => $tab,
+ ];
+
+ // Allow the link to be "changed" on the fly.
+ $args = wp_parse_args( $args, $defaults );
+
+ $url = add_query_arg(
+ $args,
+ is_network_admin() ? network_admin_url( 'settings.php' ) : admin_url( 'admin.php' )
+ );
+
+ return apply_filters( 'tribe_settings_page_url', $url, $page, $tab );
+ }
+
+ /**
+ * Get the settings page title.
+ *
+ * @since 4.15.0
+ *
+ * @param string $admin_page The admin page ID.
+ * @return string The settings page title.
+ */
+ public function get_page_title( $admin_page ) {
+ $page_title = sprintf(
+ // Translators: %s is the name of the menu item.
+ __( '%s Settings', 'tribe-common' ),
+ $this->menuName
+ );
+
+ /**
+ * Filter the tribe settings page title.
+ *
+ * @since 4.15.0
+ *
+ * @param string $page_title The settings page title.
+ * @param string $admin_page The admin page ID.
+ */
+ return apply_filters( 'tribe_settings_page_title', $page_title, $admin_page );
+ }
+
+ /**
+ * Generate the main option page.
+ * includes the view file.
+ *
+ * @since 4.15.0 Add the current page as parameter for the actions.
+ *
+ * @return void
+ */
+ public function generatePage() {
+ $admin_pages = tribe( 'admin.pages' );
+ $admin_page = $admin_pages->get_current_page();
+
+ do_action( 'tribe_settings_top', $admin_page );
+ echo '
', wp_kses_post( print_r( $v, true ) ) );
+ }
+ }
+
+ $output .= '';
+
+ return $output;
+ }
+
+ /**
+ * Logs the occurrence of rewrite rule purging
+ */
+ public function log_rewrite_rule_purge() {
+ $this->rewrite_rules_purged = true;
+ }//end log_rewrite_rule_purge
+
+ /**
+ * Sets the obfuscator to be used.
+ *
+ * @param Tribe__Support__Obfuscator $obfuscator
+ */
+ public function set_obfuscator( Tribe__Support__Obfuscator $obfuscator ) {
+ $this->obfuscator = $obfuscator;
+ }
+
+ /**
+ * Creates Fields in Help Tab to Opt In to System Info
+ *
+ * @return string
+ */
+ public static function opt_in() {
+
+
+ $optin_key = ! empty( get_option( self::$option_key ) );
+
+ $opt_in = '';
+ $opt_in .= '
' . esc_html__( 'Your system information will only be used by The Events Calendar\'s support team. All information is stored securely. We do not share this information with any third parties.', 'tribe-common' ) . '
';
+ $opt_in .= '';
+
+ return $opt_in;
+ }
+
+ /**
+ * Method to send back sysinfo
+ *
+ * @param $query
+ *
+ * @return string|void
+ *
+ */
+ public static function sysinfo_query( $query ) {
+
+ $optin_key = get_option( self::$option_key );
+
+ if ( ! $optin_key ) {
+ wp_send_json_error( __( 'Invalid Key', 'tribe-common' ) );
+ }
+
+ $key = $query['key'];
+ if ( $key != $optin_key ) {
+ wp_send_json_error( __( 'Invalid Key', 'tribe-common' ) );
+ }
+
+ $support = Tribe__Support::getInstance();
+ $systeminfo = $support->formattedSupportStats();
+
+ return $systeminfo;
+ }
+
+ /*
+ * Create Unique Endpoint Per Site
+ */
+ public static function create_sysinfo_endpoint() {
+ $optin_key = get_option( self::$option_key );
+ if ( $optin_key ) {
+ register_rest_route(
+ 'tribe_events/v2',
+ '/(?P[a-z0-9\-]+)/sysinfo/',
+ [
+ 'methods' => 'GET',
+ 'callback' => [ 'Tribe__Support', 'sysinfo_query' ],
+ 'permission_callback' => '__return_true',
+ ]
+ );
+ }
+ }
+
+ /**
+ * Ajax Method to Create Unique Key and send to tec.com
+ */
+ public static function ajax_sysinfo_optin() {
+
+ if ( ! isset( $_POST['confirm'] ) || ! wp_verify_nonce( $_POST['confirm'], 'sysinfo_optin_nonce' ) ) {
+ wp_send_json_error( __( 'Permission Error', 'tribe-common' ) );
+ }
+
+ if ( 'generate' == $_POST['generate_key'] ) {
+ $random = base_convert( rand( 0, getrandmax() ), 10, 36 );
+ $optin_key = hash( 'sha1', $random );
+
+ update_option( self::$option_key, $optin_key );
+
+ //Only Connect If a License Exists
+ $keys = apply_filters( 'tribe-pue-install-keys', [] );
+ if ( is_array( $keys ) && ! empty( $keys ) ) {
+ self::send_sysinfo_key( $optin_key );
+ } else {
+ wp_send_json_success( __( 'Unique System Info Key Generated', 'tribe-common' ) );
+ }
+
+ } elseif ( 'remove' == $_POST['generate_key'] ) {
+ $optin_key = get_option( self::$option_key );
+
+ delete_option( self::$option_key );
+
+ self::send_sysinfo_key( $optin_key, null, 'remove' );
+
+ }
+
+ wp_send_json_error( __( 'Permission Error', 'tribe-common' ) );
+ }
+
+ /**
+ * Contact Tribe Website to Add SysInfo Key
+ *
+ * @param null $optin_key provide key for system info
+ * @param null $url domain of current site
+ * @param null $remove string used if removing $optin_key from tec.com
+ * @param null $pueadd boolean to disable messaging when coming from pue script
+ */
+ public static function send_sysinfo_key( $optin_key = null, $url = null, $remove = null, $pueadd = false ) {
+ $url = $url ? $url : urlencode( str_replace( [ 'http://', 'https://' ], '', get_site_url() ) );
+ $teccom_url = defined( 'TEC_URL' ) ? TEC_URL : 'https://theeventscalendar.com';
+ $query = trailingslashit( $teccom_url ) . 'wp-json/tribe_system/v2/customer-info/' . $optin_key . '/' . $url;
+
+ if ( $remove ) {
+ $query .= '?status=remove';
+ }
+
+ $response = wp_remote_get( esc_url( $query ) );
+
+ $response = json_decode( wp_remote_retrieve_body( $response ) );
+
+ if ( ! $pueadd ) {
+ // make sure the response came back okay
+ if ( ! isset( $response->success ) ) {
+ //on error delete the key
+ delete_option( self::$option_key );
+
+ //send error response
+ wp_send_json_error( $response );
+ }
+
+ wp_send_json_success( $response->data );
+ }
+ }
+
+
+ /****************** SINGLETON GUTS ******************/
+
+ /**
+ * Enforce Singleton Pattern
+ */
+ private static $instance;
+
+
+ public static function getInstance() {
+ if ( null == self::$instance ) {
+ $instance = new self;
+ $instance->set_obfuscator( new Tribe__Support__Obfuscator( $instance->must_obfuscate_prefixes ) );
+ self::$instance = $instance;
+ }
+
+ return self::$instance;
+ }
+ }
+
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Obfuscator.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Obfuscator.php
new file mode 100644
index 000000000..af60e6a59
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Obfuscator.php
@@ -0,0 +1,68 @@
+prefixes = $prefixes;
+ }
+
+ /**
+ * Whether a value should be obfuscated or not.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function should_obfuscate( $key ) {
+ foreach ( $this->prefixes as $prefix ) {
+ if ( strpos( $key, $prefix ) === 0 ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Conditionally obfuscates a string value.
+ *
+ * @param string $key
+ * @param mixed $string_value
+ *
+ * @return mixed Either the obfuscated string or the original value if not a string.
+ */
+ public function obfuscate( $key, $string_value ) {
+ if ( ! is_string( $string_value ) ) {
+ return $string_value;
+ }
+ if ( ! $this->should_obfuscate( $key ) ) {
+ return $string_value;
+ }
+
+ $length = strlen( $string_value );
+ if ( $length <= 3 ) {
+ return preg_replace( "/./", "#", $string_value );
+ } elseif ( $length > 3 && $length <= 5 ) {
+ return preg_replace( '/^(.{1}).*$/', '$1' . str_repeat( '#', $length - 1 ) . '$2', $string_value );
+ } elseif ( $length > 5 && $length <= 9 ) {
+ return preg_replace( '/^(.{1}).*(.{1})$/', '$1' . str_repeat( '#', $length - 2 ) . '$2', $string_value );
+ } elseif ( $length > 9 && $length <= 19 ) {
+ return preg_replace( '/^(.{2}).*(.{2})$/', '$1' . str_repeat( '#', $length - 4 ) . '$2', $string_value );
+ } elseif ( $length > 19 && $length <= 31 ) {
+ return preg_replace( '/^(.{3}).*(.{3})$/', '$1' . str_repeat( '#', $length - 6 ) . '$2', $string_value );
+ }
+
+ return preg_replace( '/^(.{4}).*(.{4})$/', '$1' . str_repeat( '#', $length - 8 ) . '$2', $string_value );
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker.php
new file mode 100644
index 000000000..4bbad8789
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker.php
@@ -0,0 +1,275 @@
+plugin_version = $this->base_version_number( $plugin_version );
+ $this->plugin_views_dir = $plugin_views_dir;
+ $this->theme_views_dir = $theme_views_dir;
+
+ $this->scan_view_directory();
+ $this->scan_for_overrides();
+ }
+
+ /**
+ * Given a version number with an alpha/beta type suffix, strips that suffix and
+ * returns the "base" version number.
+ *
+ * For example, given "9.8.2beta1" this method will return "9.8.2".
+ *
+ * The utility of this is that if the author of a template change sets the
+ * version tag in the template header to 9.8.2 (to continue the same example) we
+ * don't need to worry about updating that for each alpha, beta or RC we put out.
+ *
+ * @param string $version_number
+ *
+ * @return string
+ */
+ protected function base_version_number( $version_number ) {
+ return preg_replace( '/[a-z]+[a-z0-9]*$/i', '', $version_number );
+ }
+
+ /**
+ * Recursively scans the plugin's view directory and examines the template headers
+ * of each file it finds within.
+ */
+ protected function scan_view_directory() {
+ // If the provided directory is invalid flag the problem and go no further
+ if ( $this->bad_directory( $this->plugin_views_dir ) ) {
+ return;
+ }
+
+ $view_directory = new RecursiveDirectoryIterator( $this->plugin_views_dir );
+ $directory_list = new RecursiveIteratorIterator( $view_directory );
+
+ foreach ( $directory_list as $file ) {
+ $this->scan_view( $file );
+ }
+ }
+
+ /**
+ * Scans an individual view file, adding it's version number (if found) to the
+ * $this->views array.
+ *
+ * @param SplFileInfo $file
+ */
+ protected function scan_view( SplFileInfo $file ) {
+ if ( ! $file->isFile() || ! $file->isReadable() ) {
+ return;
+ }
+
+ $version = $this->get_template_version( $file->getPathname() );
+ $this->originals[ $this->short_name( $file->getPathname() ) ] = $version;
+ }
+
+ protected function scan_for_overrides() {
+ // If the provided directory is invalid flag the problem and go no further
+ if ( $this->bad_directory( $this->theme_views_dir ) ) {
+ return;
+ }
+
+ foreach ( $this->originals as $view_file => $current_version ) {
+ $override_path = trailingslashit( $this->theme_views_dir ) . $view_file;
+
+ if ( ! is_file( $override_path ) || ! is_readable( $override_path ) ) {
+ continue;
+ }
+
+ $this->overrides[ $view_file ] = $this->get_template_version( $override_path );
+ }
+ }
+
+ /**
+ * Tests to ensure the provided view directory path is invalid or unreadable.
+ *
+ * @param string $directory
+ * @return bool
+ */
+ protected function bad_directory( $directory ) {
+ if ( is_dir( $directory ) && is_readable( $directory ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Inspects the template header block within the specified file and extracts the
+ * version number, if one can be found.
+ *
+ * @param string $template_filepath
+ * @return string
+ */
+ protected function get_template_version( $template_filepath ) {
+ if ( ! is_file( $template_filepath ) || ! is_readable( $template_filepath ) ) {
+ return '';
+ }
+
+ $view_content = file_get_contents( $template_filepath );
+
+ if ( ! preg_match( '/^\s*\*\s*@version\s*([0-9\.]+)/mi', $view_content, $matches ) ) {
+ return '';
+ }
+
+ return $matches[1];
+ }
+
+ /**
+ * Given a full filepath (ie, to a view file), chops off the base path found
+ * in $this->plugin_views_dir.
+ *
+ * For example, given:
+ *
+ * $this->plugin_views_dir = '/srv/project/wp-content/plugins/my-plugin/views'
+ * $full_filepath = '/srv/project/wp-content/plugins/my-plugin/views/modules/icon.php'
+ *
+ * Returns:
+ *
+ * 'modules/icon.php'
+ *
+ * @param string $full_filepath
+ * @return string
+ */
+ protected function short_name( $full_filepath ) {
+ if ( 0 === strpos( $full_filepath, $this->plugin_views_dir ) ) {
+ return trim( substr( $full_filepath, strlen( $this->plugin_views_dir ) ), DIRECTORY_SEPARATOR );
+ }
+
+ return $full_filepath;
+ }
+
+ /**
+ * Returns an array of the plugin's shipped view files, where each key is the
+ * view filename and the value is the version it was last updated.
+ *
+ * @return array
+ */
+ public function get_views() {
+ return $this->originals;
+ }
+
+ /**
+ * Returns an array of any or all of the plugin's shipped view files that contain
+ * a version field in their header blocks.
+ *
+ * @see $this->get_views() for format of returned array
+ *
+ * @return array
+ */
+ public function get_versioned_views() {
+ $versioned_views = [];
+
+ foreach ( $this->originals as $key => $version ) {
+ if ( ! empty( $version ) ) {
+ $versioned_views[ $key ] = $version;
+ }
+ }
+
+ return $versioned_views;
+ }
+
+ /**
+ * Returns an array of any shipped plugin views that were updated or introduced
+ * with the current release (as specified by $this->plugin_version).
+ *
+ * @see $this->get_views() for format of returned array
+ *
+ * @return array
+ */
+ public function get_views_tagged_this_release() {
+ $currently_tagged_views = [];
+
+ foreach ( $this->get_versioned_views() as $key => $version ) {
+ if ( $version === $this->plugin_version ) {
+ $currently_tagged_views[ $key ] = $version;
+ }
+ }
+
+ return $currently_tagged_views;
+ }
+
+ /**
+ * Returns an array of theme overrides, where each key is the view filename and the
+ * value is the version it was last updated (may be empty).
+ *
+ * @return array
+ */
+ public function get_overrides() {
+ return $this->overrides;
+ }
+
+ /**
+ * Returns an array of any or all theme overrides that contain a version field in their
+ * header blocks.
+ *
+ * @see $this->get_overrides() for format of returned array
+ *
+ * @return array
+ */
+ public function get_versioned_overrides() {
+ $versioned_views = [];
+
+ foreach ( $this->overrides as $key => $version ) {
+ if ( ! empty( $version ) ) {
+ $versioned_views[ $key ] = $version;
+ }
+ }
+
+ return $versioned_views;
+ }
+
+ /**
+ * Returns an array of any or all theme overrides that seem to be based on an earlier
+ * version than that which currently ships with the plugin.
+ *
+ * If optional param $include_unknown is set to true, the list will include theme
+ * overrides where the version could not be determined (for instance, this might result
+ * in theme overrides where the template header - or version tag - was removed being
+ * included).
+ *
+ * @see $this->get_overrides() for format of returned array
+ *
+ * @param bool $include_unknown = false
+ * @return array
+ */
+ public function get_outdated_overrides( $include_unknown = false ) {
+ $outdated = [];
+ $originals = $this->get_versioned_views();
+
+ $overrides = $include_unknown
+ ? $this->get_overrides()
+ : $this->get_versioned_overrides();
+
+ foreach ( $overrides as $view => $override_version ) {
+ if ( empty( $originals[ $view ] ) ) {
+ continue;
+ }
+
+ $shipped_version = $originals[ $view ];
+
+ if ( version_compare( $shipped_version, $override_version, '>' ) ) {
+ $outdated[ $view ] = $override_version;
+ }
+ }
+
+ return $outdated;
+ }
+}
diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker_Report.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker_Report.php
new file mode 100644
index 000000000..87545f41b
--- /dev/null
+++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker_Report.php
@@ -0,0 +1,119 @@
+ $plugin_template_system ) {
+ self::generate_for( $plugin_name, $plugin_template_system );
+ }
+
+ self::wrap_report();
+ return self::$complete_report;
+ }
+
+ protected static function registered_plugins() {
+ /**
+ * Provides a mechanism for plugins to register information about their template/view
+ * setups.
+ *
+ * This should be done by adding an entry to $registere_template_systems where the key
+ * should be the plugin name and the element an array structured as follows:
+ *
+ * [
+ * plugin_version,
+ * path_to_included_views,
+ * path_to_theme_overrides
+ * ]
+ *
+ * @var array $registered_template_systems
+ */
+ return apply_filters( 'tribe_support_registered_template_systems', [] );
+ }
+
+ /**
+ * Creates a report for the specified plugin.
+ *
+ * @param string $plugin_name
+ * @param array $template_system
+ */
+ protected static function generate_for( $plugin_name, array $template_system ) {
+ $report = '