190 lines
5.3 KiB
PHP
190 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\CardDav;
|
|
|
|
class VCardParser
|
|
{
|
|
public function parse(string $contents): ParsedVCard
|
|
{
|
|
$uid = null;
|
|
$fullName = null;
|
|
$firstName = null;
|
|
$lastName = null;
|
|
$organization = null;
|
|
$email = null;
|
|
$phoneNumbers = [];
|
|
$categories = [];
|
|
|
|
foreach ($this->parseLines($contents) as $line) {
|
|
[$name, $params, $value] = $line;
|
|
|
|
match ($name) {
|
|
'UID' => $uid = $uid ?? $this->cleanValue($value),
|
|
'FN' => $fullName = $fullName ?? $this->cleanValue($value),
|
|
'N' => [$lastName, $firstName] = $this->parseStructuredName($value, $lastName, $firstName),
|
|
'ORG' => $organization = $organization ?? $this->cleanValue($this->splitEscaped($value, ';')[0] ?? ''),
|
|
'EMAIL' => $email = $email ?? $this->cleanValue($value),
|
|
'TEL' => $phoneNumbers[] = new ParsedVCardPhoneNumber($this->cleanValue($value), $this->phoneLabel($params)),
|
|
'CATEGORIES' => $categories = array_merge($categories, $this->parseCategories($value)),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
$categories = array_values(array_unique(array_filter($categories)));
|
|
|
|
return new ParsedVCard(
|
|
uid: $uid,
|
|
fullName: $fullName,
|
|
firstName: $firstName,
|
|
lastName: $lastName,
|
|
organization: $organization,
|
|
email: $email,
|
|
phoneNumbers: array_values(array_filter(
|
|
$phoneNumbers,
|
|
fn (ParsedVCardPhoneNumber $phoneNumber): bool => $phoneNumber->rawNumber !== '',
|
|
)),
|
|
categories: $categories,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{0: string, 1: array<string, array<int, string>>, 2: string}>
|
|
*/
|
|
private function parseLines(string $contents): array
|
|
{
|
|
$contents = str_replace(["\r\n", "\r"], "\n", $contents);
|
|
$contents = preg_replace("/\n[ \t]/", '', $contents) ?? $contents;
|
|
$lines = [];
|
|
|
|
foreach (explode("\n", $contents) as $line) {
|
|
$line = trim($line);
|
|
|
|
if ($line === '' || ! str_contains($line, ':')) {
|
|
continue;
|
|
}
|
|
|
|
[$left, $value] = explode(':', $line, 2);
|
|
$propertyParts = explode(';', $left);
|
|
$name = strtoupper((string) array_shift($propertyParts));
|
|
|
|
if ($name === 'BEGIN' || $name === 'END' || $name === 'VERSION') {
|
|
continue;
|
|
}
|
|
|
|
$params = [];
|
|
|
|
foreach ($propertyParts as $part) {
|
|
if (str_contains($part, '=')) {
|
|
[$paramName, $paramValue] = explode('=', $part, 2);
|
|
$paramName = strtoupper($paramName);
|
|
$params[$paramName] = array_merge(
|
|
$params[$paramName] ?? [],
|
|
array_map('strtoupper', $this->splitEscaped($paramValue, ',')),
|
|
);
|
|
|
|
continue;
|
|
}
|
|
|
|
$params['TYPE'][] = strtoupper($part);
|
|
}
|
|
|
|
$lines[] = [$name, $params, $value];
|
|
}
|
|
|
|
return $lines;
|
|
}
|
|
|
|
/**
|
|
* @return array{0: ?string, 1: ?string}
|
|
*/
|
|
private function parseStructuredName(string $value, ?string $currentLastName, ?string $currentFirstName): array
|
|
{
|
|
$parts = $this->splitEscaped($value, ';');
|
|
$lastName = $this->cleanValue($parts[0] ?? '') ?: $currentLastName;
|
|
$firstName = $this->cleanValue($parts[1] ?? '') ?: $currentFirstName;
|
|
|
|
return [$lastName, $firstName];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function parseCategories(string $value): array
|
|
{
|
|
return array_map(
|
|
fn (string $category): string => $this->cleanValue($category),
|
|
$this->splitEscaped($value, ','),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array<int, string>> $params
|
|
*/
|
|
private function phoneLabel(array $params): ?string
|
|
{
|
|
$types = array_filter($params['TYPE'] ?? []);
|
|
|
|
if ($types === []) {
|
|
return null;
|
|
}
|
|
|
|
return implode(', ', array_map(
|
|
fn (string $type): string => ucfirst(strtolower($type)),
|
|
array_unique($types),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function splitEscaped(string $value, string $separator): array
|
|
{
|
|
$parts = [];
|
|
$buffer = '';
|
|
$escaped = false;
|
|
|
|
foreach (str_split($value) as $char) {
|
|
if ($escaped) {
|
|
$buffer .= '\\'.$char;
|
|
$escaped = false;
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($char === '\\') {
|
|
$escaped = true;
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($char === $separator) {
|
|
$parts[] = $buffer;
|
|
$buffer = '';
|
|
|
|
continue;
|
|
}
|
|
|
|
$buffer .= $char;
|
|
}
|
|
|
|
if ($escaped) {
|
|
$buffer .= '\\';
|
|
}
|
|
|
|
$parts[] = $buffer;
|
|
|
|
return $parts;
|
|
}
|
|
|
|
private function cleanValue(string $value): string
|
|
{
|
|
$value = str_replace(
|
|
['\n', '\N', '\;', '\,', '\\\\'],
|
|
["\n", "\n", ';', ',', '\\'],
|
|
trim($value),
|
|
);
|
|
|
|
return trim($value);
|
|
}
|
|
}
|