* @copyright 2018, Maxence Lange * @license GNU AGPL version 3 or any later version * * 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 . * */ namespace OCA\Social\Service; use Exception; use OCA\Social\AP; use OCA\Social\Db\ActorsRequest; use OCA\Social\Db\CacheDocumentsRequest; use OCA\Social\Db\StreamRequest; use OCA\Social\Exceptions\CacheContentException; use OCA\Social\Exceptions\CacheContentMimeTypeException; use OCA\Social\Exceptions\CacheDocumentDoesNotExistException; use OCA\Social\Exceptions\ItemAlreadyExistsException; use OCA\Social\Exceptions\ItemUnknownException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Exceptions\UnauthorizedFediverseException; use OCA\Social\Exceptions\UrlCloudException; use OCA\Social\Model\ActivityPub\Actor\Person; use OCA\Social\Model\ActivityPub\Object\Document; use OCA\Social\Model\ActivityPub\Object\Image; use OCA\Social\Tools\Exceptions\MalformedArrayException; use OCA\Social\Tools\Exceptions\RequestContentException; use OCA\Social\Tools\Exceptions\RequestNetworkException; use OCA\Social\Tools\Exceptions\RequestResultNotJsonException; use OCA\Social\Tools\Exceptions\RequestResultSizeException; use OCA\Social\Tools\Exceptions\RequestServerException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\IURLGenerator; class DocumentService { public const ERROR_SIZE = 1; public const ERROR_MIMETYPE = 2; public const ERROR_PERMISSION = 3; private \OCP\IURLGenerator $urlGenerator; private CacheDocumentsRequest $cacheDocumentsRequest; private ActorsRequest $actorRequest; private StreamRequest $streamRequest; private CacheDocumentService $cacheService; private ConfigService $configService; private MiscService $miscService; /** * DocumentInterface constructor. * * @param IUrlGenerator $urlGenerator * @param CacheDocumentsRequest $cacheDocumentsRequest * @param ActorsRequest $actorRequest * @param StreamRequest $streamRequest * @param CacheDocumentService $cacheService * @param ConfigService $configService * @param MiscService $miscService */ public function __construct( IUrlGenerator $urlGenerator, CacheDocumentsRequest $cacheDocumentsRequest, ActorsRequest $actorRequest, StreamRequest $streamRequest, CacheDocumentService $cacheService, ConfigService $configService, MiscService $miscService ) { $this->urlGenerator = $urlGenerator; $this->cacheDocumentsRequest = $cacheDocumentsRequest; $this->actorRequest = $actorRequest; $this->streamRequest = $streamRequest; $this->configService = $configService; $this->cacheService = $cacheService; $this->miscService = $miscService; } /** * @param string $id * @param bool $public * * @return Document * @throws CacheDocumentDoesNotExistException * @throws MalformedArrayException * @throws SocialAppConfigException */ public function cacheRemoteDocument(string $id, bool $public = false) { $document = $this->cacheDocumentsRequest->getById($id, $public); if ($document->getError() > 0) { throw new CacheDocumentDoesNotExistException(); } if ($document->getLocalCopy() !== '') { return $document; } if ($document->getCaching() > (time() - (CacheDocumentsRequest::CACHING_TIMEOUT * 60))) { return $document; } $mime = ''; $this->cacheDocumentsRequest->initCaching($document); try { $this->cacheService->saveRemoteFileToCache($document, $mime); $this->cacheDocumentsRequest->endCaching($document); $this->streamRequest->updateAttachments($document); return $document; } catch (CacheContentMimeTypeException $e) { $this->miscService->log( 'Not allowed mime type ' . json_encode($document) . ' ' . json_encode($e), 1 ); $document->setMimeType($mime); $document->setError(self::ERROR_MIMETYPE); $this->cacheDocumentsRequest->endCaching($document); } catch (NotFoundException $e) { $this->miscService->log( 'Cannot save cache file ' . json_encode($document) . ' ' . json_encode($e), 1 ); $document->setError(self::ERROR_PERMISSION); $this->cacheDocumentsRequest->endCaching($document); } catch (NotPermittedException $e) { $this->miscService->log( 'Cannot save cache file ' . json_encode($document) . ' ' . json_encode($e), 1 ); $document->setError(self::ERROR_PERMISSION); $this->cacheDocumentsRequest->endCaching($document); } catch (RequestResultSizeException $e) { $this->miscService->log( 'Downloaded file is too big ' . json_encode($document) . ' ' . json_encode($e), 1 ); $document->setError(self::ERROR_SIZE); $this->cacheDocumentsRequest->endCaching($document); } catch (RequestContentException $e) { $this->cacheDocumentsRequest->deleteById($document->getId()); } catch (UnauthorizedFediverseException $e) { $this->cacheDocumentsRequest->deleteById($document->getId()); } catch (RequestNetworkException $e) { $this->cacheDocumentsRequest->endCaching($document); } catch (RequestServerException $e) { $this->cacheDocumentsRequest->endCaching($document); } throw new CacheDocumentDoesNotExistException(); } /** * @param string $id * @param string $mime * @param bool $public * * @return ISimpleFile * @throws CacheContentException * @throws CacheDocumentDoesNotExistException * @throws MalformedArrayException * @throws RequestResultNotJsonException * @throws SocialAppConfigException */ public function getResizedFromCache(string $id, string &$mime = '', bool $public = false) { $document = $this->cacheRemoteDocument($id, $public); $mime = $document->getMimeType(); return $this->cacheService->getContentFromCache($document->getResizedCopy()); } /** * @param string $id * @param bool $public * @param string $mimeType * * @return ISimpleFile * @throws CacheContentException * @throws CacheDocumentDoesNotExistException * @throws MalformedArrayException * @throws SocialAppConfigException */ public function getFromCache( string $id, string &$mimeType = '', bool $public = false ): ISimpleFile { $document = $this->cacheRemoteDocument($id, $public); $mimeType = $document->getMimeType(); return $this->cacheService->getContentFromCache($document->getLocalCopy()); } /** * @param string $uuid * * @return ISimpleFile * @throws NotFoundException */ public function getFromUuid(string $uuid): ISimpleFile { if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid) !== 1) { throw new NotFoundException('invalid document'); } return $this->cacheService->getFromUuid($uuid); } /** * @param array $getMediaIds * @param string $account * * @return Document[] */ public function getMediaFromArray(array $getMediaIds, string $account = ''): array { return $this->cacheDocumentsRequest->getFromArray($getMediaIds, $account); } /** * @return int * @throws Exception */ public function manageCacheDocuments(): int { $update = $this->cacheDocumentsRequest->getNotCachedDocuments(); $count = 0; foreach ($update as $item) { if ($item->getLocalCopy() === 'avatar') { continue; } try { $this->cacheRemoteDocument($item->getId()); } catch (Exception $e) { continue; } $count++; } return $count; } /** * @param Person $actor * * @return string * @throws SocialAppConfigException * @throws UrlCloudException * @throws ItemUnknownException * @throws ItemAlreadyExistsException */ public function cacheLocalAvatarByUsername(Person $actor): string { $url = $this->urlGenerator->linkToRouteAbsolute( 'core.avatar.getAvatar', ['userId' => $actor->getUserId(), 'size' => 128] ); $versionCurrent = (int)$this->configService->getUserValue('version', $actor->getUserId(), 'avatar'); $versionCached = $actor->getAvatarVersion(); if ($versionCurrent > $versionCached) { /** @var Image $icon */ $icon = AP::$activityPub->getItemFromType(Image::TYPE); $icon->generateUniqueId('/documents/avatar'); $icon->setUrl($url); $icon->setMediaType(''); $icon->setLocalCopy('avatar'); $interface = AP::$activityPub->getInterfaceFromType(Image::TYPE); $interface->save($icon); $actor->setAvatarVersion($versionCurrent); $this->actorRequest->update($actor); } else { try { $icon = $this->cacheDocumentsRequest->getByUrl($url); } catch (CacheDocumentDoesNotExistException $e) { return ''; } } return $icon->getId(); } }