Publication initiale de RelaisSMS : documentation et sources nettoyees
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\CardDav;
|
||||
|
||||
use App\Contracts\CardDav\CardDavClientInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use RuntimeException;
|
||||
use SimpleXMLElement;
|
||||
use Throwable;
|
||||
|
||||
class CardDavClient implements CardDavClientInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClientInterface $http,
|
||||
) {}
|
||||
|
||||
public function fetchCards(): array
|
||||
{
|
||||
try {
|
||||
$cards = $this->fetchCardsWithReport();
|
||||
|
||||
if ($cards !== []) {
|
||||
return $cards;
|
||||
}
|
||||
} catch (GuzzleException|RuntimeException) {
|
||||
// Some CardDAV servers restrict addressbook-query. Fall back to PROPFIND + GET.
|
||||
}
|
||||
|
||||
return $this->fetchCardsWithPropfind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, CardDavRemoteCard>
|
||||
*/
|
||||
private function fetchCardsWithReport(): array
|
||||
{
|
||||
$url = rtrim((string) config('relais_sms.carddav.url'), '/').'/';
|
||||
$username = (string) config('relais_sms.carddav.username');
|
||||
$password = (string) config('relais_sms.carddav.password');
|
||||
|
||||
$this->ensureConfigured($url, $username, $password);
|
||||
|
||||
$response = $this->http->request('REPORT', $url, [
|
||||
'auth' => [$username, $password],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/xml; charset=utf-8',
|
||||
'Depth' => '1',
|
||||
],
|
||||
'body' => <<<'XML'
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav">
|
||||
<d:prop>
|
||||
<d:getetag />
|
||||
<c:address-data />
|
||||
</d:prop>
|
||||
</c:addressbook-query>
|
||||
XML,
|
||||
'timeout' => (float) config('relais_sms.carddav.timeout'),
|
||||
]);
|
||||
|
||||
return $this->parseReport((string) $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, CardDavRemoteCard>
|
||||
*/
|
||||
private function fetchCardsWithPropfind(): array
|
||||
{
|
||||
$url = rtrim((string) config('relais_sms.carddav.url'), '/').'/';
|
||||
$username = (string) config('relais_sms.carddav.username');
|
||||
$password = (string) config('relais_sms.carddav.password');
|
||||
|
||||
$this->ensureConfigured($url, $username, $password);
|
||||
|
||||
$response = $this->http->request('PROPFIND', $url, [
|
||||
'auth' => [$username, $password],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/xml; charset=utf-8',
|
||||
'Depth' => '1',
|
||||
],
|
||||
'body' => <<<'XML'
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<d:propfind xmlns:d="DAV:">
|
||||
<d:prop>
|
||||
<d:getetag />
|
||||
</d:prop>
|
||||
</d:propfind>
|
||||
XML,
|
||||
'timeout' => (float) config('relais_sms.carddav.timeout'),
|
||||
]);
|
||||
|
||||
$entries = $this->parsePropfind((string) $response->getBody(), $url);
|
||||
$cards = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$cardResponse = $this->http->request('GET', $entry['url'], [
|
||||
'auth' => [$username, $password],
|
||||
'headers' => [
|
||||
'Accept' => 'text/vcard,text/x-vcard,text/plain,*/*',
|
||||
],
|
||||
'timeout' => (float) config('relais_sms.carddav.timeout'),
|
||||
]);
|
||||
|
||||
$cards[] = new CardDavRemoteCard(
|
||||
href: $entry['href'],
|
||||
etag: $entry['etag'],
|
||||
vcard: (string) $cardResponse->getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
return $cards;
|
||||
}
|
||||
|
||||
private function ensureConfigured(string $url, string $username, string $password): void
|
||||
{
|
||||
if ($url === '/' || $username === '' || $password === '') {
|
||||
throw new RuntimeException('Configuration CardDAV incomplete.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, CardDavRemoteCard>
|
||||
*/
|
||||
private function parseReport(string $xml): array
|
||||
{
|
||||
try {
|
||||
$document = new SimpleXMLElement($xml);
|
||||
} catch (Throwable $exception) {
|
||||
throw new RuntimeException('Reponse CardDAV REPORT illisible.', previous: $exception);
|
||||
}
|
||||
|
||||
$document->registerXPathNamespace('d', 'DAV:');
|
||||
$responses = $document->xpath('//d:response') ?: [];
|
||||
$cards = [];
|
||||
|
||||
foreach ($responses as $response) {
|
||||
$response->registerXPathNamespace('d', 'DAV:');
|
||||
$response->registerXPathNamespace('c', 'urn:ietf:params:xml:ns:carddav');
|
||||
|
||||
$hrefNodes = $response->xpath('./d:href') ?: [];
|
||||
$cardNodes = $response->xpath('.//c:address-data') ?: [];
|
||||
$href = trim((string) ($hrefNodes[0] ?? ''));
|
||||
$vcard = (string) ($cardNodes[0] ?? '');
|
||||
|
||||
if ($href === '' || trim($vcard) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$etagNodes = $response->xpath('.//d:getetag') ?: [];
|
||||
$cards[] = new CardDavRemoteCard(
|
||||
href: $href,
|
||||
etag: trim((string) ($etagNodes[0] ?? ''), '"') ?: null,
|
||||
vcard: $vcard,
|
||||
);
|
||||
}
|
||||
|
||||
return $cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{href: string, etag: ?string, url: string}>
|
||||
*/
|
||||
private function parsePropfind(string $xml, string $baseUrl): array
|
||||
{
|
||||
try {
|
||||
$document = new SimpleXMLElement($xml);
|
||||
} catch (Throwable $exception) {
|
||||
throw new RuntimeException('Reponse CardDAV PROPFIND illisible.', previous: $exception);
|
||||
}
|
||||
|
||||
$document->registerXPathNamespace('d', 'DAV:');
|
||||
$responses = $document->xpath('//d:response') ?: [];
|
||||
$entries = [];
|
||||
|
||||
foreach ($responses as $response) {
|
||||
$response->registerXPathNamespace('d', 'DAV:');
|
||||
$hrefNodes = $response->xpath('./d:href') ?: [];
|
||||
$href = trim((string) ($hrefNodes[0] ?? ''));
|
||||
|
||||
if ($href === '' || ! str_ends_with(strtolower(parse_url($href, PHP_URL_PATH) ?: $href), '.vcf')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$etagNodes = $response->xpath('.//d:getetag') ?: [];
|
||||
$entries[] = [
|
||||
'href' => $href,
|
||||
'etag' => trim((string) ($etagNodes[0] ?? ''), '"') ?: null,
|
||||
'url' => $this->absoluteUrl($baseUrl, $href),
|
||||
];
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
private function absoluteUrl(string $baseUrl, string $href): string
|
||||
{
|
||||
if (parse_url($href, PHP_URL_SCHEME)) {
|
||||
return $href;
|
||||
}
|
||||
|
||||
$base = parse_url($baseUrl);
|
||||
|
||||
if (! isset($base['scheme'], $base['host'])) {
|
||||
throw new RuntimeException('URL CardDAV invalide.');
|
||||
}
|
||||
|
||||
$port = isset($base['port']) ? ':'.$base['port'] : '';
|
||||
$path = str_starts_with($href, '/') ? $href : rtrim((string) ($base['path'] ?? ''), '/').'/'.$href;
|
||||
|
||||
return $base['scheme'].'://'.$base['host'].$port.$path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\CardDav;
|
||||
|
||||
use App\Contracts\CardDav\CardDavClientInterface;
|
||||
use App\Contracts\PhoneNumberNormalizerInterface;
|
||||
use App\Models\Contact;
|
||||
use App\Models\ContactGroup;
|
||||
use App\Models\SourceSyncRun;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
class CardDavContactSyncService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CardDavClientInterface $client,
|
||||
private readonly VCardParser $parser,
|
||||
private readonly PhoneNumberNormalizerInterface $phoneNumberNormalizer,
|
||||
) {}
|
||||
|
||||
public function sync(): SourceSyncRun
|
||||
{
|
||||
$run = SourceSyncRun::query()->create([
|
||||
'source' => 'carddav',
|
||||
'status' => 'running',
|
||||
'started_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$cards = $this->client->fetchCards();
|
||||
|
||||
DB::transaction(function () use ($run, $cards): void {
|
||||
$seenContactExternalIds = [];
|
||||
$seenGroupExternalIds = [];
|
||||
$syncedContacts = 0;
|
||||
|
||||
foreach ($cards as $card) {
|
||||
$parsedCard = $this->parser->parse($card->vcard);
|
||||
$externalId = $this->externalId($parsedCard, $card);
|
||||
$seenContactExternalIds[] = $externalId;
|
||||
|
||||
$contact = $this->upsertContact($externalId, $card, $parsedCard);
|
||||
$this->replacePhoneNumbers($contact, $parsedCard);
|
||||
|
||||
$groupIds = [];
|
||||
|
||||
foreach ($parsedCard->categories as $category) {
|
||||
$group = $this->upsertGroup($category);
|
||||
$groupIds[] = $group->id;
|
||||
$seenGroupExternalIds[] = (string) $group->external_id;
|
||||
}
|
||||
|
||||
$this->syncCardDavGroupMemberships($contact, $groupIds);
|
||||
$syncedContacts++;
|
||||
}
|
||||
|
||||
$deletedContacts = $this->deleteMissingContacts($seenContactExternalIds);
|
||||
$this->deleteMissingGroups(array_values(array_unique($seenGroupExternalIds)));
|
||||
|
||||
$run->update([
|
||||
'status' => 'success',
|
||||
'finished_at' => Carbon::now(),
|
||||
'fetched_contacts' => count($cards),
|
||||
'synced_contacts' => $syncedContacts,
|
||||
'deleted_contacts' => $deletedContacts,
|
||||
'message' => $syncedContacts.' contact(s) CardDAV synchronise(s).',
|
||||
]);
|
||||
});
|
||||
} catch (Throwable $exception) {
|
||||
$run->update([
|
||||
'status' => 'failed',
|
||||
'finished_at' => Carbon::now(),
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $run->refresh();
|
||||
}
|
||||
|
||||
private function upsertContact(string $externalId, CardDavRemoteCard $card, ParsedVCard $parsedCard): Contact
|
||||
{
|
||||
$contact = Contact::withTrashed()
|
||||
->where('source', 'carddav')
|
||||
->where('external_id', $externalId)
|
||||
->first();
|
||||
|
||||
$attributes = [
|
||||
'source' => 'carddav',
|
||||
'external_id' => $externalId,
|
||||
'remote_url' => $card->href,
|
||||
'remote_etag' => $card->etag,
|
||||
'first_name' => $parsedCard->firstName,
|
||||
'last_name' => $this->lastName($parsedCard),
|
||||
'organization' => $parsedCard->organization,
|
||||
'email' => $parsedCard->email,
|
||||
'synced_at' => Carbon::now(),
|
||||
];
|
||||
|
||||
if (! $contact) {
|
||||
return Contact::query()->create($attributes);
|
||||
}
|
||||
|
||||
$contact->fill($attributes);
|
||||
$contact->deleted_at = null;
|
||||
$contact->save();
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
private function replacePhoneNumbers(Contact $contact, ParsedVCard $parsedCard): void
|
||||
{
|
||||
$contact->phoneNumbers()->delete();
|
||||
$defaultCountry = (string) config('relais_sms.default_settings.default_country');
|
||||
|
||||
foreach ($parsedCard->phoneNumbers as $index => $phoneNumber) {
|
||||
$result = $this->phoneNumberNormalizer->normalize($phoneNumber->rawNumber, $defaultCountry);
|
||||
|
||||
$contact->phoneNumbers()->create([
|
||||
'label' => $phoneNumber->label,
|
||||
'raw_number' => $result->rawNumber,
|
||||
'normalized_number' => $result->normalizedNumber,
|
||||
'country' => $result->country,
|
||||
'is_valid' => $result->isValid,
|
||||
'is_mobile' => $result->isMobile,
|
||||
'is_primary' => $index === 0,
|
||||
'validation_error' => $result->error,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function upsertGroup(string $category): ContactGroup
|
||||
{
|
||||
$externalId = Str::slug($category);
|
||||
$slug = 'carddav-'.$externalId;
|
||||
$group = ContactGroup::withTrashed()
|
||||
->where('source', 'carddav')
|
||||
->where('external_id', $externalId)
|
||||
->first();
|
||||
|
||||
$attributes = [
|
||||
'source' => 'carddav',
|
||||
'external_id' => $externalId,
|
||||
'name' => $category,
|
||||
'slug' => $slug,
|
||||
'synced_at' => Carbon::now(),
|
||||
];
|
||||
|
||||
if (! $group) {
|
||||
return ContactGroup::query()->create($attributes);
|
||||
}
|
||||
|
||||
$group->fill($attributes);
|
||||
$group->deleted_at = null;
|
||||
$group->save();
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $groupIds
|
||||
*/
|
||||
private function syncCardDavGroupMemberships(Contact $contact, array $groupIds): void
|
||||
{
|
||||
$currentCardDavGroupIds = $contact->groups()
|
||||
->where('contact_groups.source', 'carddav')
|
||||
->pluck('contact_groups.id')
|
||||
->all();
|
||||
|
||||
if ($currentCardDavGroupIds !== []) {
|
||||
$contact->groups()->detach($currentCardDavGroupIds);
|
||||
}
|
||||
|
||||
if ($groupIds !== []) {
|
||||
$contact->groups()->syncWithoutDetaching($groupIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $seenExternalIds
|
||||
*/
|
||||
private function deleteMissingContacts(array $seenExternalIds): int
|
||||
{
|
||||
$contacts = Contact::query()
|
||||
->where('source', 'carddav')
|
||||
->whereNotIn('external_id', $seenExternalIds ?: [''])
|
||||
->get();
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
$cardDavGroupIds = $contact->groups()
|
||||
->where('contact_groups.source', 'carddav')
|
||||
->pluck('contact_groups.id')
|
||||
->all();
|
||||
|
||||
if ($cardDavGroupIds !== []) {
|
||||
$contact->groups()->detach($cardDavGroupIds);
|
||||
}
|
||||
|
||||
$contact->delete();
|
||||
}
|
||||
|
||||
return $contacts->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $seenExternalIds
|
||||
*/
|
||||
private function deleteMissingGroups(array $seenExternalIds): void
|
||||
{
|
||||
ContactGroup::query()
|
||||
->where('source', 'carddav')
|
||||
->whereNotIn('external_id', $seenExternalIds ?: [''])
|
||||
->delete();
|
||||
}
|
||||
|
||||
private function externalId(ParsedVCard $parsedCard, CardDavRemoteCard $card): string
|
||||
{
|
||||
return $parsedCard->uid ?: sha1($card->href);
|
||||
}
|
||||
|
||||
private function lastName(ParsedVCard $parsedCard): string
|
||||
{
|
||||
if ($parsedCard->lastName) {
|
||||
return $parsedCard->lastName;
|
||||
}
|
||||
|
||||
if ($parsedCard->fullName) {
|
||||
return $parsedCard->fullName;
|
||||
}
|
||||
|
||||
if ($parsedCard->organization) {
|
||||
return $parsedCard->organization;
|
||||
}
|
||||
|
||||
return 'Contact CardDAV';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\CardDav;
|
||||
|
||||
readonly class CardDavRemoteCard
|
||||
{
|
||||
public function __construct(
|
||||
public string $href,
|
||||
public ?string $etag,
|
||||
public string $vcard,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\CardDav;
|
||||
|
||||
readonly class ParsedVCard
|
||||
{
|
||||
/**
|
||||
* @param array<int, ParsedVCardPhoneNumber> $phoneNumbers
|
||||
* @param array<int, string> $categories
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $uid,
|
||||
public ?string $fullName,
|
||||
public ?string $firstName,
|
||||
public ?string $lastName,
|
||||
public ?string $organization,
|
||||
public ?string $email,
|
||||
public array $phoneNumbers,
|
||||
public array $categories,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\CardDav;
|
||||
|
||||
readonly class ParsedVCardPhoneNumber
|
||||
{
|
||||
public function __construct(
|
||||
public string $rawNumber,
|
||||
public ?string $label,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user