diff options
Diffstat (limited to 'apps/dav/lib')
-rw-r--r-- | apps/dav/lib/CalDAV/CalDavBackend.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/FilesPlugin.php | 9 | ||||
-rw-r--r-- | apps/dav/lib/Controller/ExampleContentController.php | 80 | ||||
-rw-r--r-- | apps/dav/lib/Listener/UserEventsListener.php | 6 | ||||
-rw-r--r-- | apps/dav/lib/Server.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/Service/DefaultContactService.php | 77 | ||||
-rw-r--r-- | apps/dav/lib/Service/ExampleContactService.php | 132 | ||||
-rw-r--r-- | apps/dav/lib/Settings/ExampleContentSettings.php | 14 | ||||
-rw-r--r-- | apps/dav/lib/Upload/UploadAutoMkcolPlugin.php | 68 |
9 files changed, 236 insertions, 154 deletions
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index c49e843d2b9..1cbf2a1e4eb 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -2986,7 +2986,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'calendarid' => $query->createNamedParameter($calendarId), 'operation' => $query->createNamedParameter($operation), 'calendartype' => $query->createNamedParameter($calendarType), - 'created_at' => time(), + 'created_at' => $query->createNamedParameter(time()), ]); foreach ($objectUris as $uri) { $query->setParameter('uri', $uri); diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 9e2affddb6b..5364b7cff41 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -9,6 +9,7 @@ namespace OCA\DAV\Connector\Sabre; use OC\AppFramework\Http\Request; use OC\FilesMetadata\Model\FilesMetadata; +use OC\User\NoUserException; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCA\Files_Sharing\External\Mount as SharingExternalMount; use OCP\Accounts\IAccountManager; @@ -374,7 +375,13 @@ class FilesPlugin extends ServerPlugin { } // Check if the user published their display name - $ownerAccount = $this->accountManager->getAccount($owner); + try { + $ownerAccount = $this->accountManager->getAccount($owner); + } catch (NoUserException) { + // do not lock process if owner is not local + return null; + } + $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); // Since we are not logged in, we need to have at least the published scope diff --git a/apps/dav/lib/Controller/ExampleContentController.php b/apps/dav/lib/Controller/ExampleContentController.php index 905fd392e6c..e20ee4b7f49 100644 --- a/apps/dav/lib/Controller/ExampleContentController.php +++ b/apps/dav/lib/Controller/ExampleContentController.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace OCA\DAV\Controller; use OCA\DAV\AppInfo\Application; +use OCA\DAV\Service\ExampleContactService; use OCA\DAV\Service\ExampleEventService; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; @@ -17,103 +18,50 @@ use OCP\AppFramework\Http\Attribute\FrontpageRoute; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\DataDownloadResponse; use OCP\AppFramework\Http\JSONResponse; -use OCP\Files\AppData\IAppDataFactory; -use OCP\Files\IAppData; -use OCP\Files\NotFoundException; -use OCP\IAppConfig; -use OCP\IConfig; use OCP\IRequest; use Psr\Log\LoggerInterface; class ExampleContentController extends ApiController { - private IAppData $appData; - public function __construct( IRequest $request, - private IConfig $config, - private IAppConfig $appConfig, - private IAppDataFactory $appDataFactory, - private LoggerInterface $logger, - private ExampleEventService $exampleEventService, + private readonly LoggerInterface $logger, + private readonly ExampleEventService $exampleEventService, + private readonly ExampleContactService $exampleContactService, ) { parent::__construct(Application::APP_ID, $request); - $this->appData = $this->appDataFactory->get('dav'); } - public function setEnableDefaultContact($allow) { - if ($allow === 'yes' && !$this->defaultContactExists()) { + #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/config')] + public function setEnableDefaultContact(bool $allow): JSONResponse { + if ($allow && !$this->exampleContactService->defaultContactExists()) { try { - $this->setCard(); + $this->exampleContactService->setCard(); } catch (\Exception $e) { $this->logger->error('Could not create default contact', ['exception' => $e]); return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); } } - $this->config->setAppValue(Application::APP_ID, 'enableDefaultContact', $allow); + $this->exampleContactService->setDefaultContactEnabled($allow); return new JSONResponse([], Http::STATUS_OK); } #[NoCSRFRequired] + #[FrontpageRoute(verb: 'GET', url: '/api/defaultcontact/contact')] public function getDefaultContact(): DataDownloadResponse { - $cardData = $this->getCard() + $cardData = $this->exampleContactService->getCard() ?? file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf'); return new DataDownloadResponse($cardData, 'example_contact.vcf', 'text/vcard'); } + #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/contact')] public function setDefaultContact(?string $contactData = null) { - if (!$this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'yes')) { + if (!$this->exampleContactService->isDefaultContactEnabled()) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $this->setCard($contactData); + $this->exampleContactService->setCard($contactData); return new JSONResponse([], Http::STATUS_OK); } - private function getCard(): ?string { - try { - $folder = $this->appData->getFolder('defaultContact'); - } catch (NotFoundException $e) { - return null; - } - - if (!$folder->fileExists('defaultContact.vcf')) { - return null; - } - - return $folder->getFile('defaultContact.vcf')->getContent(); - } - - private function setCard(?string $cardData = null) { - try { - $folder = $this->appData->getFolder('defaultContact'); - } catch (NotFoundException $e) { - $folder = $this->appData->newFolder('defaultContact'); - } - - $isCustom = true; - if (is_null($cardData)) { - $cardData = file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf'); - $isCustom = false; - } - - if (!$cardData) { - throw new \Exception('Could not read exampleContact.vcf'); - } - - $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf'); - $file->putContent($cardData); - - $this->appConfig->setValueBool(Application::APP_ID, 'hasCustomDefaultContact', $isCustom); - } - - private function defaultContactExists(): bool { - try { - $folder = $this->appData->getFolder('defaultContact'); - } catch (NotFoundException $e) { - return false; - } - return $folder->fileExists('defaultContact.vcf'); - } - #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/enable')] public function setCreateExampleEvent(bool $enable): JSONResponse { $this->exampleEventService->setCreateExampleEvent($enable); diff --git a/apps/dav/lib/Listener/UserEventsListener.php b/apps/dav/lib/Listener/UserEventsListener.php index 67cf228515a..c876192d67f 100644 --- a/apps/dav/lib/Listener/UserEventsListener.php +++ b/apps/dav/lib/Listener/UserEventsListener.php @@ -12,7 +12,7 @@ namespace OCA\DAV\Listener; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\SyncService; -use OCA\DAV\Service\DefaultContactService; +use OCA\DAV\Service\ExampleContactService; use OCA\DAV\Service\ExampleEventService; use OCP\Accounts\UserUpdatedEvent; use OCP\Defaults; @@ -46,7 +46,7 @@ class UserEventsListener implements IEventListener { private CalDavBackend $calDav, private CardDavBackend $cardDav, private Defaults $themingDefaults, - private DefaultContactService $defaultContactService, + private ExampleContactService $exampleContactService, private ExampleEventService $exampleEventService, private LoggerInterface $logger, ) { @@ -175,7 +175,7 @@ class UserEventsListener implements IEventListener { } } if ($addressBookId) { - $this->defaultContactService->createDefaultContact($addressBookId); + $this->exampleContactService->createDefaultContact($addressBookId); } } } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index d1029afa262..18d0be45938 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -63,6 +63,7 @@ use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin; use OCA\DAV\SystemTag\SystemTagPlugin; use OCA\DAV\Upload\ChunkingPlugin; use OCA\DAV\Upload\ChunkingV2Plugin; +use OCA\DAV\Upload\UploadAutoMkcolPlugin; use OCA\Theming\ThemingDefaults; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; @@ -232,6 +233,7 @@ class Server { $this->server->addPlugin(new CopyEtagHeaderPlugin()); $this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class))); + $this->server->addPlugin(new UploadAutoMkcolPlugin()); $this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class))); $this->server->addPlugin(new ChunkingPlugin()); $this->server->addPlugin(new ZipFolderPlugin( diff --git a/apps/dav/lib/Service/DefaultContactService.php b/apps/dav/lib/Service/DefaultContactService.php deleted file mode 100644 index 24e55ef7b69..00000000000 --- a/apps/dav/lib/Service/DefaultContactService.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace OCA\DAV\Service; - -use OCA\DAV\AppInfo\Application; -use OCA\DAV\CardDAV\CardDavBackend; -use OCP\App\IAppManager; -use OCP\Files\AppData\IAppDataFactory; -use OCP\IAppConfig; -use Psr\Log\LoggerInterface; -use Symfony\Component\Uid\Uuid; - -class DefaultContactService { - public function __construct( - private CardDavBackend $cardDav, - private IAppManager $appManager, - private IAppDataFactory $appDataFactory, - private IAppConfig $config, - private LoggerInterface $logger, - ) { - } - - public function createDefaultContact(int $addressBookId): void { - $enableDefaultContact = $this->config->getValueString(Application::APP_ID, 'enableDefaultContact', 'yes'); - if ($enableDefaultContact !== 'yes') { - return; - } - $appData = $this->appDataFactory->get('dav'); - try { - $folder = $appData->getFolder('defaultContact'); - $defaultContactFile = $folder->getFile('defaultContact.vcf'); - $data = $defaultContactFile->getContent(); - } catch (\Exception $e) { - $this->logger->error('Couldn\'t get default contact file', ['exception' => $e]); - return; - } - - // Make sure the UID is unique - $newUid = Uuid::v4()->toRfc4122(); - $newRev = date('Ymd\THis\Z'); - $vcard = \Sabre\VObject\Reader::read($data, \Sabre\VObject\Reader::OPTION_FORGIVING); - if ($vcard->UID) { - $vcard->UID->setValue($newUid); - } else { - $vcard->add('UID', $newUid); - } - if ($vcard->REV) { - $vcard->REV->setValue($newRev); - } else { - $vcard->add('REV', $newRev); - } - - // Level 3 means that the document is invalid - // https://sabre.io/vobject/vcard/#validating-vcard - $level3Warnings = array_filter($vcard->validate(), function ($warning) { - return $warning['level'] === 3; - }); - - if (!empty($level3Warnings)) { - $this->logger->error('Default contact is invalid', ['warnings' => $level3Warnings]); - return; - } - try { - $this->cardDav->createCard($addressBookId, 'default', $vcard->serialize(), false); - } catch (\Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - } - - } -} diff --git a/apps/dav/lib/Service/ExampleContactService.php b/apps/dav/lib/Service/ExampleContactService.php new file mode 100644 index 00000000000..6ed6c66cbb3 --- /dev/null +++ b/apps/dav/lib/Service/ExampleContactService.php @@ -0,0 +1,132 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Service; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CardDAV\CardDavBackend; +use OCP\AppFramework\Services\IAppConfig; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use Psr\Log\LoggerInterface; +use Symfony\Component\Uid\Uuid; + +class ExampleContactService { + private readonly IAppData $appData; + + public function __construct( + IAppDataFactory $appDataFactory, + private readonly IAppConfig $appConfig, + private readonly LoggerInterface $logger, + private readonly CardDavBackend $cardDav, + ) { + $this->appData = $appDataFactory->get(Application::APP_ID); + } + + public function isDefaultContactEnabled(): bool { + return $this->appConfig->getAppValueBool('enableDefaultContact', true); + } + + public function setDefaultContactEnabled(bool $value): void { + $this->appConfig->setAppValueBool('enableDefaultContact', $value); + } + + public function getCard(): ?string { + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + return null; + } + + if (!$folder->fileExists('defaultContact.vcf')) { + return null; + } + + return $folder->getFile('defaultContact.vcf')->getContent(); + } + + public function setCard(?string $cardData = null) { + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder('defaultContact'); + } + + $isCustom = true; + if (is_null($cardData)) { + $cardData = file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf'); + $isCustom = false; + } + + if (!$cardData) { + throw new \Exception('Could not read exampleContact.vcf'); + } + + $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf'); + $file->putContent($cardData); + + $this->appConfig->setAppValueBool('hasCustomDefaultContact', $isCustom); + } + + public function defaultContactExists(): bool { + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + return false; + } + return $folder->fileExists('defaultContact.vcf'); + } + + public function createDefaultContact(int $addressBookId): void { + if (!$this->isDefaultContactEnabled()) { + return; + } + + try { + $folder = $this->appData->getFolder('defaultContact'); + $defaultContactFile = $folder->getFile('defaultContact.vcf'); + $data = $defaultContactFile->getContent(); + } catch (\Exception $e) { + $this->logger->error('Couldn\'t get default contact file', ['exception' => $e]); + return; + } + + // Make sure the UID is unique + $newUid = Uuid::v4()->toRfc4122(); + $newRev = date('Ymd\THis\Z'); + $vcard = \Sabre\VObject\Reader::read($data, \Sabre\VObject\Reader::OPTION_FORGIVING); + if ($vcard->UID) { + $vcard->UID->setValue($newUid); + } else { + $vcard->add('UID', $newUid); + } + if ($vcard->REV) { + $vcard->REV->setValue($newRev); + } else { + $vcard->add('REV', $newRev); + } + + // Level 3 means that the document is invalid + // https://sabre.io/vobject/vcard/#validating-vcard + $level3Warnings = array_filter($vcard->validate(), static function ($warning) { + return $warning['level'] === 3; + }); + + if (!empty($level3Warnings)) { + $this->logger->error('Default contact is invalid', ['warnings' => $level3Warnings]); + return; + } + try { + $this->cardDav->createCard($addressBookId, 'default', $vcard->serialize(), false); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } +} diff --git a/apps/dav/lib/Settings/ExampleContentSettings.php b/apps/dav/lib/Settings/ExampleContentSettings.php index fef2d25b8d2..7b6f9b03a3a 100644 --- a/apps/dav/lib/Settings/ExampleContentSettings.php +++ b/apps/dav/lib/Settings/ExampleContentSettings.php @@ -9,21 +9,21 @@ declare(strict_types=1); namespace OCA\DAV\Settings; use OCA\DAV\AppInfo\Application; +use OCA\DAV\Service\ExampleContactService; use OCA\DAV\Service\ExampleEventService; use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Services\IInitialState; -use OCP\IAppConfig; -use OCP\IConfig; use OCP\Settings\ISettings; class ExampleContentSettings implements ISettings { public function __construct( - private readonly IConfig $config, private readonly IAppConfig $appConfig, private readonly IInitialState $initialState, private readonly IAppManager $appManager, private readonly ExampleEventService $exampleEventService, + private readonly ExampleContactService $exampleContactService, ) { } @@ -43,11 +43,13 @@ class ExampleContentSettings implements ISettings { } if ($contactsEnabled) { - $enableDefaultContact = $this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'yes'); - $this->initialState->provideInitialState('enableDefaultContact', $enableDefaultContact); + $this->initialState->provideInitialState( + 'enableDefaultContact', + $this->exampleContactService->isDefaultContactEnabled(), + ); $this->initialState->provideInitialState( 'hasCustomDefaultContact', - $this->appConfig->getValueBool(Application::APP_ID, 'hasCustomDefaultContact'), + $this->appConfig->getAppValueBool('hasCustomDefaultContact'), ); } diff --git a/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php b/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php new file mode 100644 index 00000000000..a7030ba1133 --- /dev/null +++ b/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Upload; + +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use function Sabre\Uri\split as uriSplit; + +/** + * Class that allows automatically creating non-existing collections on file + * upload. + * + * Since this functionality is not WebDAV compliant, it needs a special + * header to be activated. + */ +class UploadAutoMkcolPlugin extends ServerPlugin { + + private Server $server; + + public function initialize(Server $server): void { + $server->on('beforeMethod:PUT', [$this, 'beforeMethod']); + $this->server = $server; + } + + /** + * @throws NotFound a node expected to exist cannot be found + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response): bool { + if ($request->getHeader('X-NC-WebDAV-Auto-Mkcol') !== '1') { + return true; + } + + [$path,] = uriSplit($request->getPath()); + + if ($this->server->tree->nodeExists($path)) { + return true; + } + + $parts = explode('/', trim($path, '/')); + $rootPath = array_shift($parts); + $node = $this->server->tree->getNodeForPath('/' . $rootPath); + + if (!($node instanceof ICollection)) { + // the root node is not a collection, let SabreDAV handle it + return true; + } + + foreach ($parts as $part) { + if (!$node->childExists($part)) { + $node->createDirectory($part); + } + + $node = $node->getChild($part); + } + + return true; + } +} |