123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833 |
- <?php
- namespace OC\Files\Storage;
- use Exception;
- use Icewind\Streams\CallbackWrapper;
- use Icewind\Streams\IteratorDirectory;
- use OC\Files\Filesystem;
- use OC\MemCache\ArrayCache;
- use OCP\AppFramework\Http;
- use OCP\Constants;
- use OCP\Diagnostics\IEventLogger;
- use OCP\Files\FileInfo;
- use OCP\Files\ForbiddenException;
- use OCP\Files\IMimeTypeDetector;
- use OCP\Files\StorageInvalidException;
- use OCP\Files\StorageNotAvailableException;
- use OCP\Http\Client\IClient;
- use OCP\Http\Client\IClientService;
- use OCP\ICertificateManager;
- use OCP\IConfig;
- use OCP\Server;
- use OCP\Util;
- use Psr\Http\Message\ResponseInterface;
- use Psr\Log\LoggerInterface;
- use Sabre\DAV\Client;
- use Sabre\DAV\Xml\Property\ResourceType;
- use Sabre\HTTP\ClientException;
- use Sabre\HTTP\ClientHttpException;
- use Sabre\HTTP\RequestInterface;
- class DAV extends Common {
-
- protected $password;
-
- protected $user;
-
- protected $authType;
-
- protected $host;
-
- protected $secure;
-
- protected $root;
-
- protected $certPath;
-
- protected $ready;
-
- protected $client;
-
- protected $statCache;
-
- protected $httpClientService;
-
- protected $certManager;
- protected LoggerInterface $logger;
- protected IEventLogger $eventLogger;
- protected IMimeTypeDetector $mimeTypeDetector;
-
- private $timeout;
- protected const PROPFIND_PROPS = [
- '{DAV:}getlastmodified',
- '{DAV:}getcontentlength',
- '{DAV:}getcontenttype',
- '{http://owncloud.org/ns}permissions',
- '{http://open-collaboration-services.org/ns}share-permissions',
- '{DAV:}resourcetype',
- '{DAV:}getetag',
- '{DAV:}quota-available-bytes',
- ];
-
- public function __construct(array $parameters) {
- $this->statCache = new ArrayCache();
- $this->httpClientService = Server::get(IClientService::class);
- if (isset($parameters['host']) && isset($parameters['user']) && isset($parameters['password'])) {
- $host = $parameters['host'];
-
- if (str_starts_with($host, 'https://')) {
- $host = substr($host, 8);
- } elseif (str_starts_with($host, 'http://')) {
- $host = substr($host, 7);
- }
- $this->host = $host;
- $this->user = $parameters['user'];
- $this->password = $parameters['password'];
- if (isset($parameters['authType'])) {
- $this->authType = $parameters['authType'];
- }
- if (isset($parameters['secure'])) {
- if (is_string($parameters['secure'])) {
- $this->secure = ($parameters['secure'] === 'true');
- } else {
- $this->secure = (bool)$parameters['secure'];
- }
- } else {
- $this->secure = false;
- }
- if ($this->secure === true) {
-
- $this->certManager = \OC::$server->getCertificateManager();
- }
- $this->root = $parameters['root'] ?? '/';
- $this->root = '/' . ltrim($this->root, '/');
- $this->root = rtrim($this->root, '/') . '/';
- } else {
- throw new \Exception('Invalid webdav storage configuration');
- }
- $this->logger = Server::get(LoggerInterface::class);
- $this->eventLogger = Server::get(IEventLogger::class);
-
- $this->timeout = Server::get(IConfig::class)->getSystemValueInt('davstorage.request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT);
- $this->mimeTypeDetector = \OC::$server->getMimeTypeDetector();
- }
- protected function init(): void {
- if ($this->ready) {
- return;
- }
- $this->ready = true;
- $settings = [
- 'baseUri' => $this->createBaseUri(),
- 'userName' => $this->user,
- 'password' => $this->password,
- ];
- if ($this->authType !== null) {
- $settings['authType'] = $this->authType;
- }
- $proxy = Server::get(IConfig::class)->getSystemValueString('proxy', '');
- if ($proxy !== '') {
- $settings['proxy'] = $proxy;
- }
- $this->client = new Client($settings);
- $this->client->setThrowExceptions(true);
- if ($this->secure === true) {
- $certPath = $this->certManager->getAbsoluteBundlePath();
- if (file_exists($certPath)) {
- $this->certPath = $certPath;
- }
- if ($this->certPath) {
- $this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
- }
- }
- $lastRequestStart = 0;
- $this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) {
- $this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']);
- $lastRequestStart = microtime(true);
- $this->eventLogger->start('fs:storage:dav:request', 'Sending dav request to external storage');
- });
- $this->client->on('afterRequest', function (RequestInterface $request) use (&$lastRequestStart) {
- $elapsed = microtime(true) - $lastRequestStart;
- $this->logger->debug('dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl() . ' took ' . round($elapsed * 1000, 1) . 'ms', ['app' => 'dav']);
- $this->eventLogger->end('fs:storage:dav:request');
- });
- }
-
- public function clearStatCache(): void {
- $this->statCache->clear();
- }
- public function getId(): string {
- return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
- }
- public function createBaseUri(): string {
- $baseUri = 'http';
- if ($this->secure) {
- $baseUri .= 's';
- }
- $baseUri .= '://' . $this->host . $this->root;
- return $baseUri;
- }
- public function mkdir(string $path): bool {
- $this->init();
- $path = $this->cleanPath($path);
- $result = $this->simpleResponse('MKCOL', $path, null, 201);
- if ($result) {
- $this->statCache->set($path, true);
- }
- return $result;
- }
- public function rmdir(string $path): bool {
- $this->init();
- $path = $this->cleanPath($path);
-
-
- $result = $this->simpleResponse('DELETE', $path . '/', null, 204);
- $this->statCache->clear($path . '/');
- $this->statCache->remove($path);
- return $result;
- }
- public function opendir(string $path) {
- $this->init();
- $path = $this->cleanPath($path);
- try {
- $content = $this->getDirectoryContent($path);
- $files = [];
- foreach ($content as $child) {
- $files[] = $child['name'];
- }
- return IteratorDirectory::wrap($files);
- } catch (\Exception $e) {
- $this->convertException($e, $path);
- }
- return false;
- }
-
- protected function propfind(string $path): array|false {
- $path = $this->cleanPath($path);
- $cachedResponse = $this->statCache->get($path);
-
- if (is_null($cachedResponse) || $cachedResponse === true) {
- $this->init();
- $response = false;
- try {
- $response = $this->client->propFind(
- $this->encodePath($path),
- self::PROPFIND_PROPS
- );
- $this->statCache->set($path, $response);
- } catch (ClientHttpException $e) {
- if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
- $this->statCache->clear($path . '/');
- $this->statCache->set($path, false);
- } else {
- $this->convertException($e, $path);
- }
- } catch (\Exception $e) {
- $this->convertException($e, $path);
- }
- } else {
- $response = $cachedResponse;
- }
- return $response;
- }
- public function filetype(string $path): string|false {
- try {
- $response = $this->propfind($path);
- if ($response === false) {
- return false;
- }
- $responseType = [];
- if (isset($response['{DAV:}resourcetype'])) {
-
- $responseType = $response['{DAV:}resourcetype']->getValue();
- }
- return (count($responseType) > 0 && $responseType[0] == '{DAV:}collection') ? 'dir' : 'file';
- } catch (\Exception $e) {
- $this->convertException($e, $path);
- }
- return false;
- }
- public function file_exists(string $path): bool {
- try {
- $path = $this->cleanPath($path);
- $cachedState = $this->statCache->get($path);
- if ($cachedState === false) {
-
- return false;
- } elseif (!is_null($cachedState)) {
- return true;
- }
-
- return ($this->propfind($path) !== false);
- } catch (\Exception $e) {
- $this->convertException($e, $path);
- }
- return false;
- }
- public function unlink(string $path): bool {
- $this->init();
- $path = $this->cleanPath($path);
- $result = $this->simpleResponse('DELETE', $path, null, 204);
- $this->statCache->clear($path . '/');
- $this->statCache->remove($path);
- return $result;
- }
- public function fopen(string $path, string $mode) {
- $this->init();
- $path = $this->cleanPath($path);
- switch ($mode) {
- case 'r':
- case 'rb':
- try {
- $response = $this->httpClientService
- ->newClient()
- ->get($this->createBaseUri() . $this->encodePath($path), [
- 'auth' => [$this->user, $this->password],
- 'stream' => true,
-
- 'timeout' => $this->timeout
- ]);
- } catch (\GuzzleHttp\Exception\ClientException $e) {
- if ($e->getResponse() instanceof ResponseInterface
- && $e->getResponse()->getStatusCode() === 404) {
- return false;
- } else {
- throw $e;
- }
- }
- if ($response->getStatusCode() !== Http::STATUS_OK) {
- if ($response->getStatusCode() === Http::STATUS_LOCKED) {
- throw new \OCP\Lock\LockedException($path);
- } else {
- $this->logger->error('Guzzle get returned status code ' . $response->getStatusCode(), ['app' => 'webdav client']);
- }
- }
- return $response->getBody();
- case 'w':
- case 'wb':
- case 'a':
- case 'ab':
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
-
- $tempManager = \OC::$server->getTempManager();
- if (strrpos($path, '.') !== false) {
- $ext = substr($path, strrpos($path, '.'));
- } else {
- $ext = '';
- }
- if ($this->file_exists($path)) {
- if (!$this->isUpdatable($path)) {
- return false;
- }
- if ($mode === 'w' || $mode === 'w+') {
- $tmpFile = $tempManager->getTemporaryFile($ext);
- } else {
- $tmpFile = $this->getCachedFile($path);
- }
- } else {
- if (!$this->isCreatable(dirname($path))) {
- return false;
- }
- $tmpFile = $tempManager->getTemporaryFile($ext);
- }
- $handle = fopen($tmpFile, $mode);
- return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
- $this->writeBack($tmpFile, $path);
- });
- }
- }
- public function writeBack(string $tmpFile, string $path): void {
- $this->uploadFile($tmpFile, $path);
- unlink($tmpFile);
- }
- public function free_space(string $path): int|float|false {
- $this->init();
- $path = $this->cleanPath($path);
- try {
- $response = $this->propfind($path);
- if ($response === false) {
- return FileInfo::SPACE_UNKNOWN;
- }
- if (isset($response['{DAV:}quota-available-bytes'])) {
- return Util::numericToNumber($response['{DAV:}quota-available-bytes']);
- } else {
- return FileInfo::SPACE_UNKNOWN;
- }
- } catch (\Exception $e) {
- return FileInfo::SPACE_UNKNOWN;
- }
- }
- public function touch(string $path, ?int $mtime = null): bool {
- $this->init();
- if (is_null($mtime)) {
- $mtime = time();
- }
- $path = $this->cleanPath($path);
-
- if ($this->file_exists($path)) {
- try {
- $this->statCache->remove($path);
- $this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
-
- $response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
- if (isset($response['{DAV:}getlastmodified'])) {
- $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
- if ($remoteMtime !== $mtime) {
-
- return false;
- }
- }
- } catch (ClientHttpException $e) {
- if ($e->getHttpStatus() === 501) {
- return false;
- }
- $this->convertException($e, $path);
- return false;
- } catch (\Exception $e) {
- $this->convertException($e, $path);
- return false;
- }
- } else {
- $this->file_put_contents($path, '');
- }
- return true;
- }
- public function file_put_contents(string $path, mixed $data): int|float|false {
- $path = $this->cleanPath($path);
- $result = parent::file_put_contents($path, $data);
- $this->statCache->remove($path);
- return $result;
- }
- protected function uploadFile(string $path, string $target): void {
- $this->init();
-
- $target = $this->cleanPath($target);
- $this->statCache->remove($target);
- $source = fopen($path, 'r');
- $this->httpClientService
- ->newClient()
- ->put($this->createBaseUri() . $this->encodePath($target), [
- 'body' => $source,
- 'auth' => [$this->user, $this->password],
-
- 'timeout' => $this->timeout
- ]);
- $this->removeCachedFile($target);
- }
- public function rename(string $source, string $target): bool {
- $this->init();
- $source = $this->cleanPath($source);
- $target = $this->cleanPath($target);
- try {
-
- if ($this->is_dir($target)) {
-
- $target = rtrim($target, '/') . '/';
- }
- $this->client->request(
- 'MOVE',
- $this->encodePath($source),
- null,
- [
- 'Destination' => $this->createBaseUri() . $this->encodePath($target),
- ]
- );
- $this->statCache->clear($source . '/');
- $this->statCache->clear($target . '/');
- $this->statCache->set($source, false);
- $this->statCache->set($target, true);
- $this->removeCachedFile($source);
- $this->removeCachedFile($target);
- return true;
- } catch (\Exception $e) {
- $this->convertException($e);
- }
- return false;
- }
- public function copy(string $source, string $target): bool {
- $this->init();
- $source = $this->cleanPath($source);
- $target = $this->cleanPath($target);
- try {
-
- if ($this->is_dir($target)) {
-
- $target = rtrim($target, '/') . '/';
- }
- $this->client->request(
- 'COPY',
- $this->encodePath($source),
- null,
- [
- 'Destination' => $this->createBaseUri() . $this->encodePath($target),
- ]
- );
- $this->statCache->clear($target . '/');
- $this->statCache->set($target, true);
- $this->removeCachedFile($target);
- return true;
- } catch (\Exception $e) {
- $this->convertException($e);
- }
- return false;
- }
- public function getMetaData(string $path): ?array {
- if (Filesystem::isFileBlacklisted($path)) {
- throw new ForbiddenException('Invalid path: ' . $path, false);
- }
- $response = $this->propfind($path);
- if (!$response) {
- return null;
- } else {
- return $this->getMetaFromPropfind($path, $response);
- }
- }
- private function getMetaFromPropfind(string $path, array $response): array {
- if (isset($response['{DAV:}getetag'])) {
- $etag = trim($response['{DAV:}getetag'], '"');
- if (strlen($etag) > 40) {
- $etag = md5($etag);
- }
- } else {
- $etag = parent::getETag($path);
- }
- $responseType = [];
- if (isset($response['{DAV:}resourcetype'])) {
-
- $responseType = $response['{DAV:}resourcetype']->getValue();
- }
- $type = (count($responseType) > 0 && $responseType[0] == '{DAV:}collection') ? 'dir' : 'file';
- if ($type === 'dir') {
- $mimeType = 'httpd/unix-directory';
- } elseif (isset($response['{DAV:}getcontenttype'])) {
- $mimeType = $response['{DAV:}getcontenttype'];
- } else {
- $mimeType = $this->mimeTypeDetector->detectPath($path);
- }
- if (isset($response['{http://owncloud.org/ns}permissions'])) {
- $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
- } elseif ($type === 'dir') {
- $permissions = Constants::PERMISSION_ALL;
- } else {
- $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
- }
- $mtime = isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null;
- if ($type === 'dir') {
- $size = -1;
- } else {
- $size = Util::numericToNumber($response['{DAV:}getcontentlength'] ?? 0);
- }
- return [
- 'name' => basename($path),
- 'mtime' => $mtime,
- 'storage_mtime' => $mtime,
- 'size' => $size,
- 'permissions' => $permissions,
- 'etag' => $etag,
- 'mimetype' => $mimeType,
- ];
- }
- public function stat(string $path): array|false {
- $meta = $this->getMetaData($path);
- return $meta ?: false;
- }
- public function getMimeType(string $path): string|false {
- $meta = $this->getMetaData($path);
- return $meta ? $meta['mimetype'] : false;
- }
- public function cleanPath(string $path): string {
- if ($path === '') {
- return $path;
- }
- $path = Filesystem::normalizePath($path);
-
- return substr($path, 1);
- }
-
- protected function encodePath(string $path): string {
-
- return str_replace('%2F', '/', rawurlencode($path));
- }
-
- protected function simpleResponse(string $method, string $path, ?string $body, int $expected): bool {
- $path = $this->cleanPath($path);
- try {
- $response = $this->client->request($method, $this->encodePath($path), $body);
- return $response['statusCode'] == $expected;
- } catch (ClientHttpException $e) {
- if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
- $this->statCache->clear($path . '/');
- $this->statCache->set($path, false);
- return false;
- }
- $this->convertException($e, $path);
- } catch (\Exception $e) {
- $this->convertException($e, $path);
- }
- return false;
- }
-
- public static function checkDependencies(): bool {
- return true;
- }
- public function isUpdatable(string $path): bool {
- return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
- }
- public function isCreatable(string $path): bool {
- return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
- }
- public function isSharable(string $path): bool {
- return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
- }
- public function isDeletable(string $path): bool {
- return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
- }
- public function getPermissions(string $path): int {
- $stat = $this->getMetaData($path);
- return $stat ? $stat['permissions'] : 0;
- }
- public function getETag(string $path): string|false {
- $meta = $this->getMetaData($path);
- return $meta ? $meta['etag'] : false;
- }
- protected function parsePermissions(string $permissionsString): int {
- $permissions = Constants::PERMISSION_READ;
- if (str_contains($permissionsString, 'R')) {
- $permissions |= Constants::PERMISSION_SHARE;
- }
- if (str_contains($permissionsString, 'D')) {
- $permissions |= Constants::PERMISSION_DELETE;
- }
- if (str_contains($permissionsString, 'W')) {
- $permissions |= Constants::PERMISSION_UPDATE;
- }
- if (str_contains($permissionsString, 'CK')) {
- $permissions |= Constants::PERMISSION_CREATE;
- $permissions |= Constants::PERMISSION_UPDATE;
- }
- return $permissions;
- }
- public function hasUpdated(string $path, int $time): bool {
- $this->init();
- $path = $this->cleanPath($path);
- try {
-
- $this->statCache->remove($path);
- $response = $this->propfind($path);
- if ($response === false) {
- if ($path === '') {
-
- throw new StorageNotAvailableException('root is gone');
- }
- return false;
- }
- if (isset($response['{DAV:}getetag'])) {
- $cachedData = $this->getCache()->get($path);
- $etag = trim($response['{DAV:}getetag'], '"');
- if (($cachedData === false) || (!empty($etag) && ($cachedData['etag'] !== $etag))) {
- return true;
- } elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
- $sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
- return $sharePermissions !== $cachedData['permissions'];
- } elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
- $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
- return $permissions !== $cachedData['permissions'];
- } else {
- return false;
- }
- } elseif (isset($response['{DAV:}getlastmodified'])) {
- $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
- return $remoteMtime > $time;
- } else {
-
- return false;
- }
- } catch (ClientHttpException $e) {
- if ($e->getHttpStatus() === 405) {
- if ($path === '') {
-
- throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
- }
- return false;
- }
- $this->convertException($e, $path);
- return false;
- } catch (\Exception $e) {
- $this->convertException($e, $path);
- return false;
- }
- }
-
- protected function convertException(Exception $e, string $path = ''): void {
- $this->logger->debug($e->getMessage(), ['app' => 'files_external', 'exception' => $e]);
- if ($e instanceof ClientHttpException) {
- if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
- throw new \OCP\Lock\LockedException($path);
- }
- if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
-
- throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
- } elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
-
- return;
- } elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
-
- throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
- }
- throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
- } elseif ($e instanceof ClientException) {
-
- throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
- } elseif ($e instanceof \InvalidArgumentException) {
-
-
- throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
- } elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
-
- throw $e;
- }
-
- }
- public function getDirectoryContent(string $directory): \Traversable {
- $this->init();
- $directory = $this->cleanPath($directory);
- try {
- $responses = $this->client->propFind(
- $this->encodePath($directory),
- self::PROPFIND_PROPS,
- 1
- );
- array_shift($responses);
- if (!$this->statCache->hasKey($directory)) {
- $this->statCache->set($directory, true);
- }
- foreach ($responses as $file => $response) {
- $file = rawurldecode($file);
- $file = substr($file, strlen($this->root));
- $file = $this->cleanPath($file);
- $this->statCache->set($file, $response);
- yield $this->getMetaFromPropfind($file, $response);
- }
- } catch (\Exception $e) {
- $this->convertException($e, $directory);
- }
- }
- }
|