$rawMeta * @return array{startDate?: string, endDate?: string, metaData?: array} */ public function map(array $rawMeta): array { $result = []; foreach (Options::getOrderItemFieldMappings() as $mapping) { if (! $mapping['enabled']) { continue; } $sourceKey = $mapping['source_key']; if (! array_key_exists($sourceKey, $rawMeta)) { continue; } if (MetaSanitizer::isSensitiveKey($sourceKey)) { continue; } try { $value = $this->transform( MetaSanitizer::sanitizeValue($rawMeta[$sourceKey], Options::getInt(Options::MAX_META_VALUE_LENGTH) ?: 1000), $mapping['transform'] ); } catch (\InvalidArgumentException $exception) { Logger::warning('Order item field mapping skipped because the value could not be transformed.', [ 'source_key' => $sourceKey, 'target' => $mapping['target'], 'transform' => $mapping['transform'], 'message' => $exception->getMessage(), ]); continue; } if (null === $value || '' === $value) { continue; } $this->assign($result, $mapping['target'], $value); } return $result; } /** * @return array */ public static function transformLabels(): array { return [ 'text' => __('Text', 'aivie-for-woocommerce'), 'integer' => __('Integer', 'aivie-for-woocommerce'), 'number' => __('Decimal number', 'aivie-for-woocommerce'), 'boolean_yes_no' => __('Yes / no boolean', 'aivie-for-woocommerce'), 'unix_datetime' => __('Unix timestamp to date-time', 'aivie-for-woocommerce'), 'unix_range_start_datetime' => __('Unix timestamp range first value to date-time', 'aivie-for-woocommerce'), 'unix_range_end_datetime' => __('Unix timestamp range second value to date-time', 'aivie-for-woocommerce'), 'local_datetime_text' => __('Date text to date-time', 'aivie-for-woocommerce'), ]; } /** * @return array */ public static function transformDescriptions(): array { return [ 'text' => __('Sends the value as plain text.', 'aivie-for-woocommerce'), 'integer' => __('Sends a whole number, for example 10864.', 'aivie-for-woocommerce'), 'number' => __('Sends a decimal number. Commas are accepted as decimal separators.', 'aivie-for-woocommerce'), 'boolean_yes_no' => __('Turns yes/no, true/false, 1/0, or on/off into a boolean.', 'aivie-for-woocommerce'), 'unix_datetime' => __('Turns a Unix timestamp into an ISO 8601 date-time in UTC.', 'aivie-for-woocommerce'), 'unix_range_start_datetime' => __('Uses the first timestamp from a colon-separated range and sends it as an ISO 8601 date-time in UTC.', 'aivie-for-woocommerce'), 'unix_range_end_datetime' => __('Uses the second timestamp from a colon-separated range and sends it as an ISO 8601 date-time in UTC.', 'aivie-for-woocommerce'), 'local_datetime_text' => __('Parses readable date text in the WordPress timezone and sends it as an ISO 8601 date-time.', 'aivie-for-woocommerce'), ]; } public static function isValidTransform(string $transform): bool { return array_key_exists($transform, self::transformLabels()); } public static function isValidTarget(string $target): bool { if (self::TARGET_START_DATE === $target || self::TARGET_END_DATE === $target) { return true; } if (! str_starts_with($target, 'meta_data:')) { return false; } return 1 === preg_match('/^meta_data:[a-z][a-z0-9_]*$/', $target); } public static function targetForStandardAlias(string $alias): ?string { return match ($alias) { 'startDate' => self::TARGET_START_DATE, 'endDate' => self::TARGET_END_DATE, default => null, }; } public function previewTransform(mixed $value, string $transform): string { if (! self::isValidTransform($transform)) { return ''; } try { $transformed = $this->transform( MetaSanitizer::sanitizeValue($value, Options::getInt(Options::MAX_META_VALUE_LENGTH) ?: 1000), $transform ); } catch (\InvalidArgumentException) { return ''; } if (is_bool($transformed)) { return $transformed ? 'true' : 'false'; } if (is_scalar($transformed) || null === $transformed) { return (string) $transformed; } $encoded = wp_json_encode($transformed); return is_string($encoded) ? $encoded : ''; } /** * @param array{startDate?: string, endDate?: string, metaData?: array} $result */ private function assign(array &$result, string $target, mixed $value): void { if (self::TARGET_START_DATE === $target) { $result['startDate'] = (string) $value; return; } if (self::TARGET_END_DATE === $target) { $result['endDate'] = (string) $value; return; } if (str_starts_with($target, 'meta_data:')) { $alias = substr($target, strlen('meta_data:')); if ('' !== $alias && 1 === preg_match('/^[a-z][a-z0-9_]*$/', $alias)) { $result['metaData'][$alias] = $value; } } } private function transform(mixed $value, string $transform): mixed { return match ($transform) { 'text' => $this->toText($value), 'integer' => $this->toInteger($value), 'number' => $this->toNumber($value), 'boolean_yes_no' => $this->toBoolean($value), 'unix_datetime' => $this->unixTimestampToDateTime($this->toText($value)), 'unix_range_start_datetime' => $this->unixTimestampToDateTime($this->unixRangePart($value, 0)), 'unix_range_end_datetime' => $this->unixTimestampToDateTime($this->unixRangePart($value, 1)), 'local_datetime_text' => $this->localDateTimeTextToDateTime($this->toText($value)), default => throw new \InvalidArgumentException("Unknown transform: {$transform}"), }; } private function toText(mixed $value): string { if (is_scalar($value) || null === $value) { return trim((string) $value); } return trim((string) wp_json_encode($value)); } private function toInteger(mixed $value): int { $text = $this->toText($value); if ('' === $text || 1 !== preg_match('/^-?\d+$/', $text)) { throw new \InvalidArgumentException('Value is not an integer.'); } return (int) $text; } private function toNumber(mixed $value): float { $text = str_replace(',', '.', $this->toText($value)); if ('' === $text || ! is_numeric($text)) { throw new \InvalidArgumentException('Value is not a number.'); } return (float) $text; } private function toBoolean(mixed $value): bool { $text = strtolower($this->toText($value)); if (in_array($text, ['1', 'true', 'yes', 'y', 'on'], true)) { return true; } if (in_array($text, ['0', 'false', 'no', 'n', 'off'], true)) { return false; } throw new \InvalidArgumentException('Value is not a yes/no boolean.'); } private function unixRangePart(mixed $value, int $index): string { $parts = explode(':', $this->toText($value)); $part = trim($parts[$index] ?? ''); if ('' === $part) { throw new \InvalidArgumentException('Unix timestamp range part is missing.'); } return $part; } private function unixTimestampToDateTime(string $timestamp): string { if (1 !== preg_match('/^-?\d+$/', trim($timestamp))) { throw new \InvalidArgumentException('Value is not a Unix timestamp.'); } return (new \DateTimeImmutable('@' . trim($timestamp))) ->setTimezone(new \DateTimeZone('UTC')) ->format(\DateTime::ATOM); } private function localDateTimeTextToDateTime(string $value): string { if ('' === $value) { throw new \InvalidArgumentException('Date text is empty.'); } $timezone = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone(function_exists('wp_timezone_string') ? wp_timezone_string() : 'UTC'); try { return (new \DateTimeImmutable($value, $timezone))->format(\DateTime::ATOM); } catch (\Exception $exception) { throw new \InvalidArgumentException('Value is not a parseable date/time.', 0, $exception); } } }