$posts * @return list<\WP_Post> */ public function primeEntities(array $posts, \WP_Query $query): array { if ($query->get('pcwp_event_query')) { WpEventRepository::primeEntities(array_map(static fn (\WP_Post $post): int => $post->ID, $posts)); } return $posts; } public function eventArchive(\WP_Query $query): void { if (is_admin() || ! $query->is_main_query() || ! $query->is_post_type_archive('pcwp_event')) { return; } foreach (EventQuerySpec::from(['limit' => (int) get_option('posts_per_page', 10)])->toWpQueryArgs() as $key => $value) { $query->set($key, $value); } } /** * @param array $query * @return array */ public function queryLoopArgs(array $query, \WP_Block $block, int $page): array { unset($page); $attrs = $block->parsed_block['attrs'] ?? []; // Core invokes this hook with Post Template / pagination children. Their // parent Query's settings are provided through context, not their attrs. $contextQuery = is_array($block->context['query'] ?? null) ? $block->context['query'] : []; $isEventContext = ($contextQuery['postType'] ?? '') === 'pcwp_event' && array_key_exists('pcwpInstance', $contextQuery); if (($attrs['namespace'] ?? '') !== self::NAMESPACE && ! $isEventContext) { return $query; } $blockQuery = is_array($attrs['query'] ?? null) ? $attrs['query'] : $contextQuery; if (($blockQuery['pcwpRelatedTo'] ?? '') === 'current') { $blockQuery['pcwpRelatedTo'] = get_queried_object_id() ?: get_the_ID(); } $spec = EventQuerySpec::from($blockQuery); $instance = self::sanitizeInstance((string) ($blockQuery['pcwpInstance'] ?? $attrs['queryId'] ?? $block->context['queryId'] ?? 'events')); $spec = $spec->withOverrides(self::requestOverrides($instance)); $args = $spec->toWpQueryArgs(); if (isset($args['post__in']) && ! empty($query['post__not_in']) && is_array($query['post__not_in'])) { $args['post__in'] = array_values(array_diff($args['post__in'], array_map('absint', $query['post__not_in']))) ?: [0]; $args['post__not_in'] = []; } $args['paged'] = $query['paged'] ?? 1; $args['offset'] = $query['offset'] ?? 0; return array_merge($query, $args); } /** * @param array $params * @return array */ public function restParams(array $params): array { if (isset($params['orderby']['enum']) && is_array($params['orderby']['enum'])) { $params['orderby']['enum'][] = 'pcwp_start'; $params['orderby']['enum'] = array_values(array_unique($params['orderby']['enum'])); } $strings = [ 'pcwpStart', 'pcwpEnd', 'pcwpStatus', 'pcwpMode', 'pcwpCategory', 'pcwpTag', 'pcwpKeyword', 'pcwpTimeFrom', 'pcwpTimeTo', 'pcwpInstance', ]; foreach ($strings as $name) { $params[$name] = ['type' => 'string', 'required' => false]; } foreach (['pcwpVenue', 'pcwpOrganizer', 'pcwpSeries', 'pcwpRelatedTo', 'pcwpWeekday'] as $name) { $params[$name] = ['type' => 'integer', 'required' => false]; } foreach (['pcwpMinPrice', 'pcwpMaxPrice', 'pcwpLatitude', 'pcwpLongitude', 'pcwpRadius'] as $name) { $params[$name] = ['type' => 'number', 'required' => false]; } foreach (['pcwpIncludePast', 'pcwpFeatured'] as $name) { $params[$name] = ['type' => 'boolean', 'required' => false]; } $params['pcwpTaxonomies'] = ['type' => 'object', 'required' => false]; foreach (['pcwpIncludeEvents', 'pcwpExcludeEvents'] as $name) { $params[$name] = ['type' => 'array', 'items' => ['type' => 'integer'], 'required' => false]; } return $params; } /** * @param array $args * @return array */ public function restQuery(array $args, \WP_REST_Request $request): array { $input = []; foreach ($this->restParams([]) as $name => $schema) { unset($schema); if ($request->has_param($name)) { $input[$name] = $request->get_param($name); } } if ($input === []) { return $args; } return array_merge($args, EventQuerySpec::from($input)->toWpQueryArgs()); } /** * @param array $clauses * @return array */ public function clauses(array $clauses, \WP_Query $query): array { if (! $query->get('pcwp_event_query')) { return $clauses; } global $wpdb; $table = $wpdb->prefix . 'pcwp_occurrences'; $alias = 'pcwp_o'; if (! str_contains($clauses['join'], " {$alias}")) { $clauses['join'] .= " INNER JOIN {$table} {$alias} ON {$alias}.event_id = {$wpdb->posts}.ID"; } $where = []; if (! is_admin()) { $where[] = "NOT EXISTS (SELECT 1 FROM {$wpdb->posts} parent WHERE parent.ID={$alias}.series_id AND (parent.post_status<>'publish' OR parent.post_password<>''))"; } $params = []; $start = (string) $query->get('start'); if ($start === '' && ! $query->get('past_events')) { $start = gmdate(DATE_ATOM); } if ($start !== '') { $where[] = "({$alias}.schedule_type='daily' OR {$alias}.end_utc >= %s)"; $params[] = $this->utcSql($start); } if (($end = (string) $query->get('end')) !== '') { $where[] = "({$alias}.schedule_type='daily' OR {$alias}.start_utc <= %s)"; $params[] = $this->utcSql($end); } foreach (['status' => 'event_status', 'mode' => 'event_mode'] as $queryVar => $column) { if (($value = (string) $query->get($queryVar)) !== '') { $where[] = "{$alias}.{$column} = %s"; $params[] = $value; } } if ($query->get('featured')) { $where[] = "{$alias}.featured = 1"; } if (($series = absint($query->get('series'))) > 0) { $where[] = "{$alias}.series_id = %d"; $params[] = $series; } if (($related = absint($query->get('related_to'))) > 0) { $where[] = "{$wpdb->posts}.ID <> %d"; $params[] = $related; $where[] = "EXISTS (SELECT 1 FROM {$wpdb->term_relationships} pcwp_r1 INNER JOIN {$wpdb->term_taxonomy} pcwp_rt1 ON pcwp_rt1.term_taxonomy_id=pcwp_r1.term_taxonomy_id INNER JOIN {$wpdb->term_relationships} pcwp_r2 ON pcwp_r2.term_taxonomy_id=pcwp_r1.term_taxonomy_id WHERE pcwp_r1.object_id=%d AND pcwp_r2.object_id={$alias}.event_id AND pcwp_rt1.taxonomy IN ('pcwp_event_category','pcwp_event_tag'))"; $params[] = $related; } foreach (['venue' => 'venue', 'organizer' => 'organizer'] as $queryVar => $entityType) { if (($entityId = absint($query->get($queryVar))) > 0) { $where[] = "EXISTS (SELECT 1 FROM {$wpdb->prefix}pcwp_event_entities pcwp_ee WHERE pcwp_ee.event_id={$alias}.event_id AND pcwp_ee.entity_type=%s AND pcwp_ee.entity_id=%d)"; $params[] = $entityType; $params[] = $entityId; } } if (($min = $query->get('min_price')) !== null && $min !== '') { $where[] = "{$alias}.cost_decimal >= %f"; $params[] = (float) $min; } if (($max = $query->get('max_price')) !== null && $max !== '') { $where[] = "{$alias}.cost_decimal <= %f"; $params[] = (float) $max; } if (($weekday = (int) $query->get('weekday')) >= 0 && $weekday <= 6) { $where[] = "({$alias}.schedule_type='daily' OR WEEKDAY({$alias}.start_local) = %d)"; $params[] = $weekday; } if (($time = (string) $query->get('time_from')) !== '') { $where[] = "({$alias}.schedule_type='daily' OR TIME({$alias}.start_local) >= %s)"; $params[] = $time . ':00'; } if (($time = (string) $query->get('time_to')) !== '') { $where[] = "({$alias}.schedule_type='daily' OR TIME({$alias}.start_local) <= %s)"; $params[] = $time . ':59'; } $latitude = $query->get('latitude'); $longitude = $query->get('longitude'); $radius = $query->get('radius'); if (is_numeric($latitude) && is_numeric($longitude) && is_numeric($radius) && (float) $radius > 0) { $latDelta = (float) $radius / 111.0; $cosine = max(0.01, cos(deg2rad((float) $latitude))); $lonDelta = (float) $radius / (111.0 * $cosine); $where[] = "{$alias}.latitude BETWEEN %f AND %f"; $params[] = (float) $latitude - $latDelta; $params[] = (float) $latitude + $latDelta; $where[] = "{$alias}.longitude BETWEEN %f AND %f"; $params[] = (float) $longitude - $lonDelta; $params[] = (float) $longitude + $lonDelta; $where[] = "(6371 * 2 * ASIN(SQRT(POWER(SIN(RADIANS({$alias}.latitude - %f) / 2), 2) + COS(RADIANS(%f)) * COS(RADIANS({$alias}.latitude)) * POWER(SIN(RADIANS({$alias}.longitude - %f) / 2), 2)))) <= %f"; $params[] = (float) $latitude; $params[] = (float) $latitude; $params[] = (float) $longitude; $params[] = (float) $radius; } if ($where !== []) { $predicate = implode(' AND ', $where); $clauses['where'] .= ' AND ' . ($params !== [] ? $wpdb->prepare($predicate, ...$params) : $predicate); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static clauses contain no values; dynamic values are bound. } $matchingStart = \ProCalendarWP\Events\ScheduleQuery::matchingStart($alias, [ 'start' => $start, 'end' => $query->get('end'), 'weekday' => $query->get('weekday'), 'time_from' => $query->get('time_from'), 'time_to' => $query->get('time_to'), 'schedule_local_start' => $query->get('schedule_local_start'), 'schedule_local_end' => $query->get('schedule_local_end'), ]); $clauses['where'] .= " AND ({$alias}.schedule_type<>'daily' OR {$matchingStart} IS NOT NULL)"; $clauses['orderby'] = "CASE WHEN {$alias}.schedule_type='daily' THEN {$matchingStart} ELSE {$alias}.start_utc END " . (strtoupper((string) $query->get('order')) === 'DESC' ? 'DESC' : 'ASC') . ", {$wpdb->posts}.ID ASC"; return apply_filters('pcwp_event_query_clauses', $clauses, $query); } /** @param array $block */ public function decorateQueryBlock(string $content, array $block): string { if (($block['attrs']['namespace'] ?? '') !== self::NAMESPACE || ! class_exists('\WP_HTML_Tag_Processor')) { return $content; } $query = is_array($block['attrs']['query'] ?? null) ? $block['attrs']['query'] : []; $instance = self::sanitizeInstance((string) ($query['pcwpInstance'] ?? $block['attrs']['queryId'] ?? 'events')); [$placement, $width] = $this->filterLayout($block['innerBlocks'] ?? []); $processor = new \WP_HTML_Tag_Processor($content); if ($processor->next_tag()) { $processor->add_class('pcwp-filter-layout--' . $placement); $processor->add_class('pcwp-filter-sidebar--' . $width); $processor->set_attribute('data-wp-interactive', 'pro-calendar-wp/filters'); $processor->set_attribute('data-wp-router-region', 'pcwp-' . $instance); $processor->set_attribute('data-pcwp-instance', $instance); $processor->set_attribute('role', 'region'); $processor->set_attribute('aria-live', 'polite'); $processor->set_attribute('aria-label', __('Event results', 'proeventswp')); $content = $processor->get_updated_html(); } return $content; } /** @return array */ public static function requestOverrides(string $instance): array { $result = []; foreach ( [ 'keyword', 'start', 'end', 'category', 'tag', 'status', 'mode', 'venue', 'organizer', 'series', 'related_to', 'featured', 'min_price', 'max_price', 'weekday', 'time_from', 'time_to', 'include_past', 'latitude', 'longitude', 'radius', ] as $key ) { $name = self::parameterName($instance, $key); if (isset($_GET[$name]) && ! is_array($_GET[$name])) { $result[$key] = sanitize_text_field(wp_unslash($_GET[$name])); } } $taxonomies = []; $prefix = self::parameterName($instance, 'tax_'); foreach ($_GET as $parameter => $raw) { if (! is_string($parameter) || ! str_starts_with($parameter, $prefix)) { continue; } $taxonomy = sanitize_key(substr($parameter, strlen($prefix))); if ($taxonomy === '') { continue; } $rawTerms = is_array($raw) ? $raw : [$raw]; $terms = []; foreach ($rawTerms as $term) { if (is_scalar($term)) { $slug = sanitize_title(wp_unslash((string) $term)); if ($slug !== '') { $terms[] = $slug; } } } if ($terms === []) { continue; } $operatorName = self::parameterName($instance, 'taxop_' . $taxonomy); $operator = isset($_GET[$operatorName]) && ! is_array($_GET[$operatorName]) ? strtoupper(sanitize_text_field(wp_unslash($_GET[$operatorName]))) : 'OR'; $taxonomies[$taxonomy] = [ 'terms' => array_values(array_unique($terms)), 'operator' => $operator === 'AND' ? 'AND' : 'OR', ]; } if ($taxonomies !== []) { $result['taxonomies'] = $taxonomies; } $custom = []; $prefix = self::parameterName($instance, 'custom_'); foreach ($_GET as $parameter => $raw) { if (! is_string($parameter) || ! str_starts_with($parameter, $prefix) || is_array($raw)) { continue; } $field = sanitize_key(substr($parameter, strlen($prefix))); if ($field !== '') { $custom[$field] = sanitize_text_field(wp_unslash($raw)); } } if ($custom !== []) { $result['custom_fields'] = $custom; } return $result; } /** * @param array> $blocks * @return array{0:string,1:string} */ private function filterLayout(array $blocks): array { foreach ($blocks as $block) { if (($block['blockName'] ?? '') === 'pro-calendar-wp/event-filters') { $attrs = is_array($block['attrs'] ?? null) ? $block['attrs'] : []; $placement = in_array($attrs['placement'] ?? '', ['left', 'right'], true) ? (string) $attrs['placement'] : 'horizontal'; $width = in_array($attrs['sidebarWidth'] ?? '', ['compact', 'wide'], true) ? (string) $attrs['sidebarWidth'] : 'standard'; return [$placement, $width]; } } return ['horizontal', 'standard']; } public static function sanitizeInstance(string $value): string { $value = strtolower((string) preg_replace('/[^a-zA-Z0-9_-]/', '', $value)); return $value !== '' ? substr($value, 0, 32) : 'events'; } public static function parameterName(string $instance, string $field): string { return 'pcwp_' . self::sanitizeInstance($instance) . '_' . sanitize_key($field); } private function utcSql(string $value): string { try { return (new \DateTimeImmutable($value))->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); } catch (\Exception) { return gmdate('Y-m-d H:i:s'); } } }