2019-10-19 17:13:13 +02:00
|
|
|
<?php
|
2020-02-22 18:14:36 +01:00
|
|
|
/**
|
|
|
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
|
|
*
|
2022-11-29 22:28:53 +01:00
|
|
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
2020-02-22 18:14:36 +01:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
2020-01-05 15:46:58 +01:00
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
2019-10-19 17:13:13 +02:00
|
|
|
namespace App\Services\Attachments;
|
|
|
|
|
|
|
|
use App\Entity\Attachments\Attachment;
|
|
|
|
use App\Entity\Attachments\AttachmentContainingDBElement;
|
2020-06-01 15:55:34 +02:00
|
|
|
use App\Entity\Attachments\AttachmentType;
|
2019-10-19 17:13:13 +02:00
|
|
|
use App\Entity\Attachments\AttachmentTypeAttachment;
|
|
|
|
use App\Entity\Attachments\CategoryAttachment;
|
|
|
|
use App\Entity\Attachments\CurrencyAttachment;
|
|
|
|
use App\Entity\Attachments\DeviceAttachment;
|
|
|
|
use App\Entity\Attachments\FootprintAttachment;
|
|
|
|
use App\Entity\Attachments\GroupAttachment;
|
|
|
|
use App\Entity\Attachments\ManufacturerAttachment;
|
|
|
|
use App\Entity\Attachments\MeasurementUnitAttachment;
|
|
|
|
use App\Entity\Attachments\PartAttachment;
|
|
|
|
use App\Entity\Attachments\StorelocationAttachment;
|
|
|
|
use App\Entity\Attachments\SupplierAttachment;
|
|
|
|
use App\Entity\Attachments\UserAttachment;
|
2019-10-19 18:42:06 +02:00
|
|
|
use App\Exceptions\AttachmentDownloadException;
|
2020-01-05 22:49:00 +01:00
|
|
|
use const DIRECTORY_SEPARATOR;
|
|
|
|
use function get_class;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
use RuntimeException;
|
2019-10-19 18:42:06 +02:00
|
|
|
use Symfony\Component\Filesystem\Filesystem;
|
2019-10-19 17:13:13 +02:00
|
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
2019-10-19 18:42:06 +02:00
|
|
|
use Symfony\Component\Mime\MimeTypesInterface;
|
2019-10-19 17:13:13 +02:00
|
|
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
2019-10-19 18:42:06 +02:00
|
|
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
|
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
2019-10-19 17:13:13 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This service handles the form submitting of an attachment and handles things like file uploading and downloading.
|
|
|
|
*/
|
|
|
|
class AttachmentSubmitHandler
|
|
|
|
{
|
2022-09-18 22:59:31 +02:00
|
|
|
protected AttachmentPathResolver $pathResolver;
|
|
|
|
protected array $folder_mapping;
|
|
|
|
protected bool $allow_attachments_downloads;
|
|
|
|
protected HttpClientInterface $httpClient;
|
|
|
|
protected MimeTypesInterface $mimeTypes;
|
|
|
|
protected FileTypeFilterTools $filterTools;
|
2020-06-01 15:55:34 +02:00
|
|
|
|
2019-11-09 00:47:20 +01:00
|
|
|
public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads,
|
2020-06-01 15:55:34 +02:00
|
|
|
HttpClientInterface $httpClient, MimeTypesInterface $mimeTypes,
|
|
|
|
FileTypeFilterTools $filterTools)
|
2019-10-19 17:13:13 +02:00
|
|
|
{
|
|
|
|
$this->pathResolver = $pathResolver;
|
|
|
|
$this->allow_attachments_downloads = $allow_attachments_downloads;
|
2019-10-19 18:42:06 +02:00
|
|
|
$this->httpClient = $httpClient;
|
|
|
|
$this->mimeTypes = $mimeTypes;
|
2019-10-19 17:13:13 +02:00
|
|
|
|
2020-06-01 15:55:34 +02:00
|
|
|
$this->filterTools = $filterTools;
|
|
|
|
|
2019-10-19 17:13:13 +02:00
|
|
|
//The mapping used to determine which folder will be used for an attachment type
|
2020-01-05 22:49:00 +01:00
|
|
|
$this->folder_mapping = [
|
|
|
|
PartAttachment::class => 'part',
|
|
|
|
AttachmentTypeAttachment::class => 'attachment_type',
|
|
|
|
CategoryAttachment::class => 'category',
|
|
|
|
CurrencyAttachment::class => 'currency',
|
|
|
|
DeviceAttachment::class => 'device',
|
|
|
|
FootprintAttachment::class => 'footprint',
|
|
|
|
GroupAttachment::class => 'group',
|
|
|
|
ManufacturerAttachment::class => 'manufacturer',
|
|
|
|
MeasurementUnitAttachment::class => 'measurement_unit',
|
|
|
|
StorelocationAttachment::class => 'storelocation',
|
|
|
|
SupplierAttachment::class => 'supplier',
|
|
|
|
UserAttachment::class => 'user',
|
|
|
|
];
|
2019-10-19 17:13:13 +02:00
|
|
|
}
|
|
|
|
|
2020-06-01 15:55:34 +02:00
|
|
|
/**
|
|
|
|
* Check if the extension of the uploaded file is allowed for the given attachment type.
|
|
|
|
* Returns true, if the file is allowed, false if not.
|
|
|
|
*/
|
|
|
|
public function isValidFileExtension(AttachmentType $attachment_type, UploadedFile $uploadedFile): bool
|
|
|
|
{
|
|
|
|
//Only validate if the attachment type has specified an filetype filter:
|
|
|
|
if (empty($attachment_type->getFiletypeFilter())) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->filterTools->isExtensionAllowed(
|
|
|
|
$attachment_type->getFiletypeFilter(),
|
|
|
|
$uploadedFile->getClientOriginalExtension()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-10-19 17:13:13 +02:00
|
|
|
/**
|
|
|
|
* Generates a filename for the given attachment and extension.
|
|
|
|
* The filename contains a random id, so every time this function is called you get an unique name.
|
2019-11-09 00:47:20 +01:00
|
|
|
*
|
2019-10-19 17:13:13 +02:00
|
|
|
* @param Attachment $attachment The attachment that should be used for generating an attachment
|
2019-11-09 00:47:20 +01:00
|
|
|
* @param string $extension The extension that the new file should have (must only contain chars allowed in pathes)
|
|
|
|
*
|
2020-01-04 20:24:09 +01:00
|
|
|
* @return string the new filename
|
2019-10-19 17:13:13 +02:00
|
|
|
*/
|
2019-11-09 00:47:20 +01:00
|
|
|
public function generateAttachmentFilename(Attachment $attachment, string $extension): string
|
2019-10-19 17:13:13 +02:00
|
|
|
{
|
|
|
|
//Normalize extension
|
|
|
|
$extension = transliterator_transliterate(
|
|
|
|
'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
|
|
|
|
$extension
|
|
|
|
);
|
|
|
|
|
|
|
|
//Use the (sanatized) attachment name as an filename part
|
|
|
|
$safeName = transliterator_transliterate(
|
|
|
|
'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
|
|
|
|
$attachment->getName()
|
|
|
|
);
|
|
|
|
|
2019-11-09 00:47:20 +01:00
|
|
|
return $safeName.'-'.uniqid('', false).'.'.$extension;
|
2019-10-19 17:13:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates an (absolute) path to a folder where the given attachment should be stored.
|
2019-11-09 00:47:20 +01:00
|
|
|
*
|
|
|
|
* @param Attachment $attachment The attachment that should be used for
|
|
|
|
* @param bool $secure_upload True if the file path should be located in a safe location
|
|
|
|
*
|
2020-01-04 20:24:09 +01:00
|
|
|
* @return string the absolute path for the attachment folder
|
2019-10-19 17:13:13 +02:00
|
|
|
*/
|
2019-11-09 00:47:20 +01:00
|
|
|
public function generateAttachmentPath(Attachment $attachment, bool $secure_upload = false): string
|
2019-10-19 17:13:13 +02:00
|
|
|
{
|
|
|
|
if ($secure_upload) {
|
|
|
|
$base_path = $this->pathResolver->getSecurePath();
|
|
|
|
} else {
|
|
|
|
$base_path = $this->pathResolver->getMediaPath();
|
|
|
|
}
|
|
|
|
|
|
|
|
//Ensure the given attachment class is known to mapping
|
2020-08-21 21:36:22 +02:00
|
|
|
if (!isset($this->folder_mapping[get_class($attachment)])) {
|
2020-01-05 22:49:00 +01:00
|
|
|
throw new InvalidArgumentException('The given attachment class is not known! The passed class was: '.get_class($attachment));
|
2019-10-19 17:13:13 +02:00
|
|
|
}
|
|
|
|
//Ensure the attachment has an assigned element
|
2019-11-09 00:47:20 +01:00
|
|
|
if (null === $attachment->getElement()) {
|
2020-01-05 22:49:00 +01:00
|
|
|
throw new InvalidArgumentException('The given attachment is not assigned to an element! An element is needed to generate a path!');
|
2019-10-19 17:13:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
//Build path
|
|
|
|
return
|
2020-01-05 22:49:00 +01:00
|
|
|
$base_path.DIRECTORY_SEPARATOR //Base path
|
|
|
|
.$this->folder_mapping[get_class($attachment)].DIRECTORY_SEPARATOR.$attachment->getElement()->getID();
|
2019-10-19 17:13:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle the submit of an attachment form.
|
|
|
|
* This function will move the uploaded file or download the URL file to server, if needed.
|
2019-11-09 00:47:20 +01:00
|
|
|
*
|
2020-01-04 20:24:09 +01:00
|
|
|
* @param Attachment $attachment the attachment that should be used for handling
|
2019-11-09 00:47:20 +01:00
|
|
|
* @param UploadedFile|null $file If given, that file will be moved to the right location
|
|
|
|
* @param array $options The options to use with the upload. Here you can specify that an URL should be downloaded,
|
|
|
|
* or an file should be moved to a secure location.
|
|
|
|
*
|
2019-10-19 17:13:13 +02:00
|
|
|
* @return Attachment The attachment with the new filename (same instance as passed $attachment)
|
|
|
|
*/
|
2019-11-09 00:47:20 +01:00
|
|
|
public function handleFormSubmit(Attachment $attachment, ?UploadedFile $file, array $options = []): Attachment
|
2019-10-19 17:13:13 +02:00
|
|
|
{
|
|
|
|
$resolver = new OptionsResolver();
|
|
|
|
$this->configureOptions($resolver);
|
|
|
|
$options = $resolver->resolve($options);
|
|
|
|
|
|
|
|
//When a file is given then upload it, otherwise check if we need to download the URL
|
|
|
|
if ($file) {
|
|
|
|
$this->upload($attachment, $file, $options);
|
|
|
|
} elseif ($options['download_url'] && $attachment->isExternal()) {
|
|
|
|
$this->downloadURL($attachment, $options);
|
|
|
|
}
|
|
|
|
|
2019-10-19 19:30:16 +02:00
|
|
|
//Move the attachment files to secure location (and back) if needed
|
|
|
|
$this->moveFile($attachment, $options['secure_attachment']);
|
|
|
|
|
2019-10-19 17:13:13 +02:00
|
|
|
//Check if we should assign this attachment to master picture
|
|
|
|
//this is only possible if the attachment is new (not yet persisted to DB)
|
2019-11-09 00:47:20 +01:00
|
|
|
if ($options['become_preview_if_empty'] && null === $attachment->getID() && $attachment->isPicture()) {
|
2019-10-19 17:13:13 +02:00
|
|
|
$element = $attachment->getElement();
|
2019-11-09 00:47:20 +01:00
|
|
|
if ($element instanceof AttachmentContainingDBElement && null === $element->getMasterPictureAttachment()) {
|
2019-10-19 17:13:13 +02:00
|
|
|
$element->setMasterPictureAttachment($attachment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $attachment;
|
|
|
|
}
|
|
|
|
|
2020-01-05 15:46:58 +01:00
|
|
|
protected function configureOptions(OptionsResolver $resolver): void
|
|
|
|
{
|
|
|
|
$resolver->setDefaults([
|
|
|
|
//If no preview image was set yet, the new uploaded file will become the preview image
|
|
|
|
'become_preview_if_empty' => true,
|
|
|
|
//When an URL is given download the URL
|
|
|
|
'download_url' => false,
|
|
|
|
'secure_attachment' => false,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2019-10-19 19:30:16 +02:00
|
|
|
/**
|
|
|
|
* Move the given attachment to secure location (or back to public folder) if needed.
|
2019-11-09 00:47:20 +01:00
|
|
|
*
|
2020-01-04 20:24:09 +01:00
|
|
|
* @param Attachment $attachment the attachment for which the file should be moved
|
|
|
|
* @param bool $secure_location this value determines, if the attachment is moved to the secure or public folder
|
2019-11-09 00:47:20 +01:00
|
|
|
*
|
2019-10-19 19:30:16 +02:00
|
|
|
* @return Attachment The attachment with the updated filepath
|
|
|
|
*/
|
2019-11-09 00:47:20 +01:00
|
|
|
protected function moveFile(Attachment $attachment, bool $secure_location): Attachment
|
2019-10-19 19:30:16 +02:00
|
|
|
{
|
|
|
|
//We can not do anything on builtins or external ressources
|
|
|
|
if ($attachment->isBuiltIn() || $attachment->isExternal()) {
|
|
|
|
return $attachment;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Check if we need to move the file
|
|
|
|
if ($secure_location === $attachment->isSecure()) {
|
|
|
|
return $attachment;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Determine the old filepath
|
|
|
|
$old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath());
|
2020-08-21 21:36:22 +02:00
|
|
|
if (!file_exists($old_path)) {
|
2019-10-19 19:30:16 +02:00
|
|
|
return $attachment;
|
|
|
|
}
|
|
|
|
|
|
|
|
$filename = basename($old_path);
|
|
|
|
//If the basename is not one of the new unique on, we have to save the old filename
|
2020-08-21 21:36:22 +02:00
|
|
|
if (!preg_match('#\w+-\w{13}\.#', $filename)) {
|
2019-10-19 19:30:16 +02:00
|
|
|
//Save filename to attachment field
|
|
|
|
$attachment->setFilename($attachment->getFilename());
|
|
|
|
}
|
|
|
|
|
|
|
|
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
|
|
|
$new_path = $this->generateAttachmentPath($attachment, $secure_location)
|
2020-01-05 22:49:00 +01:00
|
|
|
.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, $ext);
|
2019-10-19 19:30:16 +02:00
|
|
|
|
|
|
|
//Move file to new directory
|
|
|
|
$fs = new Filesystem();
|
2020-02-10 22:46:05 +01:00
|
|
|
//Ensure that the new path exists
|
|
|
|
$fs->mkdir(dirname($new_path));
|
2019-10-19 19:30:16 +02:00
|
|
|
$fs->rename($old_path, $new_path);
|
|
|
|
|
|
|
|
//Save info to attachment entity
|
|
|
|
$new_path = $this->pathResolver->realPathToPlaceholder($new_path);
|
|
|
|
$attachment->setPath($new_path);
|
|
|
|
|
|
|
|
return $attachment;
|
|
|
|
}
|
|
|
|
|
2019-10-19 17:13:13 +02:00
|
|
|
/**
|
2019-11-09 00:47:20 +01:00
|
|
|
* Download the URL set in the attachment and save it on the server.
|
|
|
|
*
|
2019-10-19 17:13:13 +02:00
|
|
|
* @param array $options The options from the handleFormSubmit function
|
2019-11-09 00:47:20 +01:00
|
|
|
*
|
2019-10-19 17:13:13 +02:00
|
|
|
* @return Attachment The attachment with the new filepath
|
|
|
|
*/
|
2019-11-09 00:47:20 +01:00
|
|
|
protected function downloadURL(Attachment $attachment, array $options): Attachment
|
2019-10-19 17:13:13 +02:00
|
|
|
{
|
|
|
|
//Check if we are allowed to download files
|
2020-08-21 21:36:22 +02:00
|
|
|
if (!$this->allow_attachments_downloads) {
|
2020-01-05 22:49:00 +01:00
|
|
|
throw new RuntimeException('Download of attachments is not allowed!');
|
2019-10-19 17:13:13 +02:00
|
|
|
}
|
2019-10-19 18:42:06 +02:00
|
|
|
|
|
|
|
$url = $attachment->getURL();
|
|
|
|
|
|
|
|
$fs = new Filesystem();
|
|
|
|
$attachment_folder = $this->generateAttachmentPath($attachment, $options['secure_attachment']);
|
2020-01-05 22:49:00 +01:00
|
|
|
$tmp_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'tmp');
|
2019-10-19 18:42:06 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
$response = $this->httpClient->request('GET', $url, [
|
|
|
|
'buffer' => false,
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (200 !== $response->getStatusCode()) {
|
2019-11-09 00:47:20 +01:00
|
|
|
throw new AttachmentDownloadException('Status code: '.$response->getStatusCode());
|
2019-10-19 18:42:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
//Open a temporary file in the attachment folder
|
|
|
|
$fs->mkdir($attachment_folder);
|
|
|
|
$fileHandler = fopen($tmp_path, 'wb');
|
|
|
|
//Write the downloaded data to file
|
|
|
|
foreach ($this->httpClient->stream($response) as $chunk) {
|
|
|
|
fwrite($fileHandler, $chunk->getContent());
|
|
|
|
}
|
|
|
|
fclose($fileHandler);
|
|
|
|
|
|
|
|
//File download should be finished here, so determine the new filename and extension
|
|
|
|
$headers = $response->getHeaders();
|
|
|
|
//Try to determine an filename
|
2019-11-09 00:47:20 +01:00
|
|
|
$filename = '';
|
2019-10-19 18:42:06 +02:00
|
|
|
|
|
|
|
//If an content disposition header was set try to extract the filename out of it
|
|
|
|
if (isset($headers['content-disposition'])) {
|
|
|
|
$tmp = [];
|
|
|
|
preg_match('/[^;\\n=]*=([\'\"])*(.*)(?(1)\1|)/', $headers['content-disposition'][0], $tmp);
|
|
|
|
$filename = $tmp[2];
|
|
|
|
}
|
|
|
|
|
|
|
|
//If we dont know filename yet, try to determine it out of url
|
2019-11-09 00:47:20 +01:00
|
|
|
if ('' === $filename) {
|
2019-10-19 18:42:06 +02:00
|
|
|
$filename = basename(parse_url($url, PHP_URL_PATH));
|
|
|
|
}
|
|
|
|
|
|
|
|
//Set original file
|
|
|
|
$attachment->setFilename($filename);
|
|
|
|
|
|
|
|
//Check if we have a extension given
|
|
|
|
$pathinfo = pathinfo($filename);
|
2020-08-21 21:36:22 +02:00
|
|
|
if (!empty($pathinfo['extension'])) {
|
2019-10-19 18:42:06 +02:00
|
|
|
$new_ext = $pathinfo['extension'];
|
|
|
|
} else { //Otherwise we have to guess the extension for the new file, based on its content
|
|
|
|
$new_ext = $this->mimeTypes->getExtensions($this->mimeTypes->guessMimeType($tmp_path))[0] ?? 'tmp';
|
|
|
|
}
|
|
|
|
|
|
|
|
//Rename the file to its new name and save path to attachment entity
|
2020-01-05 22:49:00 +01:00
|
|
|
$new_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, $new_ext);
|
2019-10-19 18:42:06 +02:00
|
|
|
$fs->rename($tmp_path, $new_path);
|
|
|
|
|
|
|
|
//Make our file path relative to %BASE%
|
|
|
|
$new_path = $this->pathResolver->realPathToPlaceholder($new_path);
|
|
|
|
//Save the path to the attachment
|
|
|
|
$attachment->setPath($new_path);
|
2020-01-05 22:49:00 +01:00
|
|
|
} catch (TransportExceptionInterface $transportExceptionInterface) {
|
2019-10-19 18:42:06 +02:00
|
|
|
throw new AttachmentDownloadException('Transport error!');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $attachment;
|
2019-10-19 17:13:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-11-09 00:47:20 +01:00
|
|
|
* Moves the given uploaded file to a permanent place and saves it into the attachment.
|
|
|
|
*
|
|
|
|
* @param Attachment $attachment The attachment in which the file should be saved
|
|
|
|
* @param UploadedFile $file The file which was uploaded
|
|
|
|
* @param array $options The options from the handleFormSubmit function
|
|
|
|
*
|
2019-10-19 17:13:13 +02:00
|
|
|
* @return Attachment The attachment with the new filepath
|
|
|
|
*/
|
2019-11-09 00:47:20 +01:00
|
|
|
protected function upload(Attachment $attachment, UploadedFile $file, array $options): Attachment
|
2019-10-19 17:13:13 +02:00
|
|
|
{
|
|
|
|
//Move our temporay attachment to its final location
|
|
|
|
$file_path = $file->move(
|
|
|
|
$this->generateAttachmentPath($attachment, $options['secure_attachment']),
|
|
|
|
$this->generateAttachmentFilename($attachment, $file->getClientOriginalExtension())
|
|
|
|
)->getRealPath();
|
|
|
|
|
|
|
|
//Make our file path relative to %BASE%
|
|
|
|
$file_path = $this->pathResolver->realPathToPlaceholder($file_path);
|
|
|
|
//Save the path to the attachment
|
|
|
|
$attachment->setPath($file_path);
|
|
|
|
//And save original filename
|
|
|
|
$attachment->setFilename($file->getClientOriginalName());
|
|
|
|
|
|
|
|
return $attachment;
|
|
|
|
}
|
2019-11-09 00:47:20 +01:00
|
|
|
}
|