config = Config::normalize($config); } /** * @return array{range_start: string, range_end: string, empty_range: bool} */ public function queryArgs(): array { $bounds = $this->bounds(); $start = $bounds['start']; $end = $bounds['end']; return [ 'range_start' => $start instanceof DateTime ? $start->format('Y-m-d H:i:s') : '', 'range_end' => $end instanceof DateTime ? $end->format('Y-m-d H:i:s') : '', 'empty_range' => $start instanceof DateTime && $end instanceof DateTime && $start >= $end, ]; } public function coverageTarget(): ?DateTime { $bounds = $this->bounds(); if ($bounds['end'] instanceof DateTime) { return clone $bounds['end']; } if ($bounds['start'] instanceof DateTime) { $target = clone $bounds['start']; $target->modify('+1 year'); return $target; } return null; } /** * @return array{start: ?DateTime, end: ?DateTime} */ private function bounds(): array { $range = isset($this->config['date_range']) && is_array($this->config['date_range']) ? $this->config['date_range'] : []; $tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC'); $utc = new DateTimeZone('UTC'); $start = self::resolveBoundary(isset($range['start']) && is_array($range['start']) ? $range['start'] : [], 'start', $tz); $end = self::resolveBoundary(isset($range['end']) && is_array($range['end']) ? $range['end'] : [], 'end', $tz); if ($start instanceof DateTime) $start->setTimezone($utc); if ($end instanceof DateTime) $end->setTimezone($utc); return [ 'start' => $start, 'end' => $end, ]; } /** * @param mixed[] $boundary */ private static function resolveBoundary(array $boundary, string $side, DateTimeZone $tz): ?DateTime { $mode = sanitize_key((string) ($boundary['mode'] ?? 'none')); if ($mode === 'none') return null; try { $date = self::dateForMode($boundary, $mode, $tz); } catch (Exception $e) { return null; } if (!$date) return null; $date->setTime(0, 0, 0); if ($side === 'end') $date->modify('+1 day'); return $date; } /** * @param mixed[] $boundary */ private static function dateForMode(array $boundary, string $mode, DateTimeZone $tz): ?DateTime { switch ($mode) { case 'today': return new DateTime('today', $tz); case 'tomorrow': return new DateTime('tomorrow', $tz); case 'yesterday': return new DateTime('yesterday', $tz); case 'start_previous_month': return new DateTime('first day of previous month', $tz); case 'start_current_month': return new DateTime('first day of this month', $tz); case 'start_next_month': return new DateTime('first day of next month', $tz); case 'end_previous_month': return new DateTime('last day of previous month', $tz); case 'end_current_month': return new DateTime('last day of this month', $tz); case 'end_next_month': return new DateTime('last day of next month', $tz); case 'fixed': return self::fixedDate((string) ($boundary['date'] ?? ''), $tz); case 'relative': return self::relativeDate($boundary, $tz); } return null; } private static function fixedDate(string $date, DateTimeZone $tz): ?DateTime { if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) return null; $value = DateTime::createFromFormat('!Y-m-d', $date, $tz); $errors = DateTime::getLastErrors(); if (!$value instanceof DateTime) return null; if ($errors !== false && ($errors['warning_count'] || $errors['error_count'])) return null; return $value; } /** * @param mixed[] $boundary */ private static function relativeDate(array $boundary, DateTimeZone $tz): DateTime { $amount = max(0, min(10000, absint($boundary['amount'] ?? 0))); $direction = sanitize_key((string) ($boundary['direction'] ?? 'future')) === 'past' ? '-' : '+'; $unit = sanitize_key((string) ($boundary['unit'] ?? 'days')); $unit = array_key_exists($unit, Registry::dateRangeUnits()) ? $unit : 'days'; $date = new DateTime('today', $tz); if ($amount > 0) { $date->modify($direction . $amount . ' ' . $unit); } return $date; } }