537 lines
22 KiB
PHP
537 lines
22 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
final class DocumentsService
|
||
{
|
||
private const ADOPTION_TEMPLATE = __DIR__ . '/../../resources/documents/CONTRAT ADOPTION V1.5.odt';
|
||
private const TEMPLATES = [
|
||
'adoption' => self::ADOPTION_TEMPLATE,
|
||
'abandon' => __DIR__ . '/../../resources/documents/ABANDON V1.0.odt',
|
||
'benevole' => __DIR__ . '/../../resources/documents/CONTRAT BENEVOLE V2.0.odt',
|
||
'fa' => __DIR__ . '/../../resources/documents/PROPOSITION FA V1.1.odt',
|
||
'pre_adoption' => __DIR__ . '/../../resources/documents/CERTIFICAT VISITE PRE-ADOPTION V1.1.odt',
|
||
];
|
||
|
||
public static function adoptionTemplate(): string
|
||
{
|
||
if (!is_file(self::ADOPTION_TEMPLATE)) {
|
||
throw new RuntimeException(t('service.document.adoption_missing'));
|
||
}
|
||
return self::ADOPTION_TEMPLATE;
|
||
}
|
||
|
||
public static function template(string $type): string
|
||
{
|
||
$path = self::TEMPLATES[$type] ?? null;
|
||
if (!$path || !is_file($path)) {
|
||
throw new RuntimeException(t('service.document.template_missing'));
|
||
}
|
||
return $path;
|
||
}
|
||
|
||
public static function generateAbandon(array $animal): string
|
||
{
|
||
return self::transformTemplate('abandon', 'abandon-' . self::safeName((string) $animal['name']), function (
|
||
DOMXPath $xp,
|
||
) use ($animal): void {
|
||
$sex = match ($animal['sex'] ?? 'U') {
|
||
'M' => 'Mâle',
|
||
'F' => 'Femelle',
|
||
default => 'Non renseigné',
|
||
};
|
||
$birth = trim((string) ($animal['birth_date'] ?? ''));
|
||
$age = '—';
|
||
if ($birth !== '') {
|
||
$born = new DateTimeImmutable($birth);
|
||
$now = new DateTimeImmutable();
|
||
$diff = $born->diff($now);
|
||
$age = $diff->y > 0 ? $diff->y . ' an' . ($diff->y > 1 ? 's' : '') : $diff->m . ' mois';
|
||
}
|
||
$chip = trim((string) ($animal['chip'] ?: $animal['chip_id'] ?? ''));
|
||
self::replaceParagraph($xp, 'Nom de l’animal', [
|
||
['Nom de l’animal : ', 'label'],
|
||
[(string) $animal['name'], 'value'],
|
||
['tab'],
|
||
['Sexe : ', 'label'],
|
||
[$sex, 'value'],
|
||
]);
|
||
self::replaceParagraph($xp, 'Race :', [
|
||
['Race : ', 'label'],
|
||
[(string) ($animal['breed'] ?? '—'), 'value'],
|
||
['tab'],
|
||
['Âge : ', 'label'],
|
||
[$age, 'value'],
|
||
['break'],
|
||
['Vaccination : ', 'label'],
|
||
['À vérifier', 'value'],
|
||
['tab'],
|
||
['Vermifugation : ', 'label'],
|
||
['À vérifier', 'value'],
|
||
['break'],
|
||
['Numéro de tatouage ou puce électronique : ', 'label'],
|
||
[$chip !== '' ? $chip : '—', 'value'],
|
||
]);
|
||
});
|
||
}
|
||
|
||
public static function generateContact(string $type, array $contact): string
|
||
{
|
||
if (!in_array($type, ['benevole', 'fa'], true)) {
|
||
throw new InvalidArgumentException(t('service.document.invalid_type'));
|
||
}
|
||
return self::transformTemplate($type, $type . '-' . self::safeName((string) $contact['name']), function (
|
||
DOMXPath $xp,
|
||
) use ($type, $contact): void {
|
||
$name = (string) $contact['name'];
|
||
$address = (string) ($contact['address'] ?? '');
|
||
$postal = (string) ($contact['postal_code'] ?? '');
|
||
$city = (string) ($contact['city'] ?? '');
|
||
$phone = (string) ($contact['phone'] ?? '');
|
||
$email = (string) ($contact['email'] ?? '');
|
||
if ($type === 'benevole') {
|
||
self::replaceParagraph(
|
||
$xp,
|
||
'Nom :',
|
||
[['Nom : ', 'label'], [$name, 'value'], ['tab'], ['Prénom : ', 'label'], ['', 'value']],
|
||
0,
|
||
);
|
||
self::replaceParagraph($xp, 'Adresse :', [['Adresse : ', 'label'], [$address, 'value']], 0);
|
||
self::replaceParagraph(
|
||
$xp,
|
||
'Code Postal :',
|
||
[['Code Postal : ', 'label'], [$postal, 'value'], ['tab'], ['Ville : ', 'label'], [$city, 'value']],
|
||
0,
|
||
);
|
||
self::replaceParagraph(
|
||
$xp,
|
||
'Numéro de téléphone',
|
||
[
|
||
['Numéro de téléphone : ', 'label'],
|
||
[$phone, 'value'],
|
||
['tab'],
|
||
['Adresse e-mail : ', 'label'],
|
||
[$email, 'value'],
|
||
],
|
||
0,
|
||
);
|
||
} else {
|
||
self::replaceParagraph($xp, 'NOM / Prénom :', [['NOM / Prénom : ', 'label'], [$name, 'value']], 0);
|
||
self::replaceParagraph($xp, 'Adresse :', [['Adresse : ', 'label'], [$address, 'value']], 0);
|
||
self::replaceParagraph(
|
||
$xp,
|
||
'Code postal :',
|
||
[['Code postal : ', 'label'], [$postal, 'value'], ['tab'], ['Ville : ', 'label'], [$city, 'value']],
|
||
0,
|
||
);
|
||
self::replaceParagraph(
|
||
$xp,
|
||
'Téléphone :',
|
||
[
|
||
['Téléphone : ', 'label'],
|
||
[$phone, 'value'],
|
||
['tab'],
|
||
['Adresse mail : ', 'label'],
|
||
[$email, 'value'],
|
||
],
|
||
0,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
public static function generateAdoption(array $animal, ?array $adoption, ?array $vaccine): string
|
||
{
|
||
$dir = self::tempDir();
|
||
$name = self::safeName((string) $animal['name']);
|
||
$target = $dir . '/contrat-adoption-' . $name . '.odt';
|
||
if (!copy(self::adoptionTemplate(), $target)) {
|
||
throw new RuntimeException(t('service.document.copy_failed'));
|
||
}
|
||
|
||
$zip = new ZipArchive();
|
||
if ($zip->open($target) !== true) {
|
||
throw new RuntimeException(t('service.document.open_failed'));
|
||
}
|
||
$xml = $zip->getFromName('content.xml');
|
||
if ($xml === false) {
|
||
$zip->close();
|
||
throw new RuntimeException(t('service.document.content_invalid'));
|
||
}
|
||
$dom = new DOMDocument();
|
||
$dom->preserveWhiteSpace = true;
|
||
if (!$dom->loadXML($xml)) {
|
||
$zip->close();
|
||
throw new RuntimeException(t('service.document.xml_invalid'));
|
||
}
|
||
$xp = new DOMXPath($dom);
|
||
$xp->registerNamespace('text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
|
||
$xp->registerNamespace('office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
|
||
self::addGeneratedFieldStyles($dom, $xp);
|
||
|
||
$date = static fn(?string $value): string => $value ? date('d/m/Y', strtotime($value)) : '';
|
||
$sex = match ($animal['sex'] ?? 'U') {
|
||
'M' => '☒ Mâle ☐ Femelle',
|
||
'F' => '☐ Mâle ☒ Femelle',
|
||
default => '☐ Mâle ☐ Femelle',
|
||
};
|
||
$origin = trim((string) ($animal['breed'] ?? ''));
|
||
$chip = trim((string) ($animal['chip'] ?: $animal['chip_id'] ?? ''));
|
||
$testStatus =
|
||
'FIV ' .
|
||
self::testLabel((string) ($animal['fiv_status'] ?? 'unknown')) .
|
||
' / FeLV ' .
|
||
self::testLabel((string) ($animal['felv_status'] ?? 'unknown'));
|
||
$signatureDate = $date($adoption['adoption_date'] ?? date('Y-m-d'));
|
||
|
||
self::setSpan($xp, 'T26', (string) $animal['name']);
|
||
self::setLabeledSpan($xp, 'T28', 'Sexe : ', $sex);
|
||
self::setSpan($xp, 'T31', ' ' . $date($animal['birth_date'] ?? null));
|
||
self::setSpan($xp, 'T33', 'Robe : ', null, 'GeneratedField');
|
||
self::setSpan($xp, 'T34', (string) ($animal['color'] ?? ''));
|
||
self::setSpan($xp, 'T41', $origin !== '' ? $origin : '—');
|
||
self::setSpan($xp, 'T42', '');
|
||
self::setLabeledSpan($xp, 'T45', 'Puce électronique N° : ', $chip !== '' ? $chip : '—');
|
||
self::setLabeledSpan(
|
||
$xp,
|
||
'T49',
|
||
' : ',
|
||
(int) ($animal['sterilized'] ?? 0) === 1
|
||
? '☒ Effectuée ☐ À effectuer avant le :'
|
||
: '☐ Effectuée ☒ À effectuer avant le :',
|
||
);
|
||
self::setLabeledSpan($xp, 'T53', 'Date du test FIV-FELV : ', $testStatus, 'tab');
|
||
self::setLabeledSpan($xp, 'T55', 'Date de la dernière vaccination : ', $date($vaccine['done_date'] ?? null));
|
||
self::setLabeledSpan($xp, 'T56', 'Rappel à effectuer le : ', $date($vaccine['due_date'] ?? null), 'tab');
|
||
|
||
self::setSpan($xp, 'T98', (string) ($adoption['adopter_name'] ?? ''));
|
||
self::setSpan($xp, 'T101', (string) ($adoption['adopter_address'] ?? ''));
|
||
self::setSpan($xp, 'T104', (string) ($adoption['adopter_postal_code'] ?? ''));
|
||
self::setSpan($xp, 'T107', (string) ($adoption['adopter_city'] ?? ''));
|
||
self::setSpan($xp, 'T110', '');
|
||
self::setSpan($xp, 'T113', (string) ($adoption['adopter_phone'] ?? ''));
|
||
self::setSpan($xp, 'T116', (string) ($adoption['adopter_email'] ?? ''));
|
||
$signatureCity = AppSettings::get('association_city') ?: '—';
|
||
self::setLabeledSpan($xp, 'T155', 'Fait à : ', $signatureCity, null, 'GeneratedSignature');
|
||
self::setLabeledSpan($xp, 'T156', 'Le : ', $signatureDate, 'tab', 'GeneratedSignature');
|
||
self::setLabeledSpan($xp, 'T247', 'Fait à : ', $signatureCity, null, 'GeneratedSignature');
|
||
self::setLabeledSpan($xp, 'T253', 'Le : ', $signatureDate, 'tab', 'GeneratedSignature');
|
||
self::setSpan($xp, 'T283', (string) $animal['name']);
|
||
self::setLabeledSpan($xp, 'T367', 'Fait à : ', $signatureCity, null, 'GeneratedSignature');
|
||
self::setLabeledSpan($xp, 'T373', 'Le : ', $signatureDate, 'tab', 'GeneratedSignature');
|
||
foreach (
|
||
[
|
||
'T25',
|
||
'T30',
|
||
'T33',
|
||
'T36',
|
||
'T40',
|
||
'T45',
|
||
'T48',
|
||
'T52',
|
||
'T53',
|
||
'T55',
|
||
'T56',
|
||
'T97',
|
||
'T100',
|
||
'T103',
|
||
'T106',
|
||
'T109',
|
||
'T112',
|
||
'T115',
|
||
'T281',
|
||
]
|
||
as $labelStyle
|
||
) {
|
||
self::applySpanStyle($xp, $labelStyle);
|
||
}
|
||
|
||
$updated = $dom->saveXML();
|
||
if ($updated === false || !$zip->addFromString('content.xml', $updated)) {
|
||
$zip->close();
|
||
throw new RuntimeException(t('service.document.update_contract_failed'));
|
||
}
|
||
$zip->close();
|
||
return $target;
|
||
}
|
||
|
||
public static function toPdf(string $odtPath): string
|
||
{
|
||
$dir = dirname($odtPath);
|
||
$profile = $dir . '/lo-profile';
|
||
mkdir($profile, 0775, true);
|
||
$cmd = [
|
||
'/usr/bin/env',
|
||
'-u',
|
||
'DISPLAY',
|
||
'SAL_USE_VCLPLUGIN=svp',
|
||
'/usr/bin/libreoffice',
|
||
'-env:UserInstallation=' . self::fileUri($profile),
|
||
'--headless',
|
||
'--nologo',
|
||
'--nodefault',
|
||
'--nolockcheck',
|
||
'--nofirststartwizard',
|
||
'--convert-to',
|
||
'pdf',
|
||
'--outdir',
|
||
$dir,
|
||
$odtPath,
|
||
];
|
||
$pipes = [];
|
||
$process = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
|
||
if (!is_resource($process)) {
|
||
throw new RuntimeException(t('service.document.libreoffice_failed'));
|
||
}
|
||
$stdout = stream_get_contents($pipes[1]);
|
||
$stderr = stream_get_contents($pipes[2]);
|
||
fclose($pipes[1]);
|
||
fclose($pipes[2]);
|
||
$code = proc_close($process);
|
||
$pdf = $dir . '/' . pathinfo($odtPath, PATHINFO_FILENAME) . '.pdf';
|
||
if ($code !== 0 || !is_file($pdf)) {
|
||
throw new RuntimeException(t('service.document.pdf_failed', ['details' => trim($stdout . ' ' . $stderr)]));
|
||
}
|
||
return $pdf;
|
||
}
|
||
|
||
public static function cleanup(string $path): void
|
||
{
|
||
$dir = dirname($path);
|
||
if (!str_starts_with($dir, sys_get_temp_dir() . '/globinours-doc-')) {
|
||
return;
|
||
}
|
||
$items = new RecursiveIteratorIterator(
|
||
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
|
||
RecursiveIteratorIterator::CHILD_FIRST,
|
||
);
|
||
foreach ($items as $item) {
|
||
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
|
||
}
|
||
rmdir($dir);
|
||
}
|
||
|
||
private static function setSpan(
|
||
DOMXPath $xp,
|
||
string $style,
|
||
string $value,
|
||
?string $prefix = null,
|
||
string $generatedStyle = 'GeneratedValue',
|
||
): void {
|
||
$nodes = $xp->query("//text:span[@text:style-name='$style']");
|
||
if (!$nodes || $nodes->length === 0) {
|
||
return;
|
||
}
|
||
$node = $nodes->item(0);
|
||
while ($node->firstChild) {
|
||
$node->removeChild($node->firstChild);
|
||
}
|
||
$node->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:style-name', $generatedStyle);
|
||
if ($prefix === 'tab') {
|
||
$node->appendChild(
|
||
$node->ownerDocument->createElementNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:tab'),
|
||
);
|
||
}
|
||
$node->appendChild($node->ownerDocument->createTextNode($value));
|
||
}
|
||
|
||
private static function setLabeledSpan(
|
||
DOMXPath $xp,
|
||
string $style,
|
||
string $label,
|
||
string $value,
|
||
?string $prefix = null,
|
||
string $labelStyle = 'GeneratedField',
|
||
): void {
|
||
$nodes = $xp->query("//text:span[@text:style-name='$style']");
|
||
if (!$nodes || $nodes->length === 0) {
|
||
return;
|
||
}
|
||
$node = $nodes->item(0);
|
||
while ($node->firstChild) {
|
||
$node->removeChild($node->firstChild);
|
||
}
|
||
$node->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:style-name', $labelStyle);
|
||
if ($prefix === 'tab') {
|
||
$node->appendChild(
|
||
$node->ownerDocument->createElementNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:tab'),
|
||
);
|
||
}
|
||
$node->appendChild($node->ownerDocument->createTextNode($label));
|
||
$valueSpan = $node->ownerDocument->createElementNS(
|
||
'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
|
||
'text:span',
|
||
);
|
||
$valueSpan->setAttributeNS(
|
||
'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
|
||
'text:style-name',
|
||
'GeneratedValue',
|
||
);
|
||
$valueSpan->appendChild($node->ownerDocument->createTextNode($value));
|
||
$node->appendChild($valueSpan);
|
||
}
|
||
|
||
private static function applySpanStyle(DOMXPath $xp, string $style, string $generatedStyle = 'GeneratedField'): void
|
||
{
|
||
$nodes = $xp->query("//text:span[@text:style-name='$style']");
|
||
if (!$nodes || $nodes->length === 0) {
|
||
return;
|
||
}
|
||
$nodes
|
||
->item(0)
|
||
->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:style-name', $generatedStyle);
|
||
}
|
||
|
||
private static function addGeneratedFieldStyles(DOMDocument $dom, DOMXPath $xp): void
|
||
{
|
||
$automatic = $xp->query('//office:automatic-styles')->item(0);
|
||
if (!$automatic) {
|
||
return;
|
||
}
|
||
self::appendTextStyle($dom, $automatic, 'GeneratedField', '11pt');
|
||
self::appendTextStyle($dom, $automatic, 'GeneratedValue', '11pt', true);
|
||
self::appendTextStyle($dom, $automatic, 'GeneratedSignature', '11pt');
|
||
}
|
||
|
||
private static function appendTextStyle(
|
||
DOMDocument $dom,
|
||
DOMNode $automatic,
|
||
string $name,
|
||
string $size,
|
||
bool $bold = false,
|
||
): void {
|
||
$style = $dom->createElementNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:style');
|
||
$style->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:name', $name);
|
||
$style->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:family', 'text');
|
||
$props = $dom->createElementNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:text-properties');
|
||
$props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-name', 'Open Sans');
|
||
$props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-name-asian', 'Open Sans');
|
||
$props->setAttributeNS(
|
||
'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
|
||
'style:font-name-complex',
|
||
'Open Sans',
|
||
);
|
||
$props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', 'fo:font-size', $size);
|
||
$weight = $bold ? 'bold' : 'normal';
|
||
$props->setAttributeNS(
|
||
'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
|
||
'fo:font-weight',
|
||
$weight,
|
||
);
|
||
$props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-size-asian', $size);
|
||
$props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-size-complex', $size);
|
||
$props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-weight-asian', $weight);
|
||
$props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-weight-complex', $weight);
|
||
$style->appendChild($props);
|
||
$automatic->appendChild($style);
|
||
}
|
||
|
||
private static function transformTemplate(string $type, string $filename, callable $transform): string
|
||
{
|
||
$dir = self::tempDir();
|
||
$target = $dir . '/' . $filename . '.odt';
|
||
if (!copy(self::template($type), $target)) {
|
||
throw new RuntimeException(t('service.document.copy_failed'));
|
||
}
|
||
$zip = new ZipArchive();
|
||
if ($zip->open($target) !== true) {
|
||
throw new RuntimeException(t('service.document.open_failed'));
|
||
}
|
||
$xml = $zip->getFromName('content.xml');
|
||
if ($xml === false) {
|
||
$zip->close();
|
||
throw new RuntimeException(t('service.document.content_invalid'));
|
||
}
|
||
$dom = new DOMDocument();
|
||
$dom->preserveWhiteSpace = true;
|
||
if (!$dom->loadXML($xml)) {
|
||
$zip->close();
|
||
throw new RuntimeException(t('service.document.xml_invalid'));
|
||
}
|
||
$xp = new DOMXPath($dom);
|
||
$xp->registerNamespace('text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
|
||
$xp->registerNamespace('office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
|
||
self::addGeneratedFieldStyles($dom, $xp);
|
||
$transform($xp);
|
||
$updated = $dom->saveXML();
|
||
if ($updated === false || !$zip->addFromString('content.xml', $updated)) {
|
||
$zip->close();
|
||
throw new RuntimeException(t('service.document.update_failed'));
|
||
}
|
||
$zip->close();
|
||
return $target;
|
||
}
|
||
|
||
private static function replaceParagraph(DOMXPath $xp, string $startsWith, array $parts, int $occurrence = 0): void
|
||
{
|
||
$matches = [];
|
||
foreach ($xp->query('//text:p') as $paragraph) {
|
||
$plain = preg_replace('/\s+/u', ' ', str_replace("\u{00A0}", ' ', $paragraph->textContent));
|
||
if (str_starts_with(trim((string) $plain), $startsWith)) {
|
||
$matches[] = $paragraph;
|
||
}
|
||
}
|
||
$node = $matches[$occurrence] ?? null;
|
||
if (!$node) {
|
||
return;
|
||
}
|
||
while ($node->firstChild) {
|
||
$node->removeChild($node->firstChild);
|
||
}
|
||
foreach ($parts as $part) {
|
||
if ($part === ['tab']) {
|
||
$node->appendChild(
|
||
$node->ownerDocument->createElementNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:tab'),
|
||
);
|
||
continue;
|
||
}
|
||
if ($part === ['break']) {
|
||
$node->appendChild(
|
||
$node->ownerDocument->createElementNS(
|
||
'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
|
||
'text:line-break',
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
[$text, $kind] = $part;
|
||
$span = $node->ownerDocument->createElementNS(
|
||
'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
|
||
'text:span',
|
||
);
|
||
$span->setAttributeNS(
|
||
'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
|
||
'text:style-name',
|
||
$kind === 'value' ? 'GeneratedValue' : 'GeneratedField',
|
||
);
|
||
$span->appendChild($node->ownerDocument->createTextNode($text));
|
||
$node->appendChild($span);
|
||
}
|
||
}
|
||
|
||
private static function testLabel(string $status): string
|
||
{
|
||
return match ($status) {
|
||
'neg' => 'négatif',
|
||
'pos' => 'positif',
|
||
default => 'non renseigné',
|
||
};
|
||
}
|
||
private static function safeName(string $name): string
|
||
{
|
||
$name = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name) ?: 'chat';
|
||
return strtolower(trim(preg_replace('/[^a-zA-Z0-9]+/', '-', $name), '-')) ?: 'chat';
|
||
}
|
||
private static function tempDir(): string
|
||
{
|
||
$dir = sys_get_temp_dir() . '/globinours-doc-' . bin2hex(random_bytes(8));
|
||
if (!mkdir($dir, 0775, true)) {
|
||
throw new RuntimeException(t('service.document.temp_failed'));
|
||
}
|
||
return $dir;
|
||
}
|
||
private static function fileUri(string $path): string
|
||
{
|
||
return 'file://' . str_replace('%2F', '/', rawurlencode($path));
|
||
}
|
||
}
|