aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/lib')
-rw-r--r--apps/files/lib/Activity/FavoriteProvider.php69
-rw-r--r--apps/files/lib/Activity/Filter/Favorites.php50
-rw-r--r--apps/files/lib/Activity/Filter/FileChanges.php36
-rw-r--r--apps/files/lib/Activity/Helper.php89
-rw-r--r--apps/files/lib/Activity/Provider.php125
-rw-r--r--apps/files/lib/Activity/Settings/FavoriteAction.php27
-rw-r--r--apps/files/lib/Activity/Settings/FileActivitySettings.php29
-rw-r--r--apps/files/lib/Activity/Settings/FileChanged.php27
-rw-r--r--apps/files/lib/Activity/Settings/FileFavoriteChanged.php27
-rw-r--r--apps/files/lib/AdvancedCapabilities.php38
-rw-r--r--apps/files/lib/App.php36
-rw-r--r--apps/files/lib/AppInfo/Application.php119
-rw-r--r--apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php35
-rw-r--r--apps/files/lib/BackgroundJob/CleanupFileLocks.php40
-rw-r--r--apps/files/lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php30
-rw-r--r--apps/files/lib/BackgroundJob/DeleteOrphanedItems.php137
-rw-r--r--apps/files/lib/BackgroundJob/ScanFiles.php106
-rw-r--r--apps/files/lib/BackgroundJob/TransferOwnership.php78
-rw-r--r--apps/files/lib/Capabilities.php59
-rw-r--r--apps/files/lib/Collaboration/Resources/Listener.php36
-rw-r--r--apps/files/lib/Collaboration/Resources/ResourceProvider.php64
-rw-r--r--apps/files/lib/Command/Copy.php116
-rw-r--r--apps/files/lib/Command/Delete.php99
-rw-r--r--apps/files/lib/Command/DeleteOrphanedFiles.php162
-rw-r--r--apps/files/lib/Command/Get.php71
-rw-r--r--apps/files/lib/Command/Move.php106
-rw-r--r--apps/files/lib/Command/Object/Delete.php60
-rw-r--r--apps/files/lib/Command/Object/Get.php63
-rw-r--r--apps/files/lib/Command/Object/Info.php80
-rw-r--r--apps/files/lib/Command/Object/ListObject.php50
-rw-r--r--apps/files/lib/Command/Object/Multi/Rename.php108
-rw-r--r--apps/files/lib/Command/Object/Multi/Users.php98
-rw-r--r--apps/files/lib/Command/Object/ObjectUtil.php115
-rw-r--r--apps/files/lib/Command/Object/Orphans.php79
-rw-r--r--apps/files/lib/Command/Object/Put.php68
-rw-r--r--apps/files/lib/Command/Put.php67
-rw-r--r--apps/files/lib/Command/RepairTree.php39
-rw-r--r--apps/files/lib/Command/SanitizeFilenames.php151
-rw-r--r--apps/files/lib/Command/Scan.php263
-rw-r--r--apps/files/lib/Command/ScanAppData.php113
-rw-r--r--apps/files/lib/Command/TransferOwnership.php139
-rw-r--r--apps/files/lib/Command/WindowsCompatibleFilenames.php52
-rw-r--r--apps/files/lib/Controller/ApiController.php363
-rw-r--r--apps/files/lib/Controller/ConversionApiController.php109
-rw-r--r--apps/files/lib/Controller/DirectEditingController.php129
-rw-r--r--apps/files/lib/Controller/DirectEditingViewController.php59
-rw-r--r--apps/files/lib/Controller/OpenLocalEditorController.php70
-rw-r--r--apps/files/lib/Controller/TemplateController.php115
-rw-r--r--apps/files/lib/Controller/TransferOwnershipController.php132
-rw-r--r--apps/files/lib/Controller/ViewController.php459
-rw-r--r--apps/files/lib/Dashboard/FavoriteWidget.php141
-rw-r--r--apps/files/lib/Db/OpenLocalEditor.php21
-rw-r--r--apps/files/lib/Db/OpenLocalEditorMapper.php24
-rw-r--r--apps/files/lib/Db/TransferOwnership.php21
-rw-r--r--apps/files/lib/Db/TransferOwnershipMapper.php25
-rw-r--r--apps/files/lib/DirectEditingCapabilities.php39
-rw-r--r--apps/files/lib/Event/LoadAdditionalScriptsEvent.php33
-rw-r--r--apps/files/lib/Event/LoadSearchPlugins.php14
-rw-r--r--apps/files/lib/Event/LoadSidebar.php21
-rw-r--r--apps/files/lib/Exception/TransferOwnershipException.php21
-rw-r--r--apps/files/lib/Helper.php190
-rw-r--r--apps/files/lib/Listener/LegacyLoadAdditionalScriptsAdapter.php57
-rw-r--r--apps/files/lib/Listener/LoadSearchPluginsListener.php25
-rw-r--r--apps/files/lib/Listener/LoadSidebarListener.php25
-rw-r--r--apps/files/lib/Listener/NodeAddedToFavoriteListener.php43
-rw-r--r--apps/files/lib/Listener/NodeRemovedFromFavoriteListener.php43
-rw-r--r--apps/files/lib/Listener/RenderReferenceEventListener.php25
-rw-r--r--apps/files/lib/Listener/SyncLivePhotosListener.php254
-rw-r--r--apps/files/lib/Migration/Version11301Date20191205150729.php22
-rw-r--r--apps/files/lib/Migration/Version12101Date20221011153334.php23
-rw-r--r--apps/files/lib/Migration/Version2003Date20241021095629.php36
-rw-r--r--apps/files/lib/Notification/Notifier.php129
-rw-r--r--apps/files/lib/ResponseDefinitions.php75
-rw-r--r--apps/files/lib/Search/FilesSearchProvider.php169
-rw-r--r--apps/files/lib/Service/ChunkedUploadConfig.php30
-rw-r--r--apps/files/lib/Service/DirectEditingService.php35
-rw-r--r--apps/files/lib/Service/LivePhotosService.php36
-rw-r--r--apps/files/lib/Service/OwnershipTransferService.php478
-rw-r--r--apps/files/lib/Service/SettingsService.php63
-rw-r--r--apps/files/lib/Service/TagService.php115
-rw-r--r--apps/files/lib/Service/UserConfig.php94
-rw-r--r--apps/files/lib/Service/ViewConfig.php168
-rw-r--r--apps/files/lib/Settings/DeclarativeAdminSettings.php67
-rw-r--r--apps/files/lib/Settings/PersonalSettings.php23
84 files changed, 4622 insertions, 2755 deletions
diff --git a/apps/files/lib/Activity/FavoriteProvider.php b/apps/files/lib/Activity/FavoriteProvider.php
index 9c7018e6a5c..e56b13b902a 100644
--- a/apps/files/lib/Activity/FavoriteProvider.php
+++ b/apps/files/lib/Activity/FavoriteProvider.php
@@ -1,28 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -35,32 +19,21 @@ class FavoriteProvider implements IProvider {
public const SUBJECT_ADDED = 'added_favorite';
public const SUBJECT_REMOVED = 'removed_favorite';
- /** @var IFactory */
- protected $languageFactory;
-
/** @var IL10N */
protected $l;
- /** @var IURLGenerator */
- protected $url;
-
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
/**
* @param IFactory $languageFactory
* @param IURLGenerator $url
* @param IManager $activityManager
* @param IEventMerger $eventMerger
*/
- public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IEventMerger $eventMerger) {
- $this->languageFactory = $languageFactory;
- $this->url = $url;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
+ public function __construct(
+ protected IFactory $languageFactory,
+ protected IURLGenerator $url,
+ protected IManager $activityManager,
+ protected IEventMerger $eventMerger,
+ ) {
}
/**
@@ -68,12 +41,12 @@ class FavoriteProvider implements IProvider {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
- public function parse($language, IEvent $event, IEvent $previousEvent = null) {
+ public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'files' || $event->getType() !== 'favorite') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('files', $language);
@@ -81,7 +54,7 @@ class FavoriteProvider implements IProvider {
if ($this->activityManager->isFormattingFilteredObject()) {
try {
return $this->parseShortVersion($event);
- } catch (\InvalidArgumentException $e) {
+ } catch (UnknownActivityException) {
// Ignore and simply use the long version...
}
}
@@ -92,10 +65,10 @@ class FavoriteProvider implements IProvider {
/**
* @param IEvent $event
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
- public function parseShortVersion(IEvent $event) {
+ public function parseShortVersion(IEvent $event): IEvent {
if ($event->getSubject() === self::SUBJECT_ADDED) {
$event->setParsedSubject($this->l->t('Added to favorites'));
if ($this->activityManager->getRequirePNG()) {
@@ -112,7 +85,7 @@ class FavoriteProvider implements IProvider {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/star.svg')));
}
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
return $event;
@@ -122,10 +95,10 @@ class FavoriteProvider implements IProvider {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
- public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) {
+ public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null): IEvent {
if ($event->getSubject() === self::SUBJECT_ADDED) {
$subject = $this->l->t('You added {file} to your favorites');
if ($this->activityManager->getRequirePNG()) {
@@ -142,7 +115,7 @@ class FavoriteProvider implements IProvider {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/star.svg')));
}
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->setSubjects($event, $subject);
@@ -166,7 +139,7 @@ class FavoriteProvider implements IProvider {
}
$parameter = [
'type' => 'file',
- 'id' => $subjectParams['id'],
+ 'id' => (string)$subjectParams['id'],
'name' => basename($subjectParams['path']),
'path' => trim($subjectParams['path'], '/'),
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $subjectParams['id']]),
diff --git a/apps/files/lib/Activity/Filter/Favorites.php b/apps/files/lib/Activity/Filter/Favorites.php
index 04f906b2f97..0159dd20b82 100644
--- a/apps/files/lib/Activity/Filter/Favorites.php
+++ b/apps/files/lib/Activity/Filter/Favorites.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity\Filter;
@@ -32,21 +16,6 @@ use OCP\IURLGenerator;
class Favorites implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
- /** @var IManager */
- protected $activityManager;
-
- /** @var Helper */
- protected $helper;
-
- /** @var IDBConnection */
- protected $db;
-
/**
* @param IL10N $l
* @param IURLGenerator $url
@@ -54,12 +23,13 @@ class Favorites implements IFilter {
* @param Helper $helper
* @param IDBConnection $db
*/
- public function __construct(IL10N $l, IURLGenerator $url, IManager $activityManager, Helper $helper, IDBConnection $db) {
- $this->l = $l;
- $this->url = $url;
- $this->activityManager = $activityManager;
- $this->helper = $helper;
- $this->db = $db;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ protected IManager $activityManager,
+ protected Helper $helper,
+ protected IDBConnection $db,
+ ) {
}
/**
diff --git a/apps/files/lib/Activity/Filter/FileChanges.php b/apps/files/lib/Activity/Filter/FileChanges.php
index 2950e7c46df..0ca8f6792e0 100644
--- a/apps/files/lib/Activity/Filter/FileChanges.php
+++ b/apps/files/lib/Activity/Filter/FileChanges.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity\Filter;
@@ -29,19 +12,14 @@ use OCP\IURLGenerator;
class FileChanges implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
/**
* @param IL10N $l
* @param IURLGenerator $url
*/
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
diff --git a/apps/files/lib/Activity/Helper.php b/apps/files/lib/Activity/Helper.php
index b9a5ae887ec..9b8ad9cd442 100644
--- a/apps/files/lib/Activity/Helper.php
+++ b/apps/files/lib/Activity/Helper.php
@@ -1,52 +1,37 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\Activity;
use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
use OCP\ITagManager;
class Helper {
/** If a user has a lot of favorites the query might get too slow and long */
public const FAVORITE_LIMIT = 50;
- /** @var ITagManager */
- protected $tagManager;
-
- /**
- * @param ITagManager $tagManager
- */
- public function __construct(ITagManager $tagManager) {
- $this->tagManager = $tagManager;
+ public function __construct(
+ protected ITagManager $tagManager,
+ protected IRootFolder $rootFolder,
+ ) {
}
/**
- * Returns an array with the favorites
+ * Return an array with nodes marked as favorites
*
- * @param string $user
- * @return array
+ * @param string $user User ID
+ * @param bool $foldersOnly Only return folders (default false)
+ * @return Node[]
+ * @psalm-return ($foldersOnly is true ? Folder[] : Node[])
* @throws \RuntimeException when too many or no favorites where found
*/
- public function getFavoriteFilePaths($user) {
+ public function getFavoriteNodes(string $user, bool $foldersOnly = false): array {
$tags = $this->tagManager->load('files', [], false, $user);
$favorites = $tags->getFavorites();
@@ -57,26 +42,44 @@ class Helper {
}
// Can not DI because the user is not known on instantiation
- $rootFolder = \OC::$server->getUserFolder($user);
- $folders = $items = [];
+ $userFolder = $this->rootFolder->getUserFolder($user);
+ $favoriteNodes = [];
foreach ($favorites as $favorite) {
- $nodes = $rootFolder->getById($favorite);
- if (!empty($nodes)) {
- /** @var \OCP\Files\Node $node */
- $node = array_shift($nodes);
- $path = substr($node->getPath(), strlen($user . '/files/'));
-
- $items[] = $path;
- if ($node instanceof Folder) {
- $folders[] = $path;
+ $node = $userFolder->getFirstNodeById($favorite);
+ if ($node) {
+ if (!$foldersOnly || $node instanceof Folder) {
+ $favoriteNodes[] = $node;
}
}
}
- if (empty($items)) {
+ if (empty($favoriteNodes)) {
throw new \RuntimeException('No favorites', 1);
}
+ return $favoriteNodes;
+ }
+
+ /**
+ * Returns an array with the favorites
+ *
+ * @param string $user
+ * @return array
+ * @throws \RuntimeException when too many or no favorites where found
+ */
+ public function getFavoriteFilePaths(string $user): array {
+ $userFolder = $this->rootFolder->getUserFolder($user);
+ $nodes = $this->getFavoriteNodes($user);
+ $folders = $items = [];
+ foreach ($nodes as $node) {
+ $path = $userFolder->getRelativePath($node->getPath());
+
+ $items[] = $path;
+ if ($node instanceof Folder) {
+ $folders[] = $path;
+ }
+ }
+
return [
'items' => $items,
'folders' => $folders,
diff --git a/apps/files/lib/Activity/Provider.php b/apps/files/lib/Activity/Provider.php
index 2cfd48ede3b..3ef79ac107f 100644
--- a/apps/files/lib/Activity/Provider.php
+++ b/apps/files/lib/Activity/Provider.php
@@ -1,30 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -42,56 +24,24 @@ use OCP\IUserManager;
use OCP\L10N\IFactory;
class Provider implements IProvider {
- /** @var IFactory */
- protected $languageFactory;
-
/** @var IL10N */
protected $l;
- /** @var IL10N */
- protected $activityLang;
-
- /** @var IURLGenerator */
- protected $url;
-
- /** @var IManager */
- protected $activityManager;
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IRootFolder */
- protected $rootFolder;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
- /** @var ICloudIdManager */
- protected $cloudIdManager;
-
- /** @var IContactsManager */
- protected $contactsManager;
/** @var string[] cached displayNames - key is the cloud id and value the displayname */
protected $displayNames = [];
protected $fileIsEncrypted = false;
- public function __construct(IFactory $languageFactory,
- IURLGenerator $url,
- IManager $activityManager,
- IUserManager $userManager,
- IRootFolder $rootFolder,
- ICloudIdManager $cloudIdManager,
- IContactsManager $contactsManager,
- IEventMerger $eventMerger) {
- $this->languageFactory = $languageFactory;
- $this->url = $url;
- $this->activityManager = $activityManager;
- $this->userManager = $userManager;
- $this->rootFolder = $rootFolder;
- $this->cloudIdManager = $cloudIdManager;
- $this->contactsManager = $contactsManager;
- $this->eventMerger = $eventMerger;
+ public function __construct(
+ protected IFactory $languageFactory,
+ protected IURLGenerator $url,
+ protected IManager $activityManager,
+ protected IUserManager $userManager,
+ protected IRootFolder $rootFolder,
+ protected ICloudIdManager $cloudIdManager,
+ protected IContactsManager $contactsManager,
+ protected IEventMerger $eventMerger,
+ ) {
}
/**
@@ -99,21 +49,20 @@ class Provider implements IProvider {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
- public function parse($language, IEvent $event, IEvent $previousEvent = null) {
+ public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'files') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('files', $language);
- $this->activityLang = $this->languageFactory->get('activity', $language);
if ($this->activityManager->isFormattingFilteredObject()) {
try {
return $this->parseShortVersion($event, $previousEvent);
- } catch (\InvalidArgumentException $e) {
+ } catch (UnknownActivityException) {
// Ignore and simply use the long version...
}
}
@@ -133,10 +82,10 @@ class Provider implements IProvider {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
- public function parseShortVersion(IEvent $event, IEvent $previousEvent = null) {
+ public function parseShortVersion(IEvent $event, ?IEvent $previousEvent = null): IEvent {
$parsedParameters = $this->getParameters($event);
if ($event->getSubject() === 'created_by') {
@@ -158,12 +107,12 @@ class Provider implements IProvider {
$subject = $this->l->t('Moved by {user}');
$this->setIcon($event, 'change');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
if (!isset($parsedParameters['user'])) {
// External user via public link share
- $subject = str_replace('{user}', $this->activityLang->t('"remote user"'), $subject);
+ $subject = str_replace('{user}', $this->l->t('"remote account"'), $subject);
}
$this->setSubjects($event, $subject, $parsedParameters);
@@ -175,10 +124,10 @@ class Provider implements IProvider {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
- public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) {
+ public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null): IEvent {
$this->fileIsEncrypted = false;
$parsedParameters = $this->getParameters($event);
@@ -272,7 +221,7 @@ class Provider implements IProvider {
$subject = $this->l->t('{user} moved {oldfile} to {newfile}');
$this->setIcon($event, 'change');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
if ($this->fileIsEncrypted) {
@@ -281,7 +230,7 @@ class Provider implements IProvider {
if (!isset($parsedParameters['user'])) {
// External user via public link share
- $subject = str_replace('{user}', $this->activityLang->t('"remote user"'), $subject);
+ $subject = str_replace('{user}', $this->l->t('"remote account"'), $subject);
}
$this->setSubjects($event, $subject, $parsedParameters);
@@ -311,9 +260,9 @@ class Provider implements IProvider {
/**
* @param IEvent $event
* @return array
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
*/
- protected function getParameters(IEvent $event) {
+ protected function getParameters(IEvent $event): array {
$parameters = $event->getSubjectParameters();
switch ($event->getSubject()) {
case 'created_self':
@@ -366,18 +315,18 @@ class Provider implements IProvider {
* @param array|string $parameter
* @param IEvent|null $event
* @return array
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
*/
- protected function getFile($parameter, IEvent $event = null) {
+ protected function getFile($parameter, ?IEvent $event = null): array {
if (is_array($parameter)) {
$path = reset($parameter);
- $id = (string) key($parameter);
+ $id = (int)key($parameter);
} elseif ($event !== null) {
// Legacy from before ownCloud 8.2
$path = $parameter;
$id = $event->getObjectId();
} else {
- throw new \InvalidArgumentException('Could not generate file parameter');
+ throw new UnknownActivityException('Could not generate file parameter');
}
$encryptionContainer = $this->getEndToEndEncryptionContainer($id, $path);
@@ -393,7 +342,7 @@ class Provider implements IProvider {
return [
'type' => 'file',
- 'id' => $encryptionContainer->getId(),
+ 'id' => (string)$encryptionContainer->getId(),
'name' => $encryptionContainer->getName(),
'path' => $path,
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $encryptionContainer->getId()]),
@@ -406,7 +355,7 @@ class Provider implements IProvider {
return [
'type' => 'file',
- 'id' => $id,
+ 'id' => (string)$id,
'name' => basename($path),
'path' => trim($path, '/'),
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $id]),
@@ -433,8 +382,8 @@ class Provider implements IProvider {
}
$userFolder = $this->rootFolder->getUserFolder($this->activityManager->getCurrentUserId());
- $files = $userFolder->getById($fileId);
- if (empty($files)) {
+ $file = $userFolder->getFirstNodeById($fileId);
+ if (!$file) {
try {
// Deleted, try with parent
$file = $this->findExistingParent($userFolder, dirname($path));
@@ -450,8 +399,6 @@ class Provider implements IProvider {
return $file;
}
- $file = array_shift($files);
-
if ($file instanceof Folder && $file->isEncrypted()) {
// If the folder is encrypted, it is the Container,
// but can be the name is just fine.
diff --git a/apps/files/lib/Activity/Settings/FavoriteAction.php b/apps/files/lib/Activity/Settings/FavoriteAction.php
index 3c6ceb23959..73b200341ec 100644
--- a/apps/files/lib/Activity/Settings/FavoriteAction.php
+++ b/apps/files/lib/Activity/Settings/FavoriteAction.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity\Settings;
@@ -42,8 +25,8 @@ class FavoriteAction extends FileActivitySettings {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/files/lib/Activity/Settings/FileActivitySettings.php b/apps/files/lib/Activity/Settings/FileActivitySettings.php
index 8bb97a81cc8..0ca7100832f 100644
--- a/apps/files/lib/Activity/Settings/FileActivitySettings.php
+++ b/apps/files/lib/Activity/Settings/FileActivitySettings.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity\Settings;
@@ -29,14 +12,12 @@ use OCP\Activity\ActivitySettings;
use OCP\IL10N;
abstract class FileActivitySettings extends ActivitySettings {
- /** @var IL10N */
- protected $l;
-
/**
* @param IL10N $l
*/
- public function __construct(IL10N $l) {
- $this->l = $l;
+ public function __construct(
+ protected IL10N $l,
+ ) {
}
public function getGroupIdentifier() {
diff --git a/apps/files/lib/Activity/Settings/FileChanged.php b/apps/files/lib/Activity/Settings/FileChanged.php
index 2d826c30b5b..c33ed5e1eba 100644
--- a/apps/files/lib/Activity/Settings/FileChanged.php
+++ b/apps/files/lib/Activity/Settings/FileChanged.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity\Settings;
@@ -42,8 +25,8 @@ class FileChanged extends FileActivitySettings {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/files/lib/Activity/Settings/FileFavoriteChanged.php b/apps/files/lib/Activity/Settings/FileFavoriteChanged.php
index 829f3e0d9d0..5000902ed3f 100644
--- a/apps/files/lib/Activity/Settings/FileFavoriteChanged.php
+++ b/apps/files/lib/Activity/Settings/FileFavoriteChanged.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Activity\Settings;
@@ -42,8 +25,8 @@ class FileFavoriteChanged extends FileActivitySettings {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/files/lib/AdvancedCapabilities.php b/apps/files/lib/AdvancedCapabilities.php
new file mode 100644
index 00000000000..22f990f0cf8
--- /dev/null
+++ b/apps/files/lib/AdvancedCapabilities.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files;
+
+use OCA\Files\Service\SettingsService;
+use OCP\Capabilities\ICapability;
+use OCP\Capabilities\IInitialStateExcludedCapability;
+
+/**
+ * Capabilities not needed for every request.
+ * This capabilities might be hard to compute or no used by the webui.
+ */
+class AdvancedCapabilities implements ICapability, IInitialStateExcludedCapability {
+
+ public function __construct(
+ protected SettingsService $service,
+ ) {
+ }
+
+ /**
+ * Return this classes capabilities
+ *
+ * @return array{files: array{'windows_compatible_filenames': bool}}
+ */
+ public function getCapabilities(): array {
+ return [
+ 'files' => [
+ 'windows_compatible_filenames' => $this->service->hasFilesWindowsSupport(),
+ ],
+ ];
+ }
+}
diff --git a/apps/files/lib/App.php b/apps/files/lib/App.php
index e172f0ae826..9e6d35a7538 100644
--- a/apps/files/lib/App.php
+++ b/apps/files/lib/App.php
@@ -1,32 +1,16 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Vincent Petry <vincent@nextcloud.com>
- * @author Carl Schwan <carl@carlschwan.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files;
use OC\NavigationManager;
+use OCA\Files\Service\ChunkedUploadConfig;
use OCP\App\IAppManager;
+use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\INavigationManager;
@@ -34,6 +18,7 @@ use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Server;
+use Psr\Log\LoggerInterface;
class App {
private static ?INavigationManager $navigationManager = null;
@@ -50,7 +35,9 @@ class App {
Server::get(IFactory::class),
Server::get(IUserSession::class),
Server::get(IGroupManager::class),
- Server::get(IConfig::class)
+ Server::get(IConfig::class),
+ Server::get(LoggerInterface::class),
+ Server::get(IEventDispatcher::class),
);
self::$navigationManager->clear(false);
}
@@ -60,9 +47,8 @@ class App {
public static function extendJsConfig($settings): void {
$appConfig = json_decode($settings['array']['oc_appconfig'], true);
- $maxChunkSize = (int)Server::get(IConfig::class)->getAppValue('files', 'max_chunk_size', (string)(10 * 1024 * 1024));
$appConfig['files'] = [
- 'max_chunk_size' => $maxChunkSize
+ 'max_chunk_size' => ChunkedUploadConfig::getMaxChunkSize(),
];
$settings['array']['oc_appconfig'] = json_encode($appConfig);
diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php
index 01fe46bb877..2761b44ecf9 100644
--- a/apps/files/lib/AppInfo/Application.php
+++ b/apps/files/lib/AppInfo/Application.php
@@ -1,63 +1,53 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\AppInfo;
use Closure;
-use OC\Search\Provider\File;
+use OCA\Files\AdvancedCapabilities;
use OCA\Files\Capabilities;
use OCA\Files\Collaboration\Resources\Listener;
use OCA\Files\Collaboration\Resources\ResourceProvider;
use OCA\Files\Controller\ApiController;
+use OCA\Files\Dashboard\FavoriteWidget;
use OCA\Files\DirectEditingCapabilities;
-use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\Files\Event\LoadSearchPlugins;
use OCA\Files\Event\LoadSidebar;
-use OCA\Files\Listener\LegacyLoadAdditionalScriptsAdapter;
+use OCA\Files\Listener\LoadSearchPluginsListener;
use OCA\Files\Listener\LoadSidebarListener;
+use OCA\Files\Listener\NodeAddedToFavoriteListener;
+use OCA\Files\Listener\NodeRemovedFromFavoriteListener;
+use OCA\Files\Listener\RenderReferenceEventListener;
+use OCA\Files\Listener\SyncLivePhotosListener;
use OCA\Files\Notification\Notifier;
use OCA\Files\Search\FilesSearchProvider;
use OCA\Files\Service\TagService;
use OCA\Files\Service\UserConfig;
+use OCA\Files\Service\ViewConfig;
+use OCA\Files\Settings\DeclarativeAdminSettings;
use OCP\Activity\IManager as IActivityManager;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\Collaboration\Resources\IProviderManager;
+use OCP\Files\Cache\CacheEntryRemovedEvent;
+use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
+use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
+use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
+use OCP\Files\Events\Node\NodeCopiedEvent;
+use OCP\Files\Events\NodeAddedToFavorite;
+use OCP\Files\Events\NodeRemovedFromFavorite;
+use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
-use OCP\ISearch;
use OCP\IRequest;
use OCP\IServerContainer;
use OCP\ITagManager;
@@ -65,6 +55,7 @@ use OCP\IUserSession;
use OCP\Share\IManager as IShareManager;
use OCP\Util;
use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
class Application extends App implements IBootstrap {
public const APP_ID = 'files';
@@ -91,6 +82,10 @@ class Application extends App implements IBootstrap {
$c->get(IConfig::class),
$server->getUserFolder(),
$c->get(UserConfig::class),
+ $c->get(ViewConfig::class),
+ $c->get(IL10N::class),
+ $c->get(IRootFolder::class),
+ $c->get(LoggerInterface::class),
);
});
@@ -106,7 +101,6 @@ class Application extends App implements IBootstrap {
$c->get(IActivityManager::class),
$c->get(ITagManager::class)->load(self::APP_ID),
$server->getUserFolder(),
- $server->getEventDispatcher()
);
});
@@ -114,22 +108,30 @@ class Application extends App implements IBootstrap {
* Register capabilities
*/
$context->registerCapability(Capabilities::class);
+ $context->registerCapability(AdvancedCapabilities::class);
$context->registerCapability(DirectEditingCapabilities::class);
- $context->registerEventListener(LoadAdditionalScriptsEvent::class, LegacyLoadAdditionalScriptsAdapter::class);
- $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
+ $context->registerDeclarativeSettings(DeclarativeAdminSettings::class);
+ $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
+ $context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
+ $context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class);
+ $context->registerEventListener(BeforeNodeDeletedEvent::class, SyncLivePhotosListener::class);
+ $context->registerEventListener(CacheEntryRemovedEvent::class, SyncLivePhotosListener::class, 1); // Ensure this happen before the metadata are deleted.
+ $context->registerEventListener(BeforeNodeCopiedEvent::class, SyncLivePhotosListener::class);
+ $context->registerEventListener(NodeCopiedEvent::class, SyncLivePhotosListener::class);
+ $context->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class);
+ $context->registerEventListener(NodeAddedToFavorite::class, NodeAddedToFavoriteListener::class);
+ $context->registerEventListener(NodeRemovedFromFavorite::class, NodeRemovedFromFavoriteListener::class);
$context->registerSearchProvider(FilesSearchProvider::class);
$context->registerNotifierService(Notifier::class);
+ $context->registerDashboardWidget(FavoriteWidget::class);
}
public function boot(IBootContext $context): void {
$context->injectFn(Closure::fromCallable([$this, 'registerCollaboration']));
$context->injectFn([Listener::class, 'register']);
- $context->injectFn(Closure::fromCallable([$this, 'registerSearchProvider']));
- $this->registerTemplates();
- $context->injectFn(Closure::fromCallable([$this, 'registerNavigation']));
$this->registerHooks();
}
@@ -137,47 +139,6 @@ class Application extends App implements IBootstrap {
$providerManager->registerResourceProvider(ResourceProvider::class);
}
- private function registerSearchProvider(ISearch $search): void {
- $search->registerProvider(File::class, ['apps' => ['files']]);
- }
-
- private function registerTemplates(): void {
- $templateManager = \OC_Helper::getFileTemplateManager();
- $templateManager->registerTemplate('application/vnd.oasis.opendocument.presentation', 'core/templates/filetemplates/template.odp');
- $templateManager->registerTemplate('application/vnd.oasis.opendocument.text', 'core/templates/filetemplates/template.odt');
- $templateManager->registerTemplate('application/vnd.oasis.opendocument.spreadsheet', 'core/templates/filetemplates/template.ods');
- }
-
- private function registerNavigation(IL10N $l10n): void {
- \OCA\Files\App::getNavigationManager()->add(function () use ($l10n) {
- return [
- 'id' => 'files',
- 'appname' => 'files',
- 'script' => 'list.php',
- 'order' => 0,
- 'name' => $l10n->t('All files')
- ];
- });
- \OCA\Files\App::getNavigationManager()->add(function () use ($l10n) {
- return [
- 'id' => 'recent',
- 'appname' => 'files',
- 'script' => 'recentlist.php',
- 'order' => 2,
- 'name' => $l10n->t('Recent')
- ];
- });
- \OCA\Files\App::getNavigationManager()->add(function () use ($l10n) {
- return [
- 'id' => 'favorites',
- 'appname' => 'files',
- 'script' => 'simplelist.php',
- 'order' => 5,
- 'name' => $l10n->t('Favorites'),
- ];
- });
- }
-
private function registerHooks(): void {
Util::connectHook('\OCP\Config', 'js', '\OCA\Files\App', 'extendJsConfig');
}
diff --git a/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php b/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php
index a9b5b1446b2..a1032b2787d 100644
--- a/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php
+++ b/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\BackgroundJob;
@@ -31,15 +13,12 @@ use OCP\BackgroundJob\TimedJob;
use OCP\DirectEditing\IManager;
class CleanupDirectEditingTokens extends TimedJob {
- private const INTERVAL_MINUTES = 15 * 60;
-
- private IManager $manager;
-
- public function __construct(ITimeFactory $time,
- IManager $manager) {
+ public function __construct(
+ ITimeFactory $time,
+ private IManager $manager,
+ ) {
parent::__construct($time);
- $this->interval = self::INTERVAL_MINUTES;
- $this->manager = $manager;
+ $this->setInterval(15 * 60);
}
/**
diff --git a/apps/files/lib/BackgroundJob/CleanupFileLocks.php b/apps/files/lib/BackgroundJob/CleanupFileLocks.php
index e0ad72eaaf0..91bb145884b 100644
--- a/apps/files/lib/BackgroundJob/CleanupFileLocks.php
+++ b/apps/files/lib/BackgroundJob/CleanupFileLocks.php
@@ -1,50 +1,28 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Max Kovalenko <mxss1998@yandex.ru>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\BackgroundJob;
+use OC\Lock\DBLockingProvider;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
-use OC\Lock\DBLockingProvider;
+use OCP\Lock\ILockingProvider;
+use OCP\Server;
/**
* Clean up all file locks that are expired for the DB file locking provider
*/
class CleanupFileLocks extends TimedJob {
/**
- * Default interval in minutes
- *
- * @var int $defaultIntervalMin
- **/
- protected $defaultIntervalMin = 5;
-
- /**
* sets the correct interval for this timed job
*/
public function __construct(ITimeFactory $time) {
parent::__construct($time);
-
- $this->interval = $this->defaultIntervalMin * 60;
+ $this->setInterval(5 * 60);
}
/**
@@ -54,7 +32,7 @@ class CleanupFileLocks extends TimedJob {
* @throws \Exception
*/
public function run($argument) {
- $lockingProvider = \OC::$server->getLockingProvider();
+ $lockingProvider = Server::get(ILockingProvider::class);
if ($lockingProvider instanceof DBLockingProvider) {
$lockingProvider->cleanExpiredLocks();
}
diff --git a/apps/files/lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php b/apps/files/lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php
index d96728fc713..8a20b6dfb0c 100644
--- a/apps/files/lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php
+++ b/apps/files/lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php
@@ -3,51 +3,29 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\BackgroundJob;
-use OCA\Files\Controller\OpenLocalEditorController;
use OCA\Files\Db\OpenLocalEditorMapper;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
/**
* Delete all expired "Open local editor" token
*/
class DeleteExpiredOpenLocalEditor extends TimedJob {
- protected OpenLocalEditorMapper $mapper;
-
public function __construct(
ITimeFactory $time,
- OpenLocalEditorMapper $mapper
+ protected OpenLocalEditorMapper $mapper,
) {
parent::__construct($time);
- $this->mapper = $mapper;
// Run every 12h
$this->interval = 12 * 3600;
- $this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
/**
diff --git a/apps/files/lib/BackgroundJob/DeleteOrphanedItems.php b/apps/files/lib/BackgroundJob/DeleteOrphanedItems.php
index 669c2a4cde6..b925974f24a 100644
--- a/apps/files/lib/BackgroundJob/DeleteOrphanedItems.php
+++ b/apps/files/lib/BackgroundJob/DeleteOrphanedItems.php
@@ -1,32 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\BackgroundJob;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use Psr\Log\LoggerInterface;
/**
* Delete all share entries that have no matching entries in the file cache table.
@@ -34,27 +19,16 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
class DeleteOrphanedItems extends TimedJob {
public const CHUNK_SIZE = 200;
- /** @var \OCP\IDBConnection */
- protected $connection;
-
- /** @var \OCP\ILogger */
- protected $logger;
-
- /**
- * Default interval in minutes
- *
- * @var int $defaultIntervalMin
- **/
- protected $defaultIntervalMin = 60;
-
/**
* sets the correct interval for this timed job
*/
- public function __construct(ITimeFactory $time) {
+ public function __construct(
+ ITimeFactory $time,
+ protected IDBConnection $connection,
+ protected LoggerInterface $logger,
+ ) {
parent::__construct($time);
- $this->interval = $this->defaultIntervalMin * 60;
- $this->connection = \OC::$server->getDatabaseConnection();
- $this->logger = \OC::$server->getLogger();
+ $this->setInterval(60 * 60);
}
/**
@@ -77,38 +51,87 @@ class DeleteOrphanedItems extends TimedJob {
* @param string $typeCol
* @return int Number of deleted entries
*/
- protected function cleanUp($table, $idCol, $typeCol) {
+ protected function cleanUp(string $table, string $idCol, string $typeCol): int {
$deletedEntries = 0;
- $query = $this->connection->getQueryBuilder();
- $query->select('t1.' . $idCol)
- ->from($table, 't1')
- ->where($query->expr()->eq($typeCol, $query->expr()->literal('files')))
- ->andWhere($query->expr()->isNull('t2.fileid'))
- ->leftJoin('t1', 'filecache', 't2', $query->expr()->eq($query->expr()->castColumn('t1.' . $idCol, IQueryBuilder::PARAM_INT), 't2.fileid'))
- ->groupBy('t1.' . $idCol)
- ->setMaxResults(self::CHUNK_SIZE);
-
$deleteQuery = $this->connection->getQueryBuilder();
$deleteQuery->delete($table)
->where($deleteQuery->expr()->eq($idCol, $deleteQuery->createParameter('objectid')));
- $deletedInLastChunk = self::CHUNK_SIZE;
- while ($deletedInLastChunk === self::CHUNK_SIZE) {
- $result = $query->execute();
- $deletedInLastChunk = 0;
- while ($row = $result->fetch()) {
- $deletedInLastChunk++;
- $deletedEntries += $deleteQuery->setParameter('objectid', (int) $row[$idCol])
- ->execute();
+ if ($this->connection->getShardDefinition('filecache')) {
+ $sourceIdChunks = $this->getItemIds($table, $idCol, $typeCol, 1000);
+ foreach ($sourceIdChunks as $sourceIdChunk) {
+ $deletedSources = $this->findMissingSources($sourceIdChunk);
+ $deleteQuery->setParameter('objectid', $deletedSources, IQueryBuilder::PARAM_INT_ARRAY);
+ $deletedEntries += $deleteQuery->executeStatement();
+ }
+ } else {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('t1.' . $idCol)
+ ->from($table, 't1')
+ ->where($query->expr()->eq($typeCol, $query->expr()->literal('files')))
+ ->leftJoin('t1', 'filecache', 't2', $query->expr()->eq($query->expr()->castColumn('t1.' . $idCol, IQueryBuilder::PARAM_INT), 't2.fileid'))
+ ->andWhere($query->expr()->isNull('t2.fileid'))
+ ->groupBy('t1.' . $idCol)
+ ->setMaxResults(self::CHUNK_SIZE);
+
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete($table)
+ ->where($deleteQuery->expr()->in($idCol, $deleteQuery->createParameter('objectid')));
+
+ $deletedInLastChunk = self::CHUNK_SIZE;
+ while ($deletedInLastChunk === self::CHUNK_SIZE) {
+ $chunk = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ $deletedInLastChunk = count($chunk);
+
+ $deleteQuery->setParameter('objectid', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $deletedEntries += $deleteQuery->executeStatement();
}
- $result->closeCursor();
}
return $deletedEntries;
}
/**
+ * @param string $table
+ * @param string $idCol
+ * @param string $typeCol
+ * @param int $chunkSize
+ * @return \Iterator<int[]>
+ * @throws \OCP\DB\Exception
+ */
+ private function getItemIds(string $table, string $idCol, string $typeCol, int $chunkSize): \Iterator {
+ $query = $this->connection->getQueryBuilder();
+ $query->select($idCol)
+ ->from($table)
+ ->where($query->expr()->eq($typeCol, $query->expr()->literal('files')))
+ ->groupBy($idCol)
+ ->andWhere($query->expr()->gt($idCol, $query->createParameter('min_id')))
+ ->setMaxResults($chunkSize);
+
+ $minId = 0;
+ while (true) {
+ $query->setParameter('min_id', $minId);
+ $rows = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ if (count($rows) > 0) {
+ $minId = $rows[count($rows) - 1];
+ yield $rows;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private function findMissingSources(array $ids): array {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('fileid')
+ ->from('filecache')
+ ->where($qb->expr()->in('fileid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));
+ $found = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ return array_diff($ids, $found);
+ }
+
+ /**
* Deleting orphaned system tag mappings
*
* @return int Number of deleted entries
diff --git a/apps/files/lib/BackgroundJob/ScanFiles.php b/apps/files/lib/BackgroundJob/ScanFiles.php
index 8dc9dcf37ff..f3f9093d648 100644
--- a/apps/files/lib/BackgroundJob/ScanFiles.php
+++ b/apps/files/lib/BackgroundJob/ScanFiles.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\BackgroundJob;
@@ -40,29 +24,19 @@ use Psr\Log\LoggerInterface;
* @package OCA\Files\BackgroundJob
*/
class ScanFiles extends TimedJob {
- private IConfig $config;
- private IEventDispatcher $dispatcher;
- private LoggerInterface $logger;
- private IDBConnection $connection;
-
/** Amount of users that should get scanned per execution */
public const USERS_PER_SESSION = 500;
public function __construct(
- IConfig $config,
- IEventDispatcher $dispatcher,
- LoggerInterface $logger,
- IDBConnection $connection,
- ITimeFactory $time
+ private IConfig $config,
+ private IEventDispatcher $dispatcher,
+ private LoggerInterface $logger,
+ private IDBConnection $connection,
+ ITimeFactory $time,
) {
parent::__construct($time);
// Run once per 10 minutes
$this->setInterval(60 * 10);
-
- $this->config = $config;
- $this->dispatcher = $dispatcher;
- $this->logger = $logger;
- $this->connection = $connection;
}
protected function runScanner(string $user): void {
@@ -86,15 +60,61 @@ class ScanFiles extends TimedJob {
* @return string|false
*/
private function getUserToScan() {
+ if ($this->connection->getShardDefinition('filecache')) {
+ // for sharded filecache, the "LIMIT" from the normal query doesn't work
+
+ // first we try it with a "LEFT JOIN" on mounts, this is fast, but might return a storage that isn't mounted.
+ // we also ask for up to 10 results from different storages to increase the odds of finding a result that is mounted
+ $query = $this->connection->getQueryBuilder();
+ $query->select('m.user_id')
+ ->from('filecache', 'f')
+ ->leftJoin('f', 'mounts', 'm', $query->expr()->eq('m.storage_id', 'f.storage'))
+ ->where($query->expr()->eq('f.size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->gt('f.parent', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
+ ->setMaxResults(10)
+ ->groupBy('f.storage')
+ ->runAcrossAllShards();
+
+ $result = $query->executeQuery();
+ while ($res = $result->fetch()) {
+ if ($res['user_id']) {
+ return $res['user_id'];
+ }
+ }
+
+ // as a fallback, we try a slower approach where we find all mounted storages first
+ // this is essentially doing the inner join manually
+ $storages = $this->getAllMountedStorages();
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select('m.user_id')
+ ->from('filecache', 'f')
+ ->leftJoin('f', 'mounts', 'm', $query->expr()->eq('m.storage_id', 'f.storage'))
+ ->where($query->expr()->eq('f.size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->gt('f.parent', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->in('f.storage', $query->createNamedParameter($storages, IQueryBuilder::PARAM_INT_ARRAY)))
+ ->setMaxResults(1)
+ ->runAcrossAllShards();
+ return $query->executeQuery()->fetchOne();
+ } else {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('m.user_id')
+ ->from('filecache', 'f')
+ ->innerJoin('f', 'mounts', 'm', $query->expr()->eq('m.storage_id', 'f.storage'))
+ ->where($query->expr()->eq('f.size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->gt('f.parent', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
+ ->setMaxResults(1)
+ ->runAcrossAllShards();
+
+ return $query->executeQuery()->fetchOne();
+ }
+ }
+
+ private function getAllMountedStorages(): array {
$query = $this->connection->getQueryBuilder();
- $query->select('user_id')
- ->from('filecache', 'f')
- ->innerJoin('f', 'mounts', 'm', $query->expr()->eq('storage_id', 'storage'))
- ->where($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
- ->andWhere($query->expr()->gt('parent', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
- ->setMaxResults(1);
-
- return $query->executeQuery()->fetchOne();
+ $query->selectDistinct('storage_id')
+ ->from('mounts');
+ return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
}
/**
diff --git a/apps/files/lib/BackgroundJob/TransferOwnership.php b/apps/files/lib/BackgroundJob/TransferOwnership.php
index 8c96fcf8385..de8d1989733 100644
--- a/apps/files/lib/BackgroundJob/TransferOwnership.php
+++ b/apps/files/lib/BackgroundJob/TransferOwnership.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\BackgroundJob;
@@ -33,46 +16,23 @@ use OCA\Files\Service\OwnershipTransferService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\Files\IRootFolder;
-use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as NotificationManager;
+use Psr\Log\LoggerInterface;
use function ltrim;
class TransferOwnership extends QueuedJob {
-
- /** @var IUserManager $userManager */
- private $userManager;
-
- /** @var OwnershipTransferService */
- private $transferService;
-
- /** @var ILogger */
- private $logger;
-
- /** @var NotificationManager */
- private $notificationManager;
-
- /** @var TransferOwnershipMapper */
- private $mapper;
- /** @var IRootFolder */
- private $rootFolder;
-
- public function __construct(ITimeFactory $timeFactory,
- IUserManager $userManager,
- OwnershipTransferService $transferService,
- ILogger $logger,
- NotificationManager $notificationManager,
- TransferOwnershipMapper $mapper,
- IRootFolder $rootFolder) {
+ public function __construct(
+ ITimeFactory $timeFactory,
+ private IUserManager $userManager,
+ private OwnershipTransferService $transferService,
+ private LoggerInterface $logger,
+ private NotificationManager $notificationManager,
+ private TransferOwnershipMapper $mapper,
+ private IRootFolder $rootFolder,
+ ) {
parent::__construct($timeFactory);
-
- $this->userManager = $userManager;
- $this->transferService = $transferService;
- $this->logger = $logger;
- $this->notificationManager = $notificationManager;
- $this->mapper = $mapper;
- $this->rootFolder = $rootFolder;
}
protected function run($argument) {
@@ -84,14 +44,14 @@ class TransferOwnership extends QueuedJob {
$fileId = $transfer->getFileId();
$userFolder = $this->rootFolder->getUserFolder($sourceUser);
- $nodes = $userFolder->getById($fileId);
+ $node = $userFolder->getFirstNodeById($fileId);
- if (empty($nodes)) {
+ if (!$node) {
$this->logger->alert('Could not transfer ownership: Node not found');
$this->failedNotication($transfer);
return;
}
- $path = $userFolder->getRelativePath($nodes[0]->getPath());
+ $path = $userFolder->getRelativePath($node->getPath());
$sourceUserObject = $this->userManager->get($sourceUser);
$destinationUserObject = $this->userManager->get($destinationUser);
@@ -116,7 +76,12 @@ class TransferOwnership extends QueuedJob {
);
$this->successNotification($transfer);
} catch (TransferOwnershipException $e) {
- $this->logger->logException($e);
+ $this->logger->error(
+ $e->getMessage(),
+ [
+ 'exception' => $e,
+ ],
+ );
$this->failedNotication($transfer);
}
@@ -136,7 +101,6 @@ class TransferOwnership extends QueuedJob {
])
->setObject('transfer', (string)$transfer->getId());
$this->notificationManager->notify($notification);
-
// Send notification to source user
$notification = $this->notificationManager->createNotification();
$notification->setUser($transfer->getTargetUser())
diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php
index 3eb43578351..6b50e5807a5 100644
--- a/apps/files/lib/Capabilities.php
+++ b/apps/files/lib/Capabilities.php
@@ -1,51 +1,50 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tom Needham <tom@owncloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files;
+use OC\Files\FilenameValidator;
+use OCA\Files\Service\ChunkedUploadConfig;
use OCP\Capabilities\ICapability;
-use OCP\IConfig;
+use OCP\Files\Conversion\ConversionMimeProvider;
+use OCP\Files\Conversion\IConversionManager;
class Capabilities implements ICapability {
- protected IConfig $config;
-
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ protected FilenameValidator $filenameValidator,
+ protected IConversionManager $fileConversionManager,
+ ) {
}
/**
* Return this classes capabilities
*
- * @return array
+ * @return array{files: array{'$comment': ?string, bigfilechunking: bool, blacklisted_files: list<mixed>, forbidden_filenames: list<string>, forbidden_filename_basenames: list<string>, forbidden_filename_characters: list<string>, forbidden_filename_extensions: list<string>, chunked_upload: array{max_size: int, max_parallel_count: int}, file_conversions: list<array{from: string, to: string, extension: string, displayName: string}>}}
*/
- public function getCapabilities() {
+ public function getCapabilities(): array {
return [
'files' => [
+ '$comment' => '"blacklisted_files" is deprecated as of Nextcloud 30, use "forbidden_filenames" instead',
+ 'blacklisted_files' => $this->filenameValidator->getForbiddenFilenames(),
+ 'forbidden_filenames' => $this->filenameValidator->getForbiddenFilenames(),
+ 'forbidden_filename_basenames' => $this->filenameValidator->getForbiddenBasenames(),
+ 'forbidden_filename_characters' => $this->filenameValidator->getForbiddenCharacters(),
+ 'forbidden_filename_extensions' => $this->filenameValidator->getForbiddenExtensions(),
+
'bigfilechunking' => true,
- 'blacklisted_files' => $this->config->getSystemValue('blacklisted_files', ['.htaccess'])
+ 'chunked_upload' => [
+ 'max_size' => ChunkedUploadConfig::getMaxChunkSize(),
+ 'max_parallel_count' => ChunkedUploadConfig::getMaxParallelCount(),
+ ],
+
+ 'file_conversions' => array_map(function (ConversionMimeProvider $mimeProvider) {
+ return $mimeProvider->jsonSerialize();
+ }, $this->fileConversionManager->getProviders()),
],
];
}
diff --git a/apps/files/lib/Collaboration/Resources/Listener.php b/apps/files/lib/Collaboration/Resources/Listener.php
index 64dd693a4da..e4ff5d83b7a 100644
--- a/apps/files/lib/Collaboration/Resources/Listener.php
+++ b/apps/files/lib/Collaboration/Resources/Listener.php
@@ -3,37 +3,23 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Collaboration\Resources;
-use OCP\Server;
use OCP\Collaboration\Resources\IManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Server;
+use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\Events\ShareDeletedEvent;
+use OCP\Share\Events\ShareDeletedFromSelfEvent;
class Listener {
- public static function register(EventDispatcherInterface $dispatcher): void {
- $dispatcher->addListener('OCP\Share::postShare', [self::class, 'shareModification']);
- $dispatcher->addListener('OCP\Share::postUnshare', [self::class, 'shareModification']);
- $dispatcher->addListener('OCP\Share::postUnshareFromSelf', [self::class, 'shareModification']);
+ public static function register(IEventDispatcher $dispatcher): void {
+ $dispatcher->addListener(ShareCreatedEvent::class, [self::class, 'shareModification']);
+ $dispatcher->addListener(ShareDeletedEvent::class, [self::class, 'shareModification']);
+ $dispatcher->addListener(ShareDeletedFromSelfEvent::class, [self::class, 'shareModification']);
}
public static function shareModification(): void {
diff --git a/apps/files/lib/Collaboration/Resources/ResourceProvider.php b/apps/files/lib/Collaboration/Resources/ResourceProvider.php
index 841a2bdd4f7..73883bc4c6a 100644
--- a/apps/files/lib/Collaboration/Resources/ResourceProvider.php
+++ b/apps/files/lib/Collaboration/Resources/ResourceProvider.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Collaboration\Resources;
@@ -38,32 +20,24 @@ use OCP\IUser;
class ResourceProvider implements IProvider {
public const RESOURCE_TYPE = 'file';
- /** @var IRootFolder */
- protected $rootFolder;
- /** @var IPreview */
- private $preview;
- /** @var IURLGenerator */
- private $urlGenerator;
-
/** @var array */
protected $nodes = [];
- public function __construct(IRootFolder $rootFolder,
- IPreview $preview,
- IURLGenerator $urlGenerator) {
- $this->rootFolder = $rootFolder;
- $this->preview = $preview;
- $this->urlGenerator = $urlGenerator;
+ public function __construct(
+ protected IRootFolder $rootFolder,
+ private IPreview $preview,
+ private IURLGenerator $urlGenerator,
+ ) {
}
private function getNode(IResource $resource): ?Node {
- if (isset($this->nodes[(int) $resource->getId()])) {
- return $this->nodes[(int) $resource->getId()];
+ if (isset($this->nodes[(int)$resource->getId()])) {
+ return $this->nodes[(int)$resource->getId()];
}
- $nodes = $this->rootFolder->getById((int) $resource->getId());
- if (!empty($nodes)) {
- $this->nodes[(int) $resource->getId()] = array_shift($nodes);
- return $this->nodes[(int) $resource->getId()];
+ $node = $this->rootFolder->getFirstNodeById((int)$resource->getId());
+ if ($node) {
+ $this->nodes[(int)$resource->getId()] = $node;
+ return $this->nodes[(int)$resource->getId()];
}
return null;
}
@@ -74,8 +48,8 @@ class ResourceProvider implements IProvider {
* @since 16.0.0
*/
public function getResourceRichObject(IResource $resource): array {
- if (isset($this->nodes[(int) $resource->getId()])) {
- $node = $this->nodes[(int) $resource->getId()]->getPath();
+ if (isset($this->nodes[(int)$resource->getId()])) {
+ $node = $this->nodes[(int)$resource->getId()]->getPath();
} else {
$node = $this->getNode($resource);
}
@@ -107,16 +81,16 @@ class ResourceProvider implements IProvider {
* @return bool
* @since 16.0.0
*/
- public function canAccessResource(IResource $resource, IUser $user = null): bool {
+ public function canAccessResource(IResource $resource, ?IUser $user = null): bool {
if (!$user instanceof IUser) {
return false;
}
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
- $nodes = $userFolder->getById((int) $resource->getId());
+ $node = $userFolder->getById((int)$resource->getId());
- if (!empty($nodes)) {
- $this->nodes[(int) $resource->getId()] = array_shift($nodes);
+ if ($node) {
+ $this->nodes[(int)$resource->getId()] = $node;
return true;
}
diff --git a/apps/files/lib/Command/Copy.php b/apps/files/lib/Command/Copy.php
new file mode 100644
index 00000000000..ad0dfa90de1
--- /dev/null
+++ b/apps/files/lib/Command/Copy.php
@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command;
+
+use OC\Core\Command\Info\FileUtils;
+use OCP\Files\Folder;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class Copy extends Command {
+ public function __construct(
+ private FileUtils $fileUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:copy')
+ ->setDescription('Copy a file or folder')
+ ->addArgument('source', InputArgument::REQUIRED, 'Source file id or path')
+ ->addArgument('target', InputArgument::REQUIRED, 'Target path')
+ ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for confirmation and don't output any warnings")
+ ->addOption('no-target-directory', 'T', InputOption::VALUE_NONE, 'When target path is folder, overwrite the folder instead of copying into the folder');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $sourceInput = $input->getArgument('source');
+ $targetInput = $input->getArgument('target');
+ $force = $input->getOption('force');
+ $noTargetDir = $input->getOption('no-target-directory');
+
+ $node = $this->fileUtils->getNode($sourceInput);
+ $targetNode = $this->fileUtils->getNode($targetInput);
+
+ if (!$node) {
+ $output->writeln("<error>file $sourceInput not found</error>");
+ return 1;
+ }
+
+ $targetParentPath = dirname(rtrim($targetInput, '/'));
+ $targetParent = $this->fileUtils->getNode($targetParentPath);
+ if (!$targetParent) {
+ $output->writeln("<error>Target parent path $targetParentPath doesn't exist</error>");
+ return 1;
+ }
+
+ $wouldRequireDelete = false;
+
+ if ($targetNode) {
+ if (!$targetNode->isUpdateable()) {
+ $output->writeln("<error>$targetInput isn't writable</error>");
+ return 1;
+ }
+
+ if ($targetNode instanceof Folder) {
+ if ($noTargetDir) {
+ if (!$force) {
+ $output->writeln("Warning: <info>$sourceInput</info> is a file, but <info>$targetInput</info> is a folder");
+ }
+ $wouldRequireDelete = true;
+ } else {
+ $targetInput = $targetNode->getFullPath($node->getName());
+ $targetNode = $this->fileUtils->getNode($targetInput);
+ }
+ } else {
+ if ($node instanceof Folder) {
+ if (!$force) {
+ $output->writeln("Warning: <info>$sourceInput</info> is a folder, but <info>$targetInput</info> is a file");
+ }
+ $wouldRequireDelete = true;
+ }
+ }
+
+ if ($wouldRequireDelete && $targetNode->getInternalPath() === '') {
+ $output->writeln("<error>Mount root can't be overwritten with a different type</error>");
+ return 1;
+ }
+
+ if ($wouldRequireDelete && !$targetNode->isDeletable()) {
+ $output->writeln("<error>$targetInput can't be deleted to be replaced with $sourceInput</error>");
+ return 1;
+ }
+
+ if (!$force && $targetNode) {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+
+ $question = new ConfirmationQuestion('<info>' . $targetInput . '</info> already exists, overwrite? [y/N] ', false);
+ if (!$helper->ask($input, $output, $question)) {
+ return 1;
+ }
+ }
+ }
+
+ if ($wouldRequireDelete && $targetNode) {
+ $targetNode->delete();
+ }
+
+ $node->copy($targetInput);
+
+ return 0;
+ }
+
+}
diff --git a/apps/files/lib/Command/Delete.php b/apps/files/lib/Command/Delete.php
new file mode 100644
index 00000000000..d984f839c91
--- /dev/null
+++ b/apps/files/lib/Command/Delete.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command;
+
+use OC\Core\Command\Info\FileUtils;
+use OCA\Files_Sharing\SharedStorage;
+use OCP\Files\Folder;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class Delete extends Command {
+ public function __construct(
+ private FileUtils $fileUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:delete')
+ ->setDescription('Delete a file or folder')
+ ->addArgument('file', InputArgument::REQUIRED, 'File id or path')
+ ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $fileInput = $input->getArgument('file');
+ $inputIsId = is_numeric($fileInput);
+ $force = $input->getOption('force');
+ $node = $this->fileUtils->getNode($fileInput);
+
+ if (!$node) {
+ $output->writeln("<error>file $fileInput not found</error>");
+ return self::FAILURE;
+ }
+
+ $deleteConfirmed = $force;
+ if (!$deleteConfirmed) {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $storage = $node->getStorage();
+ if (!$inputIsId && $storage->instanceOfStorage(SharedStorage::class) && $node->getInternalPath() === '') {
+ /** @var SharedStorage $storage */
+ [,$user] = explode('/', $fileInput, 3);
+ $question = new ConfirmationQuestion("<info>$fileInput</info> in a shared file, do you want to unshare the file from <info>$user</info> instead of deleting the source file? [Y/n] ", true);
+ if ($helper->ask($input, $output, $question)) {
+ $storage->unshareStorage();
+ return self::SUCCESS;
+ } else {
+ $node = $storage->getShare()->getNode();
+ $output->writeln('');
+ }
+ }
+
+ $filesByUsers = $this->fileUtils->getFilesByUser($node);
+ if (count($filesByUsers) > 1) {
+ $output->writeln('Warning: the provided file is accessible by more than one user');
+ $output->writeln(' all of the following users will lose access to the file when deleted:');
+ $output->writeln('');
+ foreach ($filesByUsers as $user => $filesByUser) {
+ $output->writeln($user . ':');
+ foreach ($filesByUser as $file) {
+ $output->writeln(' - ' . $file->getPath());
+ }
+ }
+ $output->writeln('');
+ }
+
+ if ($node instanceof Folder) {
+ $maybeContents = " and all it's contents";
+ } else {
+ $maybeContents = '';
+ }
+ $question = new ConfirmationQuestion('Delete ' . $node->getPath() . $maybeContents . '? [y/N] ', false);
+ $deleteConfirmed = $helper->ask($input, $output, $question);
+ }
+
+ if ($deleteConfirmed) {
+ if ($node->isDeletable()) {
+ $node->delete();
+ } else {
+ $output->writeln('<error>File cannot be deleted, insufficient permissions.</error>');
+ }
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files/lib/Command/DeleteOrphanedFiles.php b/apps/files/lib/Command/DeleteOrphanedFiles.php
index e3305fe3b9b..37cb3159f4a 100644
--- a/apps/files/lib/Command/DeleteOrphanedFiles.php
+++ b/apps/files/lib/Command/DeleteOrphanedFiles.php
@@ -1,32 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\Command;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -35,56 +20,123 @@ use Symfony\Component\Console\Output\OutputInterface;
class DeleteOrphanedFiles extends Command {
public const CHUNK_SIZE = 200;
- /**
- * @var IDBConnection
- */
- protected $connection;
-
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ protected IDBConnection $connection,
+ ) {
parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('files:cleanup')
- ->setDescription('cleanup filecache');
+ ->setDescription('Clean up orphaned filecache and mount entries')
+ ->setHelp('Deletes orphaned filecache and mount entries (those without an existing storage).')
+ ->addOption('skip-filecache-extended', null, InputOption::VALUE_NONE, 'don\'t remove orphaned entries from filecache_extended');
}
public function execute(InputInterface $input, OutputInterface $output): int {
- $deletedEntries = 0;
+ $fileIdsByStorage = [];
+
+ $deletedStorages = array_diff($this->getReferencedStorages(), $this->getExistingStorages());
+
+ $deleteExtended = !$input->getOption('skip-filecache-extended');
+ if ($deleteExtended) {
+ $fileIdsByStorage = $this->getFileIdsForStorages($deletedStorages);
+ }
+
+ $deletedEntries = $this->cleanupOrphanedFileCache($deletedStorages);
+ $output->writeln("$deletedEntries orphaned file cache entries deleted");
+ if ($deleteExtended) {
+ $deletedFileCacheExtended = $this->cleanupOrphanedFileCacheExtended($fileIdsByStorage);
+ $output->writeln("$deletedFileCacheExtended orphaned file cache extended entries deleted");
+ }
+
+ $deletedMounts = $this->cleanupOrphanedMounts();
+ $output->writeln("$deletedMounts orphaned mount entries deleted");
+
+ return self::SUCCESS;
+ }
+
+ private function getReferencedStorages(): array {
$query = $this->connection->getQueryBuilder();
- $query->select('fc.fileid')
- ->from('filecache', 'fc')
- ->where($query->expr()->isNull('s.numeric_id'))
- ->leftJoin('fc', 'storages', 's', $query->expr()->eq('fc.storage', 's.numeric_id'))
- ->setMaxResults(self::CHUNK_SIZE);
+ $query->select('storage')
+ ->from('filecache')
+ ->groupBy('storage')
+ ->runAcrossAllShards();
+ return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ }
+
+ private function getExistingStorages(): array {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('numeric_id')
+ ->from('storages')
+ ->groupBy('numeric_id');
+ return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ }
+
+ /**
+ * @param int[] $storageIds
+ * @return array<int, int[]>
+ */
+ private function getFileIdsForStorages(array $storageIds): array {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('storage', 'fileid')
+ ->from('filecache')
+ ->where($query->expr()->in('storage', $query->createParameter('storage_ids')));
+
+ $result = [];
+ $storageIdChunks = array_chunk($storageIds, self::CHUNK_SIZE);
+ foreach ($storageIdChunks as $storageIdChunk) {
+ $query->setParameter('storage_ids', $storageIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $chunk = $query->executeQuery()->fetchAll();
+ foreach ($chunk as $row) {
+ $result[$row['storage']][] = $row['fileid'];
+ }
+ }
+ return $result;
+ }
+
+ private function cleanupOrphanedFileCache(array $deletedStorages): int {
+ $deletedEntries = 0;
$deleteQuery = $this->connection->getQueryBuilder();
$deleteQuery->delete('filecache')
- ->where($deleteQuery->expr()->eq('fileid', $deleteQuery->createParameter('objectid')));
+ ->where($deleteQuery->expr()->in('storage', $deleteQuery->createParameter('storage_ids')));
- $deletedInLastChunk = self::CHUNK_SIZE;
- while ($deletedInLastChunk === self::CHUNK_SIZE) {
- $deletedInLastChunk = 0;
- $result = $query->execute();
- while ($row = $result->fetch()) {
- $deletedInLastChunk++;
- $deletedEntries += $deleteQuery->setParameter('objectid', (int) $row['fileid'])
- ->execute();
- }
- $result->closeCursor();
+ $deletedStorageChunks = array_chunk($deletedStorages, self::CHUNK_SIZE);
+ foreach ($deletedStorageChunks as $deletedStorageChunk) {
+ $deleteQuery->setParameter('storage_ids', $deletedStorageChunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $deletedEntries += $deleteQuery->executeStatement();
}
- $output->writeln("$deletedEntries orphaned file cache entries deleted");
+ return $deletedEntries;
+ }
- $deletedMounts = $this->cleanupOrphanedMounts();
- $output->writeln("$deletedMounts orphaned mount entries deleted");
- return 0;
+ /**
+ * @param array<int, int[]> $fileIdsByStorage
+ * @return int
+ */
+ private function cleanupOrphanedFileCacheExtended(array $fileIdsByStorage): int {
+ $deletedEntries = 0;
+
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete('filecache_extended')
+ ->where($deleteQuery->expr()->in('fileid', $deleteQuery->createParameter('file_ids')));
+
+ foreach ($fileIdsByStorage as $storageId => $fileIds) {
+ $deleteQuery->hintShardKey('storage', $storageId, true);
+ $fileChunks = array_chunk($fileIds, self::CHUNK_SIZE);
+ foreach ($fileChunks as $fileChunk) {
+ $deleteQuery->setParameter('file_ids', $fileChunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $deletedEntries += $deleteQuery->executeStatement();
+ }
+ }
+
+ return $deletedEntries;
}
- private function cleanupOrphanedMounts() {
+ private function cleanupOrphanedMounts(): int {
$deletedEntries = 0;
$query = $this->connection->getQueryBuilder();
@@ -102,11 +154,11 @@ class DeleteOrphanedFiles extends Command {
$deletedInLastChunk = self::CHUNK_SIZE;
while ($deletedInLastChunk === self::CHUNK_SIZE) {
$deletedInLastChunk = 0;
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$deletedInLastChunk++;
- $deletedEntries += $deleteQuery->setParameter('storageid', (int) $row['storage_id'])
- ->execute();
+ $deletedEntries += $deleteQuery->setParameter('storageid', (int)$row['storage_id'])
+ ->executeStatement();
}
$result->closeCursor();
}
diff --git a/apps/files/lib/Command/Get.php b/apps/files/lib/Command/Get.php
new file mode 100644
index 00000000000..60e028f615e
--- /dev/null
+++ b/apps/files/lib/Command/Get.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command;
+
+use OC\Core\Command\Info\FileUtils;
+use OCP\Files\File;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Get extends Command {
+ public function __construct(
+ private FileUtils $fileUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:get')
+ ->setDescription('Get the contents of a file')
+ ->addArgument('file', InputArgument::REQUIRED, 'Source file id or Nextcloud path')
+ ->addArgument('output', InputArgument::OPTIONAL, 'Target local file to output to, defaults to STDOUT');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $fileInput = $input->getArgument('file');
+ $outputName = $input->getArgument('output');
+ $node = $this->fileUtils->getNode($fileInput);
+
+ if (!$node) {
+ $output->writeln("<error>file $fileInput not found</error>");
+ return self::FAILURE;
+ }
+
+ if (!($node instanceof File)) {
+ $output->writeln("<error>$fileInput is a directory</error>");
+ return self::FAILURE;
+ }
+
+ $isTTY = stream_isatty(STDOUT);
+ if ($outputName === null && $isTTY && $node->getMimePart() !== 'text') {
+ $output->writeln([
+ '<error>Warning: Binary output can mess up your terminal</error>',
+ " Use <info>occ files:get $fileInput -</info> to output it to the terminal anyway",
+ " Or <info>occ files:get $fileInput <FILE></info> to save to a file instead"
+ ]);
+ return self::FAILURE;
+ }
+ $source = $node->fopen('r');
+ if (!$source) {
+ $output->writeln("<error>Failed to open $fileInput for reading</error>");
+ return self::FAILURE;
+ }
+ $target = ($outputName === null || $outputName === '-') ? STDOUT : fopen($outputName, 'w');
+ if (!$target) {
+ $output->writeln("<error>Failed to open $outputName for reading</error>");
+ return self::FAILURE;
+ }
+
+ stream_copy_to_stream($source, $target);
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files/lib/Command/Move.php b/apps/files/lib/Command/Move.php
new file mode 100644
index 00000000000..29dd8860b2a
--- /dev/null
+++ b/apps/files/lib/Command/Move.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command;
+
+use OC\Core\Command\Info\FileUtils;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class Move extends Command {
+ public function __construct(
+ private FileUtils $fileUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:move')
+ ->setDescription('Move a file or folder')
+ ->addArgument('source', InputArgument::REQUIRED, 'Source file id or path')
+ ->addArgument('target', InputArgument::REQUIRED, 'Target path')
+ ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $sourceInput = $input->getArgument('source');
+ $targetInput = $input->getArgument('target');
+ $force = $input->getOption('force');
+
+ $node = $this->fileUtils->getNode($sourceInput);
+ $targetNode = $this->fileUtils->getNode($targetInput);
+
+ if (!$node) {
+ $output->writeln("<error>file $sourceInput not found</error>");
+ return 1;
+ }
+
+ $targetParentPath = dirname(rtrim($targetInput, '/'));
+ $targetParent = $this->fileUtils->getNode($targetParentPath);
+ if (!$targetParent) {
+ $output->writeln("<error>Target parent path $targetParentPath doesn't exist</error>");
+ return 1;
+ }
+
+ $wouldRequireDelete = false;
+
+ if ($targetNode) {
+ if (!$targetNode->isUpdateable()) {
+ $output->writeln("<error>$targetInput already exists and isn't writable</error>");
+ return 1;
+ }
+
+ if ($node instanceof Folder && $targetNode instanceof File) {
+ $output->writeln("Warning: <info>$sourceInput</info> is a folder, but <info>$targetInput</info> is a file");
+ $wouldRequireDelete = true;
+ }
+
+ if ($node instanceof File && $targetNode instanceof Folder) {
+ $output->writeln("Warning: <info>$sourceInput</info> is a file, but <info>$targetInput</info> is a folder");
+ $wouldRequireDelete = true;
+ }
+
+ if ($wouldRequireDelete && $targetNode->getInternalPath() === '') {
+ $output->writeln("<error>Mount root can't be overwritten with a different type</error>");
+ return 1;
+ }
+
+ if ($wouldRequireDelete && !$targetNode->isDeletable()) {
+ $output->writeln("<error>$targetInput can't be deleted to be replaced with $sourceInput</error>");
+ return 1;
+ }
+
+ if (!$force) {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+
+ $question = new ConfirmationQuestion('<info>' . $targetInput . '</info> already exists, overwrite? [y/N] ', false);
+ if (!$helper->ask($input, $output, $question)) {
+ return 1;
+ }
+ }
+ }
+
+ if ($wouldRequireDelete && $targetNode) {
+ $targetNode->delete();
+ }
+
+ $node->move($targetInput);
+
+ return 0;
+ }
+
+}
diff --git a/apps/files/lib/Command/Object/Delete.php b/apps/files/lib/Command/Object/Delete.php
new file mode 100644
index 00000000000..07613ecc616
--- /dev/null
+++ b/apps/files/lib/Command/Object/Delete.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class Delete extends Command {
+ public function __construct(
+ private ObjectUtil $objectUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:object:delete')
+ ->setDescription('Delete an object from the object store')
+ ->addArgument('object', InputArgument::REQUIRED, 'Object to delete')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to delete the object from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $object = $input->getArgument('object');
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return -1;
+ }
+
+ if ($fileId = $this->objectUtils->objectExistsInDb($object)) {
+ $output->writeln("<error>Warning, object $object belongs to an existing file, deleting the object will lead to unexpected behavior if not replaced</error>");
+ $output->writeln(" Note: use <info>occ files:delete $fileId</info> to delete the file cleanly or <info>occ info:file $fileId</info> for more information about the file");
+ $output->writeln('');
+ }
+
+ if (!$objectStore->objectExists($object)) {
+ $output->writeln("<error>Object $object does not exist</error>");
+ return -1;
+ }
+
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion("Delete $object? [y/N] ", false);
+ if ($helper->ask($input, $output, $question)) {
+ $objectStore->deleteObject($object);
+ }
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files/lib/Command/Object/Get.php b/apps/files/lib/Command/Object/Get.php
new file mode 100644
index 00000000000..c32de020c5a
--- /dev/null
+++ b/apps/files/lib/Command/Object/Get.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Get extends Command {
+ public function __construct(
+ private ObjectUtil $objectUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:object:get')
+ ->setDescription('Get the contents of an object')
+ ->addArgument('object', InputArgument::REQUIRED, 'Object to get')
+ ->addArgument('output', InputArgument::REQUIRED, 'Target local file to output to, use - for STDOUT')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to get the object from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $object = $input->getArgument('object');
+ $outputName = $input->getArgument('output');
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return self::FAILURE;
+ }
+
+ if (!$objectStore->objectExists($object)) {
+ $output->writeln("<error>Object $object does not exist</error>");
+ return self::FAILURE;
+ }
+
+ try {
+ $source = $objectStore->readObject($object);
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ $output->writeln("<error>Failed to read $object from object store: $msg</error>");
+ return self::FAILURE;
+ }
+ $target = $outputName === '-' ? STDOUT : fopen($outputName, 'w');
+ if (!$target) {
+ $output->writeln("<error>Failed to open $outputName for writing</error>");
+ return self::FAILURE;
+ }
+
+ stream_copy_to_stream($source, $target);
+ return self::SUCCESS;
+ }
+
+}
diff --git a/apps/files/lib/Command/Object/Info.php b/apps/files/lib/Command/Object/Info.php
new file mode 100644
index 00000000000..6748de37cfe
--- /dev/null
+++ b/apps/files/lib/Command/Object/Info.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OC\Core\Command\Base;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\ObjectStore\IObjectStoreMetaData;
+use OCP\Util;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Info extends Base {
+ public function __construct(
+ private ObjectUtil $objectUtils,
+ private IMimeTypeDetector $mimeTypeDetector,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:info')
+ ->setDescription('Get the metadata of an object')
+ ->addArgument('object', InputArgument::REQUIRED, 'Object to get')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to get the object from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $object = $input->getArgument('object');
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return self::FAILURE;
+ }
+
+ if (!$objectStore instanceof IObjectStoreMetaData) {
+ $output->writeln('<error>Configured object store does currently not support retrieve metadata</error>');
+ return self::FAILURE;
+ }
+
+ if (!$objectStore->objectExists($object)) {
+ $output->writeln("<error>Object $object does not exist</error>");
+ return self::FAILURE;
+ }
+
+ try {
+ $meta = $objectStore->getObjectMetaData($object);
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ $output->writeln("<error>Failed to read $object from object store: $msg</error>");
+ return self::FAILURE;
+ }
+
+ if ($input->getOption('output') === 'plain' && isset($meta['size'])) {
+ $meta['size'] = Util::humanFileSize($meta['size']);
+ }
+ if (isset($meta['mtime'])) {
+ $meta['mtime'] = $meta['mtime']->format(\DateTimeImmutable::ATOM);
+ }
+ if (!isset($meta['mimetype'])) {
+ $handle = $objectStore->readObject($object);
+ $head = fread($handle, 8192);
+ fclose($handle);
+ $meta['mimetype'] = $this->mimeTypeDetector->detectString($head);
+ }
+
+ $this->writeArrayInOutputFormat($input, $output, $meta);
+
+ return self::SUCCESS;
+ }
+
+}
diff --git a/apps/files/lib/Command/Object/ListObject.php b/apps/files/lib/Command/Object/ListObject.php
new file mode 100644
index 00000000000..5d30232e09f
--- /dev/null
+++ b/apps/files/lib/Command/Object/ListObject.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OC\Core\Command\Base;
+use OCP\Files\ObjectStore\IObjectStoreMetaData;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListObject extends Base {
+ private const CHUNK_SIZE = 100;
+
+ public function __construct(
+ private readonly ObjectUtil $objectUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:list')
+ ->setDescription('List all objects in the object store')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return self::FAILURE;
+ }
+
+ if (!$objectStore instanceof IObjectStoreMetaData) {
+ $output->writeln('<error>Configured object store does currently not support listing objects</error>');
+ return self::FAILURE;
+ }
+ $objects = $objectStore->listObjects();
+ $objects = $this->objectUtils->formatObjects($objects, $input->getOption('output') === self::OUTPUT_FORMAT_PLAIN);
+ $this->writeStreamingTableInOutputFormat($input, $output, $objects, self::CHUNK_SIZE);
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files/lib/Command/Object/Multi/Rename.php b/apps/files/lib/Command/Object/Multi/Rename.php
new file mode 100644
index 00000000000..562c68eb07f
--- /dev/null
+++ b/apps/files/lib/Command/Object/Multi/Rename.php
@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object\Multi;
+
+use OC\Core\Command\Base;
+use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class Rename extends Base {
+ public function __construct(
+ private readonly IDBConnection $connection,
+ private readonly PrimaryObjectStoreConfig $objectStoreConfig,
+ private readonly IConfig $config,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:multi:rename-config')
+ ->setDescription('Rename an object store configuration and move all users over to the new configuration,')
+ ->addArgument('source', InputArgument::REQUIRED, 'Object store configuration to rename')
+ ->addArgument('target', InputArgument::REQUIRED, 'New name for the object store configuration');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $source = $input->getArgument('source');
+ $target = $input->getArgument('target');
+
+ $configs = $this->objectStoreConfig->getObjectStoreConfigs();
+ if (!isset($configs[$source])) {
+ $output->writeln('<error>Unknown object store configuration: ' . $source . '</error>');
+ return 1;
+ }
+
+ if ($source === 'root') {
+ $output->writeln('<error>Renaming the root configuration is not supported.</error>');
+ return 1;
+ }
+
+ if ($source === 'default') {
+ $output->writeln('<error>Renaming the default configuration is not supported.</error>');
+ return 1;
+ }
+
+ if (!isset($configs[$target])) {
+ $output->writeln('<comment>Target object store configuration ' . $target . ' doesn\'t exist yet.</comment>');
+ $output->writeln('The target configuration can be created automatically.');
+ $output->writeln('However, as this depends on modifying the config.php, this only works as long as the instance runs on a single node or all nodes in a clustered setup have a shared config file (such as from a shared network mount).');
+ $output->writeln('If the different nodes have a separate copy of the config.php file, the automatic object store configuration creation will lead to the configuration going out of sync.');
+ $output->writeln('If these requirements are not met, you can manually create the target object store configuration in each node\'s configuration before running the command.');
+ $output->writeln('');
+ $output->writeln('<error>Failure to check these requirements will lead to data loss for users.</error>');
+
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion('Automatically create target object store configuration? [y/N] ', false);
+ if ($helper->ask($input, $output, $question)) {
+ $configs[$target] = $configs[$source];
+
+ // update all aliases
+ foreach ($configs as &$config) {
+ if ($config === $source) {
+ $config = $target;
+ }
+ }
+ $this->config->setSystemValue('objectstore', $configs);
+ } else {
+ return 0;
+ }
+ } elseif (($configs[$source] !== $configs[$target]) || $configs[$source] !== $target) {
+ $output->writeln('<error>Source and target configuration differ.</error>');
+ $output->writeln('');
+ $output->writeln('To ensure proper migration of users, the source and target configuration must be the same to ensure that the objects for the moved users exist on the target configuration.');
+ $output->writeln('The usual migration process consists of creating a clone of the old configuration, moving the users from the old configuration to the new one, and then adjust the old configuration that is longer used.');
+ return 1;
+ }
+
+ $query = $this->connection->getQueryBuilder();
+ $query->update('preferences')
+ ->set('configvalue', $query->createNamedParameter($target))
+ ->where($query->expr()->eq('appid', $query->createNamedParameter('homeobjectstore')))
+ ->andWhere($query->expr()->eq('configkey', $query->createNamedParameter('objectstore')))
+ ->andWhere($query->expr()->eq('configvalue', $query->createNamedParameter($source)));
+ $count = $query->executeStatement();
+
+ if ($count > 0) {
+ $output->writeln('Moved <info>' . $count . '</info> users');
+ } else {
+ $output->writeln('No users moved');
+ }
+
+ return 0;
+ }
+}
diff --git a/apps/files/lib/Command/Object/Multi/Users.php b/apps/files/lib/Command/Object/Multi/Users.php
new file mode 100644
index 00000000000..e8f7d012641
--- /dev/null
+++ b/apps/files/lib/Command/Object/Multi/Users.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object\Multi;
+
+use OC\Core\Command\Base;
+use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Users extends Base {
+ public function __construct(
+ private readonly IUserManager $userManager,
+ private readonly PrimaryObjectStoreConfig $objectStoreConfig,
+ private readonly IConfig $config,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:multi:users')
+ ->setDescription('Get the mapping between users and object store buckets')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, 'Only list users using the specified bucket')
+ ->addOption('object-store', 'o', InputOption::VALUE_REQUIRED, 'Only list users using the specified object store configuration')
+ ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Only show the mapping for the specified user, ignores all other options');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ if ($userId = $input->getOption('user')) {
+ $user = $this->userManager->get($userId);
+ if (!$user) {
+ $output->writeln("<error>User $userId not found</error>");
+ return 1;
+ }
+ $users = new \ArrayIterator([$user]);
+ } else {
+ $bucket = (string)$input->getOption('bucket');
+ $objectStore = (string)$input->getOption('object-store');
+ if ($bucket !== '' && $objectStore === '') {
+ $users = $this->getUsers($this->config->getUsersForUserValue('homeobjectstore', 'bucket', $bucket));
+ } elseif ($bucket === '' && $objectStore !== '') {
+ $users = $this->getUsers($this->config->getUsersForUserValue('homeobjectstore', 'objectstore', $objectStore));
+ } elseif ($bucket) {
+ $users = $this->getUsers(array_intersect(
+ $this->config->getUsersForUserValue('homeobjectstore', 'bucket', $bucket),
+ $this->config->getUsersForUserValue('homeobjectstore', 'objectstore', $objectStore)
+ ));
+ } else {
+ $users = $this->userManager->getSeenUsers();
+ }
+ }
+
+ $this->writeStreamingTableInOutputFormat($input, $output, $this->infoForUsers($users), 100);
+ return 0;
+ }
+
+ /**
+ * @param string[] $userIds
+ * @return \Iterator<IUser>
+ */
+ private function getUsers(array $userIds): \Iterator {
+ foreach ($userIds as $userId) {
+ $user = $this->userManager->get($userId);
+ if ($user) {
+ yield $user;
+ }
+ }
+ }
+
+ /**
+ * @param \Iterator<IUser> $users
+ * @return \Iterator<array>
+ */
+ private function infoForUsers(\Iterator $users): \Iterator {
+ foreach ($users as $user) {
+ yield $this->infoForUser($user);
+ }
+ }
+
+ private function infoForUser(IUser $user): array {
+ return [
+ 'user' => $user->getUID(),
+ 'object-store' => $this->objectStoreConfig->getObjectStoreForUser($user),
+ 'bucket' => $this->objectStoreConfig->getSetBucketForUser($user) ?? 'unset',
+ ];
+ }
+}
diff --git a/apps/files/lib/Command/Object/ObjectUtil.php b/apps/files/lib/Command/Object/ObjectUtil.php
new file mode 100644
index 00000000000..5f053c2c42f
--- /dev/null
+++ b/apps/files/lib/Command/Object/ObjectUtil.php
@@ -0,0 +1,115 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\ObjectStore\IObjectStore;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Util;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ObjectUtil {
+ public function __construct(
+ private IConfig $config,
+ private IDBConnection $connection,
+ ) {
+ }
+
+ private function getObjectStoreConfig(): ?array {
+ $config = $this->config->getSystemValue('objectstore_multibucket');
+ if (is_array($config)) {
+ $config['multibucket'] = true;
+ return $config;
+ }
+ $config = $this->config->getSystemValue('objectstore');
+ if (is_array($config)) {
+ if (!isset($config['multibucket'])) {
+ $config['multibucket'] = false;
+ }
+ return $config;
+ }
+
+ return null;
+ }
+
+ public function getObjectStore(?string $bucket, OutputInterface $output): ?IObjectStore {
+ $config = $this->getObjectStoreConfig();
+ if (!$config) {
+ $output->writeln('<error>Instance is not using primary object store</error>');
+ return null;
+ }
+ if ($config['multibucket'] && !$bucket) {
+ $output->writeln('<error>--bucket option required</error> because <info>multi bucket</info> is enabled.');
+ return null;
+ }
+
+ if (!isset($config['arguments'])) {
+ throw new \Exception('no arguments configured for object store configuration');
+ }
+ if (!isset($config['class'])) {
+ throw new \Exception('no class configured for object store configuration');
+ }
+
+ if ($bucket) {
+ // s3, swift
+ $config['arguments']['bucket'] = $bucket;
+ // azure
+ $config['arguments']['container'] = $bucket;
+ }
+
+ $store = new $config['class']($config['arguments']);
+ if (!$store instanceof IObjectStore) {
+ throw new \Exception('configured object store class is not an object store implementation');
+ }
+ return $store;
+ }
+
+ /**
+ * Check if an object is referenced in the database
+ */
+ public function objectExistsInDb(string $object): int|false {
+ if (!str_starts_with($object, 'urn:oid:')) {
+ return false;
+ }
+
+ $fileId = (int)substr($object, strlen('urn:oid:'));
+ $query = $this->connection->getQueryBuilder();
+ $query->select('fileid')
+ ->from('filecache')
+ ->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
+ $result = $query->executeQuery();
+
+ if ($result->fetchOne() === false) {
+ return false;
+ }
+
+ return $fileId;
+ }
+
+ public function formatObjects(\Iterator $objects, bool $humanOutput): \Iterator {
+ foreach ($objects as $object) {
+ yield $this->formatObject($object, $humanOutput);
+ }
+ }
+
+ public function formatObject(array $object, bool $humanOutput): array {
+ $row = array_merge([
+ 'urn' => $object['urn'],
+ ], ($object['metadata'] ?? []));
+
+ if ($humanOutput && isset($row['size'])) {
+ $row['size'] = Util::humanFileSize($row['size']);
+ }
+ if (isset($row['mtime'])) {
+ $row['mtime'] = $row['mtime']->format(\DateTimeImmutable::ATOM);
+ }
+ return $row;
+ }
+}
diff --git a/apps/files/lib/Command/Object/Orphans.php b/apps/files/lib/Command/Object/Orphans.php
new file mode 100644
index 00000000000..f7132540fc8
--- /dev/null
+++ b/apps/files/lib/Command/Object/Orphans.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OC\Core\Command\Base;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\ObjectStore\IObjectStoreMetaData;
+use OCP\IDBConnection;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Orphans extends Base {
+ private const CHUNK_SIZE = 100;
+
+ private ?IQueryBuilder $query = null;
+
+ public function __construct(
+ private readonly ObjectUtil $objectUtils,
+ private readonly IDBConnection $connection,
+ ) {
+ parent::__construct();
+ }
+
+ private function getQuery(): IQueryBuilder {
+ if (!$this->query) {
+ $this->query = $this->connection->getQueryBuilder();
+ $this->query->select('fileid')
+ ->from('filecache')
+ ->where($this->query->expr()->eq('fileid', $this->query->createParameter('file_id')));
+ }
+ return $this->query;
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:orphans')
+ ->setDescription('List all objects in the object store that don\'t have a matching entry in the database')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return self::FAILURE;
+ }
+
+ if (!$objectStore instanceof IObjectStoreMetaData) {
+ $output->writeln('<error>Configured object store does currently not support listing objects</error>');
+ return self::FAILURE;
+ }
+ $prefixLength = strlen('urn:oid:');
+
+ $objects = $objectStore->listObjects('urn:oid:');
+ $orphans = new \CallbackFilterIterator($objects, function (array $object) use ($prefixLength) {
+ $fileId = (int)substr($object['urn'], $prefixLength);
+ return !$this->fileIdInDb($fileId);
+ });
+
+ $orphans = $this->objectUtils->formatObjects($orphans, $input->getOption('output') === self::OUTPUT_FORMAT_PLAIN);
+ $this->writeStreamingTableInOutputFormat($input, $output, $orphans, self::CHUNK_SIZE);
+
+ return self::SUCCESS;
+ }
+
+ private function fileIdInDb(int $fileId): bool {
+ $query = $this->getQuery();
+ $query->setParameter('file_id', $fileId, IQueryBuilder::PARAM_INT);
+ $result = $query->executeQuery();
+ return $result->fetchOne() !== false;
+ }
+}
diff --git a/apps/files/lib/Command/Object/Put.php b/apps/files/lib/Command/Object/Put.php
new file mode 100644
index 00000000000..8516eb51183
--- /dev/null
+++ b/apps/files/lib/Command/Object/Put.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OCP\Files\IMimeTypeDetector;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class Put extends Command {
+ public function __construct(
+ private ObjectUtil $objectUtils,
+ private IMimeTypeDetector $mimeTypeDetector,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:object:put')
+ ->setDescription('Write a file to the object store')
+ ->addArgument('input', InputArgument::REQUIRED, 'Source local path, use - to read from STDIN')
+ ->addArgument('object', InputArgument::REQUIRED, 'Object to write')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket where to store the object, only required in cases where it can't be determined from the config");
+ ;
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $object = $input->getArgument('object');
+ $inputName = (string)$input->getArgument('input');
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return -1;
+ }
+
+ if ($fileId = $this->objectUtils->objectExistsInDb($object)) {
+ $output->writeln("<error>Warning, object $object belongs to an existing file, overwriting the object contents can lead to unexpected behavior.</error>");
+ $output->writeln("You can use <info>occ files:put $inputName $fileId</info> to write to the file safely.");
+ $output->writeln('');
+
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion('Write to the object anyway? [y/N] ', false);
+ if (!$helper->ask($input, $output, $question)) {
+ return -1;
+ }
+ }
+
+ $source = $inputName === '-' ? STDIN : fopen($inputName, 'r');
+ if (!$source) {
+ $output->writeln("<error>Failed to open $inputName</error>");
+ return self::FAILURE;
+ }
+ $objectStore->writeObject($object, $source, $this->mimeTypeDetector->detectPath($inputName));
+ return self::SUCCESS;
+ }
+
+}
diff --git a/apps/files/lib/Command/Put.php b/apps/files/lib/Command/Put.php
new file mode 100644
index 00000000000..fd9d75db78c
--- /dev/null
+++ b/apps/files/lib/Command/Put.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command;
+
+use OC\Core\Command\Info\FileUtils;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Put extends Command {
+ public function __construct(
+ private FileUtils $fileUtils,
+ private IRootFolder $rootFolder,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:put')
+ ->setDescription('Write contents of a file')
+ ->addArgument('input', InputArgument::REQUIRED, 'Source local path, use - to read from STDIN')
+ ->addArgument('file', InputArgument::REQUIRED, 'Target Nextcloud file path to write to or fileid of existing file');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $fileOutput = $input->getArgument('file');
+ $inputName = $input->getArgument('input');
+ $node = $this->fileUtils->getNode($fileOutput);
+
+ if ($node instanceof Folder) {
+ $output->writeln("<error>$fileOutput is a folder</error>");
+ return self::FAILURE;
+ }
+ if (!$node and is_numeric($fileOutput)) {
+ $output->writeln("<error>$fileOutput not found</error>");
+ return self::FAILURE;
+ }
+
+ $source = ($inputName === null || $inputName === '-') ? STDIN : fopen($inputName, 'r');
+ if (!$source) {
+ $output->writeln("<error>Failed to open $inputName</error>");
+ return self::FAILURE;
+ }
+ if ($node instanceof File) {
+ $target = $node->fopen('w');
+ if (!$target) {
+ $output->writeln("<error>Failed to open $fileOutput</error>");
+ return self::FAILURE;
+ }
+ stream_copy_to_stream($source, $target);
+ } else {
+ $this->rootFolder->newFile($fileOutput, $source);
+ }
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files/lib/Command/RepairTree.php b/apps/files/lib/Command/RepairTree.php
index 521fe3d6a4a..622ccba48a3 100644
--- a/apps/files/lib/Command/RepairTree.php
+++ b/apps/files/lib/Command/RepairTree.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Command;
@@ -33,17 +16,13 @@ use Symfony\Component\Console\Output\OutputInterface;
class RepairTree extends Command {
public const CHUNK_SIZE = 200;
- /**
- * @var IDBConnection
- */
- protected $connection;
-
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ protected IDBConnection $connection,
+ ) {
parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('files:repair-tree')
->setDescription('Try and repair malformed filesystem tree structures')
@@ -54,7 +33,7 @@ class RepairTree extends Command {
$rows = $this->findBrokenTreeBits();
$fix = !$input->getOption('dry-run');
- $output->writeln("Found " . count($rows) . " file entries with an invalid path");
+ $output->writeln('Found ' . count($rows) . ' file entries with an invalid path');
if ($fix) {
$this->connection->beginTransaction();
@@ -90,7 +69,7 @@ class RepairTree extends Command {
$this->connection->commit();
}
- return 0;
+ return self::SUCCESS;
}
private function getFileId(int $storage, string $path) {
@@ -102,7 +81,7 @@ class RepairTree extends Command {
return $query->execute()->fetch(\PDO::FETCH_COLUMN);
}
- private function deleteById(int $fileId) {
+ private function deleteById(int $fileId): void {
$query = $this->connection->getQueryBuilder();
$query->delete('filecache')
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId)));
diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php
new file mode 100644
index 00000000000..88d41d1cb5e
--- /dev/null
+++ b/apps/files/lib/Command/SanitizeFilenames.php
@@ -0,0 +1,151 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Command;
+
+use Exception;
+use OC\Core\Command\Base;
+use OC\Files\FilenameValidator;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotPermittedException;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Lock\LockedException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SanitizeFilenames extends Base {
+
+ private OutputInterface $output;
+ private ?string $charReplacement;
+ private bool $dryRun;
+
+ public function __construct(
+ private IUserManager $userManager,
+ private IRootFolder $rootFolder,
+ private IUserSession $session,
+ private IFactory $l10nFactory,
+ private FilenameValidator $filenameValidator,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName('files:sanitize-filenames')
+ ->setDescription('Renames files to match naming constraints')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
+ 'will only rename files the given user(s) have access to'
+ )
+ ->addOption(
+ 'dry-run',
+ mode: InputOption::VALUE_NONE,
+ description: 'Do not actually rename any files but just check filenames.',
+ )
+ ->addOption(
+ 'char-replacement',
+ 'c',
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'Replacement for invalid character (by default space, underscore or dash is used)',
+ );
+
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $this->charReplacement = $input->getOption('char-replacement');
+ // check if replacement is needed
+ $c = $this->filenameValidator->getForbiddenCharacters();
+ if (count($c) > 0) {
+ try {
+ $this->filenameValidator->sanitizeFilename($c[0], $this->charReplacement);
+ } catch (\InvalidArgumentException) {
+ if ($this->charReplacement === null) {
+ $output->writeln('<error>Character replacement required</error>');
+ } else {
+ $output->writeln('<error>Invalid character replacement given</error>');
+ }
+ return 1;
+ }
+ }
+
+ $this->dryRun = $input->getOption('dry-run');
+ if ($this->dryRun) {
+ $output->writeln('<info>Dry run is enabled, no actual renaming will be applied.</>');
+ }
+
+ $this->output = $output;
+ $users = $input->getArgument('user_id');
+ if (!empty($users)) {
+ foreach ($users as $userId) {
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $output->writeln("<error>User '$userId' does not exist - skipping</>");
+ continue;
+ }
+ $this->sanitizeUserFiles($user);
+ }
+ } else {
+ $this->userManager->callForSeenUsers($this->sanitizeUserFiles(...));
+ }
+ return self::SUCCESS;
+ }
+
+ private function sanitizeUserFiles(IUser $user): void {
+ // Set an active user so that event listeners can correctly work (e.g. files versions)
+ $this->session->setVolatileActiveUser($user);
+
+ $this->output->writeln('<info>Analyzing files of ' . $user->getUID() . '</>');
+
+ $folder = $this->rootFolder->getUserFolder($user->getUID());
+ $this->sanitizeFiles($folder);
+ }
+
+ private function sanitizeFiles(Folder $folder): void {
+ foreach ($folder->getDirectoryListing() as $node) {
+ $this->output->writeln('scanning: ' . $node->getPath(), OutputInterface::VERBOSITY_VERBOSE);
+
+ try {
+ $oldName = $node->getName();
+ $newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement);
+ if ($oldName !== $newName) {
+ $newName = $folder->getNonExistingName($newName);
+ $path = rtrim(dirname($node->getPath()), '/');
+
+ if (!$this->dryRun) {
+ $node->move("$path/$newName");
+ } elseif (!$folder->isCreatable()) {
+ // simulate error for dry run
+ throw new NotPermittedException();
+ }
+ $this->output->writeln('renamed: "' . $oldName . '" to "' . $newName . '"');
+ }
+ } catch (LockedException) {
+ $this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (file is locked)</>');
+ } catch (NotPermittedException) {
+ $this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (no permissions)</>');
+ } catch (Exception $error) {
+ $this->output->writeln('<error>failed: ' . $node->getPath() . '</>');
+ $this->output->writeln('<error>' . $error->getMessage() . '</>', OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_VERBOSE);
+ }
+
+ if ($node instanceof Folder) {
+ $this->sanitizeFiles($node);
+ }
+ }
+ }
+
+}
diff --git a/apps/files/lib/Command/Scan.php b/apps/files/lib/Command/Scan.php
index 710c76de493..b9057139b0e 100644
--- a/apps/files/lib/Command/Scan.php
+++ b/apps/files/lib/Command/Scan.php
@@ -1,35 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Blaok <i@blaok.me>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Joel S <joel.devbox@protonmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author martin.mattel@diemattels.at <martin.mattel@diemattels.at>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\Command;
@@ -37,15 +11,22 @@ use OC\Core\Command\Base;
use OC\Core\Command\InterruptedException;
use OC\DB\Connection;
use OC\DB\ConnectionAdapter;
-use OCP\Files\File;
+use OC\Files\Storage\Wrapper\Jail;
+use OC\Files\Utils\Scanner;
+use OC\FilesMetadata\FilesMetadataManager;
use OC\ForbiddenException;
-use OC\Metadata\MetadataManager;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Events\FileCacheUpdated;
+use OCP\Files\Events\NodeAddedToCache;
+use OCP\Files\Events\NodeRemovedFromCache;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
use OCP\Files\StorageNotAvailableException;
+use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IUserManager;
+use OCP\Lock\LockedException;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
@@ -54,25 +35,25 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Scan extends Base {
- private IUserManager $userManager;
protected float $execTime = 0;
protected int $foldersCounter = 0;
protected int $filesCounter = 0;
- private IRootFolder $root;
- private MetadataManager $metadataManager;
+ protected int $errorsCounter = 0;
+ protected int $newCounter = 0;
+ protected int $updatedCounter = 0;
+ protected int $removedCounter = 0;
public function __construct(
- IUserManager $userManager,
- IRootFolder $rootFolder,
- MetadataManager $metadataManager
+ private IUserManager $userManager,
+ private IRootFolder $rootFolder,
+ private FilesMetadataManager $filesMetadataManager,
+ private IEventDispatcher $eventDispatcher,
+ private LoggerInterface $logger,
) {
- $this->userManager = $userManager;
parent::__construct();
- $this->root = $rootFolder;
- $this->metadataManager = $metadataManager;
}
- protected function configure() {
+ protected function configure(): void {
parent::configure();
$this
@@ -86,14 +67,15 @@ class Scan extends Base {
->addOption(
'path',
'p',
- InputArgument::OPTIONAL,
+ InputOption::VALUE_REQUIRED,
'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored'
)
->addOption(
'generate-metadata',
null,
- InputOption::VALUE_NONE,
- 'Generate metadata for all scanned files'
+ InputOption::VALUE_OPTIONAL,
+ 'Generate metadata for all scanned files; if specified only generate for named value',
+ ''
)
->addOption(
'all',
@@ -118,63 +100,95 @@ class Scan extends Base {
);
}
- protected function scanFiles(string $user, string $path, bool $scanMetadata, OutputInterface $output, bool $backgroundScan = false, bool $recursive = true, bool $homeOnly = false): void {
+ protected function scanFiles(
+ string $user,
+ string $path,
+ ?string $scanMetadata,
+ OutputInterface $output,
+ callable $mountFilter,
+ bool $backgroundScan = false,
+ bool $recursive = true,
+ ): void {
$connection = $this->reconnectToDatabase($output);
- $scanner = new \OC\Files\Utils\Scanner(
+ $scanner = new Scanner(
$user,
new ConnectionAdapter($connection),
- \OC::$server->get(IEventDispatcher::class),
- \OC::$server->get(LoggerInterface::class)
+ Server::get(IEventDispatcher::class),
+ Server::get(LoggerInterface::class)
);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata): void {
$output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->filesCounter;
$this->abortIfInterrupted();
- if ($scanMetadata) {
- $node = $this->root->get($path);
- if ($node instanceof File) {
- $this->metadataManager->generateMetadata($node, false);
- }
+ if ($scanMetadata !== null) {
+ $node = $this->rootFolder->get($path);
+ $this->filesMetadataManager->refreshMetadata(
+ $node,
+ ($scanMetadata !== '') ? IFilesMetadataManager::PROCESS_NAMED : IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND,
+ $scanMetadata
+ );
}
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output): void {
$output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->foldersCounter;
$this->abortIfInterrupted();
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output): void {
$output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
+ ++$this->errorsCounter;
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output): void {
$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
+ ++$this->errorsCounter;
+ });
+
+ $this->eventDispatcher->addListener(NodeAddedToCache::class, function (): void {
+ ++$this->newCounter;
+ });
+ $this->eventDispatcher->addListener(FileCacheUpdated::class, function (): void {
+ ++$this->updatedCounter;
+ });
+ $this->eventDispatcher->addListener(NodeRemovedFromCache::class, function (): void {
+ ++$this->removedCounter;
});
try {
if ($backgroundScan) {
$scanner->backgroundScan($path);
} else {
- $scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null);
+ $scanner->scan($path, $recursive, $mountFilter);
}
} catch (ForbiddenException $e) {
$output->writeln("<error>Home storage for user $user not writable or 'files' subdirectory missing</error>");
+ $output->writeln(' ' . $e->getMessage());
$output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
+ ++$this->errorsCounter;
} catch (InterruptedException $e) {
# exit the function if ctrl-c has been pressed
$output->writeln('Interrupted by user');
} catch (NotFoundException $e) {
$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
+ ++$this->errorsCounter;
+ } catch (LockedException $e) {
+ if (str_starts_with($e->getPath(), 'scanner::')) {
+ $output->writeln('<error>Another process is already scanning \'' . substr($e->getPath(), strlen('scanner::')) . '\'</error>');
+ } else {
+ throw $e;
+ }
} catch (\Exception $e) {
$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
+ ++$this->errorsCounter;
}
}
- public function filterHomeMount(IMountPoint $mountPoint) {
+ public function isHomeMount(IMountPoint $mountPoint): bool {
// any mountpoint inside '/$user/files/'
return substr_count($mountPoint->getMountPoint(), '/') <= 3;
}
@@ -191,30 +205,62 @@ class Scan extends Base {
$users = $input->getArgument('user_id');
}
- # restrict the verbosity level to VERBOSITY_VERBOSE
- if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
- $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
- }
-
# check quantity of users to be process and show it on the command line
$users_total = count($users);
if ($users_total === 0) {
$output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>');
- return 1;
+ return self::FAILURE;
+ }
+
+ $this->initTools($output);
+
+ // getOption() logic on VALUE_OPTIONAL
+ $metadata = null; // null if --generate-metadata is not set, empty if option have no value, value if set
+ if ($input->getOption('generate-metadata') !== '') {
+ $metadata = $input->getOption('generate-metadata') ?? '';
}
- $this->initTools();
+ $homeOnly = $input->getOption('home-only');
+ $scannedStorages = [];
+ $mountFilter = function (IMountPoint $mount) use ($homeOnly, &$scannedStorages) {
+ if ($homeOnly && !$this->isHomeMount($mount)) {
+ return false;
+ }
+
+ // when scanning multiple users, the scanner might encounter the same storage multiple times (e.g. external storages, or group folders)
+ // we can filter out any storage we've already scanned to avoid double work
+ $storage = $mount->getStorage();
+ $storageKey = $storage->getId();
+ while ($storage->instanceOfStorage(Jail::class)) {
+ $storageKey .= '/' . $storage->getUnjailedPath('');
+ $storage = $storage->getUnjailedStorage();
+ }
+ if (array_key_exists($storageKey, $scannedStorages)) {
+ return false;
+ }
+
+ $scannedStorages[$storageKey] = true;
+ return true;
+ };
$user_count = 0;
foreach ($users as $user) {
if (is_object($user)) {
$user = $user->getUID();
}
- $path = $inputPath ? $inputPath : '/' . $user;
+ $path = $inputPath ?: '/' . $user;
++$user_count;
if ($this->userManager->userExists($user)) {
$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
- $this->scanFiles($user, $path, $input->getOption('generate-metadata'), $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only'));
+ $this->scanFiles(
+ $user,
+ $path,
+ $metadata,
+ $output,
+ $mountFilter,
+ $input->getOption('unscanned'),
+ !$input->getOption('shallow'),
+ );
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
} else {
$output->writeln("<error>Unknown user $user_count $user</error>");
@@ -229,21 +275,25 @@ class Scan extends Base {
}
$this->presentStats($output);
- return 0;
+ return self::SUCCESS;
}
/**
* Initialises some useful tools for the Command
*/
- protected function initTools() {
+ protected function initTools(OutputInterface $output): void {
// Start the timer
$this->execTime = -microtime(true);
// Convert PHP errors to exceptions
- set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
+ set_error_handler(
+ fn (int $severity, string $message, string $file, int $line): bool
+ => $this->exceptionErrorHandler($output, $severity, $message, $file, $line),
+ E_ALL
+ );
}
/**
- * Processes PHP errors as exceptions in order to be able to keep track of problems
+ * Processes PHP errors in order to be able to show them in the output
*
* @see https://www.php.net/manual/en/function.set-error-handler.php
*
@@ -251,47 +301,44 @@ class Scan extends Base {
* @param string $message
* @param string $file the filename that the error was raised in
* @param int $line the line number the error was raised
- *
- * @throws \ErrorException
*/
- public function exceptionErrorHandler($severity, $message, $file, $line) {
- if (!(error_reporting() & $severity)) {
- // This error code is not included in error_reporting
- return;
+ public function exceptionErrorHandler(OutputInterface $output, int $severity, string $message, string $file, int $line): bool {
+ if (($severity === E_DEPRECATED) || ($severity === E_USER_DEPRECATED)) {
+ // Do not show deprecation warnings
+ return false;
}
- throw new \ErrorException($message, 0, $severity, $file, $line);
+ $e = new \ErrorException($message, 0, $severity, $file, $line);
+ $output->writeln('<error>Error during scan: ' . $e->getMessage() . '</error>');
+ $output->writeln('<error>' . $e->getTraceAsString() . '</error>', OutputInterface::VERBOSITY_VERY_VERBOSE);
+ ++$this->errorsCounter;
+ return true;
}
- /**
- * @param OutputInterface $output
- */
- protected function presentStats(OutputInterface $output) {
+ protected function presentStats(OutputInterface $output): void {
// Stop the timer
$this->execTime += microtime(true);
+ $this->logger->info("Completed scan of {$this->filesCounter} files in {$this->foldersCounter} folder. Found {$this->newCounter} new, {$this->updatedCounter} updated and {$this->removedCounter} removed items");
+
$headers = [
- 'Folders', 'Files', 'Elapsed time'
+ 'Folders',
+ 'Files',
+ 'New',
+ 'Updated',
+ 'Removed',
+ 'Errors',
+ 'Elapsed time',
];
-
- $this->showSummary($headers, null, $output);
- }
-
- /**
- * Shows a summary of operations
- *
- * @param string[] $headers
- * @param string[] $rows
- * @param OutputInterface $output
- */
- protected function showSummary($headers, $rows, OutputInterface $output) {
$niceDate = $this->formatExecTime();
- if (!$rows) {
- $rows = [
- $this->foldersCounter,
- $this->filesCounter,
- $niceDate,
- ];
- }
+ $rows = [
+ $this->foldersCounter,
+ $this->filesCounter,
+ $this->newCounter,
+ $this->updatedCounter,
+ $this->removedCounter,
+ $this->errorsCounter,
+ $niceDate,
+ ];
$table = new Table($output);
$table
->setHeaders($headers)
@@ -301,11 +348,9 @@ class Scan extends Base {
/**
- * Formats microtime into a human readable format
- *
- * @return string
+ * Formats microtime into a human-readable format
*/
- protected function formatExecTime() {
+ protected function formatExecTime(): string {
$secs = (int)round($this->execTime);
# convert seconds into HH:MM:SS form
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), $secs % 60);
@@ -313,7 +358,7 @@ class Scan extends Base {
protected function reconnectToDatabase(OutputInterface $output): Connection {
/** @var Connection $connection */
- $connection = \OC::$server->get(Connection::class);
+ $connection = Server::get(Connection::class);
try {
$connection->close();
} catch (\Exception $ex) {
diff --git a/apps/files/lib/Command/ScanAppData.php b/apps/files/lib/Command/ScanAppData.php
index 63e13733b2a..0e08c6a8cfe 100644
--- a/apps/files/lib/Command/ScanAppData.php
+++ b/apps/files/lib/Command/ScanAppData.php
@@ -1,31 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Joel S <joel.devbox@protonmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Erik Wouters <6179932+EWouters@users.noreply.github.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Command;
@@ -33,12 +10,16 @@ use OC\Core\Command\Base;
use OC\Core\Command\InterruptedException;
use OC\DB\Connection;
use OC\DB\ConnectionAdapter;
+use OC\Files\Utils\Scanner;
use OC\ForbiddenException;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Folder;
use OCP\Files\IRootFolder;
+use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
@@ -46,26 +27,20 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ScanAppData extends Base {
+ protected float $execTime = 0;
- /** @var IRootFolder */
- protected $root;
- /** @var IConfig */
- protected $config;
- /** @var float */
- protected $execTime = 0;
- /** @var int */
- protected $foldersCounter = 0;
- /** @var int */
- protected $filesCounter = 0;
-
- public function __construct(IRootFolder $rootFolder, IConfig $config) {
- parent::__construct();
+ protected int $foldersCounter = 0;
- $this->root = $rootFolder;
- $this->config = $config;
+ protected int $filesCounter = 0;
+
+ public function __construct(
+ protected IRootFolder $rootFolder,
+ protected IConfig $config,
+ ) {
+ parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
parent::configure();
$this
@@ -77,10 +52,11 @@ class ScanAppData extends Base {
protected function scanFiles(OutputInterface $output, string $folder): int {
try {
+ /** @var Folder $appData */
$appData = $this->getAppDataFolder();
} catch (NotFoundException $e) {
$output->writeln('<error>NoAppData folder found</error>');
- return 1;
+ return self::FAILURE;
}
if ($folder !== '') {
@@ -88,36 +64,36 @@ class ScanAppData extends Base {
$appData = $appData->get($folder);
} catch (NotFoundException $e) {
$output->writeln('<error>Could not find folder: ' . $folder . '</error>');
- return 1;
+ return self::FAILURE;
}
}
$connection = $this->reconnectToDatabase($output);
- $scanner = new \OC\Files\Utils\Scanner(
+ $scanner = new Scanner(
null,
new ConnectionAdapter($connection),
- \OC::$server->query(IEventDispatcher::class),
- \OC::$server->get(LoggerInterface::class)
+ Server::get(IEventDispatcher::class),
+ Server::get(LoggerInterface::class)
);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output): void {
$output->writeln("\tFile <info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->filesCounter;
$this->abortIfInterrupted();
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output): void {
$output->writeln("\tFolder <info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->foldersCounter;
$this->abortIfInterrupted();
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output): void {
$output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output): void {
$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
});
@@ -126,21 +102,21 @@ class ScanAppData extends Base {
} catch (ForbiddenException $e) {
$output->writeln('<error>Storage not writable</error>');
$output->writeln('<info>Make sure you\'re running the scan command only as the user the web server runs as</info>');
- return 1;
+ return self::FAILURE;
} catch (InterruptedException $e) {
# exit the function if ctrl-c has been pressed
$output->writeln('<info>Interrupted by user</info>');
- return 1;
+ return self::FAILURE;
} catch (NotFoundException $e) {
$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
- return 1;
+ return self::FAILURE;
} catch (\Exception $e) {
$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
- return 1;
+ return self::FAILURE;
}
- return 0;
+ return self::SUCCESS;
}
@@ -167,7 +143,7 @@ class ScanAppData extends Base {
/**
* Initialises some useful tools for the Command
*/
- protected function initTools() {
+ protected function initTools(): void {
// Start the timer
$this->execTime = -microtime(true);
// Convert PHP errors to exceptions
@@ -194,10 +170,7 @@ class ScanAppData extends Base {
throw new \ErrorException($message, 0, $severity, $file, $line);
}
- /**
- * @param OutputInterface $output
- */
- protected function presentStats(OutputInterface $output) {
+ protected function presentStats(OutputInterface $output): void {
// Stop the timer
$this->execTime += microtime(true);
@@ -213,9 +186,8 @@ class ScanAppData extends Base {
*
* @param string[] $headers
* @param string[] $rows
- * @param OutputInterface $output
*/
- protected function showSummary($headers, $rows, OutputInterface $output) {
+ protected function showSummary($headers, $rows, OutputInterface $output): void {
$niceDate = $this->formatExecTime();
if (!$rows) {
$rows = [
@@ -233,19 +205,17 @@ class ScanAppData extends Base {
/**
- * Formats microtime into a human readable format
- *
- * @return string
+ * Formats microtime into a human-readable format
*/
- protected function formatExecTime() {
+ protected function formatExecTime(): string {
$secs = round($this->execTime);
# convert seconds into HH:MM:SS form
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), (int)$secs % 60);
}
protected function reconnectToDatabase(OutputInterface $output): Connection {
- /** @var Connection $connection*/
- $connection = \OC::$server->get(Connection::class);
+ /** @var Connection $connection */
+ $connection = Server::get(Connection::class);
try {
$connection->close();
} catch (\Exception $ex) {
@@ -263,16 +233,15 @@ class ScanAppData extends Base {
}
/**
- * @return \OCP\Files\Folder
* @throws NotFoundException
*/
- private function getAppDataFolder() {
+ private function getAppDataFolder(): Node {
$instanceId = $this->config->getSystemValue('instanceid', null);
if ($instanceId === null) {
throw new NotFoundException();
}
- return $this->root->get('appdata_'.$instanceId);
+ return $this->rootFolder->get('appdata_' . $instanceId);
}
}
diff --git a/apps/files/lib/Command/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php
index 50aa0b21a5f..f7663e26f28 100644
--- a/apps/files/lib/Command/TransferOwnership.php
+++ b/apps/files/lib/Command/TransferOwnership.php
@@ -1,71 +1,41 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Carla Schroder <carla@owncloud.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sujith Haridasan <sujith.h@gmail.com>
- * @author Sujith H <sharidasan@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tobia De Koninck <LEDfan@users.noreply.github.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\Command;
use OCA\Files\Exception\TransferOwnershipException;
use OCA\Files\Service\OwnershipTransferService;
+use OCA\Files_External\Config\ConfigAdapter;
+use OCP\Files\Mount\IMountManager;
+use OCP\Files\Mount\IMountPoint;
+use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
-use OCP\IConfig;
use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
class TransferOwnership extends Command {
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var OwnershipTransferService */
- private $transferService;
-
- /** @var IConfig */
- private $config;
-
- public function __construct(IUserManager $userManager,
- OwnershipTransferService $transferService,
- IConfig $config) {
+ public function __construct(
+ private IUserManager $userManager,
+ private OwnershipTransferService $transferService,
+ private IConfig $config,
+ private IMountManager $mountManager,
+ ) {
parent::__construct();
- $this->userManager = $userManager;
- $this->transferService = $transferService;
- $this->config = $config;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('files:transfer-ownership')
->setDescription('All files and folders are moved to another user - outgoing shares and incoming user file shares (optionally) are moved as well.')
@@ -94,9 +64,19 @@ class TransferOwnership extends Command {
'transfer-incoming-shares',
null,
InputOption::VALUE_OPTIONAL,
- 'transfer incoming user file shares to destination user. Usage: --transfer-incoming-shares=1 (value required)',
+ 'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore',
'2'
- );
+ )->addOption(
+ 'include-external-storage',
+ null,
+ InputOption::VALUE_NONE,
+ 'include files on external storages, this will _not_ setup an external storage for the target user, but instead moves all the files from the external storages into the target users home directory',
+ )->addOption(
+ 'force-include-external-storage',
+ null,
+ InputOption::VALUE_NONE,
+ 'don\'t ask for confirmation for transferring external storages',
+ );
}
protected function execute(InputInterface $input, OutputInterface $output): int {
@@ -107,59 +87,62 @@ class TransferOwnership extends Command {
if ($input->getArgument(('source-user')) === $input->getArgument('destination-user')) {
$output->writeln("<error>Ownership can't be transferred when Source and Destination users are the same user. Please check your input.</error>");
- return 1;
+ return self::FAILURE;
}
$sourceUserObject = $this->userManager->get($input->getArgument('source-user'));
$destinationUserObject = $this->userManager->get($input->getArgument('destination-user'));
if (!$sourceUserObject instanceof IUser) {
- $output->writeln("<error>Unknown source user " . $input->getArgument('source-user') . "</error>");
- return 1;
+ $output->writeln('<error>Unknown source user ' . $input->getArgument('source-user') . '</error>');
+ return self::FAILURE;
}
if (!$destinationUserObject instanceof IUser) {
- $output->writeln("<error>Unknown destination user " . $input->getArgument('destination-user') . "</error>");
- return 1;
+ $output->writeln('<error>Unknown destination user ' . $input->getArgument('destination-user') . '</error>');
+ return self::FAILURE;
}
- try {
- $includeIncomingArgument = $input->getOption('transfer-incoming-shares');
-
- switch ($includeIncomingArgument) {
- case '0':
- $includeIncoming = false;
- break;
- case '1':
- $includeIncoming = true;
- break;
- case '2':
- $includeIncoming = $this->config->getSystemValue('transferIncomingShares', false);
- if (gettype($includeIncoming) !== 'boolean') {
- $output->writeln("<error> config.php: 'transfer-incoming-shares': wrong usage. Transfer aborted.</error>");
- return 1;
+ $path = ltrim($input->getOption('path'), '/');
+ $includeExternalStorage = $input->getOption('include-external-storage');
+ if ($includeExternalStorage) {
+ $mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/'));
+ /** @var IMountPoint[] $mounts */
+ $mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class);
+ if (count($mounts) > 0) {
+ $output->writeln(count($mounts) . ' external storages will be transferred:');
+ foreach ($mounts as $mount) {
+ $output->writeln(' - <info>' . $mount->getMountPoint() . '</info>');
+ }
+ $output->writeln('');
+ $output->writeln('<comment>Any other users with access to these external storages will lose access to the files.</comment>');
+ $output->writeln('');
+ if (!$input->getOption('force-include-external-storage')) {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion('Are you sure you want to transfer external storages? (y/N) ', false);
+ if (!$helper->ask($input, $output, $question)) {
+ return self::FAILURE;
}
- break;
- default:
- $output->writeln("<error>Option --transfer-incoming-shares: wrong usage. Transfer aborted.</error>");
- return 1;
- break;
+ }
}
+ }
+ try {
$this->transferService->transfer(
$sourceUserObject,
$destinationUserObject,
- ltrim($input->getOption('path'), '/'),
+ $path,
$output,
$input->getOption('move') === true,
false,
- $includeIncoming
+ $includeExternalStorage,
);
} catch (TransferOwnershipException $e) {
- $output->writeln("<error>" . $e->getMessage() . "</error>");
- return $e->getCode() !== 0 ? $e->getCode() : 1;
+ $output->writeln('<error>' . $e->getMessage() . '</error>');
+ return $e->getCode() !== 0 ? $e->getCode() : self::FAILURE;
}
- return 0;
+ return self::SUCCESS;
}
}
diff --git a/apps/files/lib/Command/WindowsCompatibleFilenames.php b/apps/files/lib/Command/WindowsCompatibleFilenames.php
new file mode 100644
index 00000000000..84a1b277824
--- /dev/null
+++ b/apps/files/lib/Command/WindowsCompatibleFilenames.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files\Service\SettingsService;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class WindowsCompatibleFilenames extends Base {
+
+ public function __construct(
+ private SettingsService $service,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName('files:windows-compatible-filenames')
+ ->setDescription('Enforce naming constraints for windows compatible filenames')
+ ->addOption('enable', description: 'Enable windows naming constraints')
+ ->addOption('disable', description: 'Disable windows naming constraints');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ if ($input->getOption('enable')) {
+ if ($this->service->hasFilesWindowsSupport()) {
+ $output->writeln('<error>Windows compatible filenames already enforced.</error>', OutputInterface::VERBOSITY_VERBOSE);
+ }
+ $this->service->setFilesWindowsSupport(true);
+ $output->writeln('Windows compatible filenames enforced.');
+ } elseif ($input->getOption('disable')) {
+ if (!$this->service->hasFilesWindowsSupport()) {
+ $output->writeln('<error>Windows compatible filenames already disabled.</error>', OutputInterface::VERBOSITY_VERBOSE);
+ }
+ $this->service->setFilesWindowsSupport(false);
+ $output->writeln('Windows compatible filename constraints removed.');
+ } else {
+ $output->writeln('Windows compatible filenames are ' . ($this->service->hasFilesWindowsSupport() ? 'enforced' : 'disabled'));
+ }
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php
index 604cf9a3c64..8bb024fb698 100644
--- a/apps/files/lib/Controller/ApiController.php
+++ b/apps/files/lib/Controller/ApiController.php
@@ -1,134 +1,124 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Felix Nüsse <Felix.nuesse@t-online.de>
- * @author fnuesse <felix.nuesse@t-online.de>
- * @author fnuesse <fnuesse@techfak.uni-bielefeld.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Max Kovalenko <mxss1998@yandex.ru>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Nina Pypchenko <22447785+nina-py@users.noreply.github.com>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\Controller;
use OC\Files\Node\Node;
+use OCA\Files\Helper;
+use OCA\Files\ResponseDefinitions;
use OCA\Files\Service\TagService;
use OCA\Files\Service\UserConfig;
+use OCA\Files\Service\ViewConfig;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\ApiRoute;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\Attribute\StrictCookiesRequired;
+use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
+use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\File;
use OCP\Files\Folder;
+use OCP\Files\InvalidPathException;
+use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\Storage\ISharedStorage;
+use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
+use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
+use OCP\IUser;
use OCP\IUserSession;
+use OCP\PreConditionNotMetException;
use OCP\Share\IManager;
use OCP\Share\IShare;
+use Psr\Log\LoggerInterface;
+use Throwable;
/**
- * Class ApiController
+ * @psalm-import-type FilesFolderTree from ResponseDefinitions
*
* @package OCA\Files\Controller
*/
class ApiController extends Controller {
- private TagService $tagService;
- private IManager $shareManager;
- private IPreview $previewManager;
- private IUserSession $userSession;
- private IConfig $config;
- private Folder $userFolder;
- private UserConfig $userConfig;
-
- /**
- * @param string $appName
- * @param IRequest $request
- * @param IUserSession $userSession
- * @param TagService $tagService
- * @param IPreview $previewManager
- * @param IManager $shareManager
- * @param IConfig $config
- * @param Folder $userFolder
- */
- public function __construct($appName,
- IRequest $request,
- IUserSession $userSession,
- TagService $tagService,
- IPreview $previewManager,
- IManager $shareManager,
- IConfig $config,
- Folder $userFolder,
- UserConfig $userConfig) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IUserSession $userSession,
+ private TagService $tagService,
+ private IPreview $previewManager,
+ private IManager $shareManager,
+ private IConfig $config,
+ private ?Folder $userFolder,
+ private UserConfig $userConfig,
+ private ViewConfig $viewConfig,
+ private IL10N $l10n,
+ private IRootFolder $rootFolder,
+ private LoggerInterface $logger,
+ ) {
parent::__construct($appName, $request);
- $this->userSession = $userSession;
- $this->tagService = $tagService;
- $this->previewManager = $previewManager;
- $this->shareManager = $shareManager;
- $this->config = $config;
- $this->userFolder = $userFolder;
- $this->userConfig = $userConfig;
}
/**
* Gets a thumbnail of the specified file
*
* @since API version 1.0
+ * @deprecated 32.0.0 Use the preview endpoint provided by core instead
*
- * @NoAdminRequired
- * @NoCSRFRequired
- * @StrictCookieRequired
- *
- * @param int $x
- * @param int $y
+ * @param int $x Width of the thumbnail
+ * @param int $y Height of the thumbnail
* @param string $file URL-encoded filename
- * @return DataResponse|FileDisplayResponse
+ * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message?: string}, array{}>
+ *
+ * 200: Thumbnail returned
+ * 400: Getting thumbnail is not possible
+ * 404: File not found
*/
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[StrictCookiesRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getThumbnail($x, $y, $file) {
if ($x < 1 || $y < 1) {
return new DataResponse(['message' => 'Requested size must be numeric and a positive value.'], Http::STATUS_BAD_REQUEST);
}
try {
- $file = $this->userFolder->get($file);
- if ($file instanceof Folder) {
+ $file = $this->userFolder?->get($file);
+ if ($file === null
+ || !($file instanceof File)
+ || ($file->getId() <= 0)
+ ) {
throw new NotFoundException();
}
- /** @var File $file */
+ // Validate the user is allowed to download the file (preview is some kind of download)
+ /** @var ISharedStorage $storage */
+ $storage = $file->getStorage();
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var IShare $share */
+ $share = $storage->getShare();
+ if (!$share->canSeeContent()) {
+ throw new NotFoundException();
+ }
+ }
+
$preview = $this->previewManager->getPreview($file, $x, $y, true);
return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]);
- } catch (NotFoundException $e) {
+ } catch (NotFoundException|NotPermittedException|InvalidPathException) {
return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND);
} catch (\Exception $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
@@ -140,23 +130,22 @@ class ApiController extends Controller {
* The passed tags are absolute, which means they will
* replace the actual tag selection.
*
- * @NoAdminRequired
- *
* @param string $path path
* @param array|string $tags array of tags
* @return DataResponse
*/
+ #[NoAdminRequired]
public function updateFileTags($path, $tags = null) {
$result = [];
// if tags specified or empty array, update tags
if (!is_null($tags)) {
try {
$this->tagService->updateFileTags($path, $tags);
- } catch (\OCP\Files\NotFoundException $e) {
+ } catch (NotFoundException $e) {
return new DataResponse([
'message' => $e->getMessage()
], Http::STATUS_NOT_FOUND);
- } catch (\OCP\Files\StorageNotAvailableException $e) {
+ } catch (StorageNotAvailableException $e) {
return new DataResponse([
'message' => $e->getMessage()
], Http::STATUS_SERVICE_UNAVAILABLE);
@@ -178,7 +167,7 @@ class ApiController extends Controller {
$shareTypesForNodes = $this->getShareTypesForNodes($nodes);
return array_values(array_map(function (Node $node) use ($shareTypesForNodes) {
$shareTypes = $shareTypesForNodes[$node->getId()] ?? [];
- $file = \OCA\Files\Helper::formatFileInfo($node->getFileInfo());
+ $file = Helper::formatFileInfo($node->getFileInfo());
$file['hasPreview'] = $this->previewManager->isAvailable($node);
$parts = explode('/', dirname($node->getPath()), 4);
if (isset($parts[3])) {
@@ -209,6 +198,7 @@ class ApiController extends Controller {
IShare::TYPE_EMAIL,
IShare::TYPE_ROOM,
IShare::TYPE_DECK,
+ IShare::TYPE_SCIENCEMESH,
];
$shareTypes = [];
@@ -247,62 +237,143 @@ class ApiController extends Controller {
/**
* Returns a list of recently modified files.
*
- * @NoAdminRequired
- *
* @return DataResponse
*/
+ #[NoAdminRequired]
public function getRecentFiles() {
$nodes = $this->userFolder->getRecent(100);
$files = $this->formatNodes($nodes);
return new DataResponse(['files' => $files]);
}
+ /**
+ * @param \OCP\Files\Node[] $nodes
+ * @param int $depth The depth to traverse into the contents of each node
+ */
+ private function getChildren(array $nodes, int $depth = 1, int $currentDepth = 0): array {
+ if ($currentDepth >= $depth) {
+ return [];
+ }
+
+ $children = [];
+ foreach ($nodes as $node) {
+ if (!($node instanceof Folder)) {
+ continue;
+ }
+
+ $basename = basename($node->getPath());
+ $entry = [
+ 'id' => $node->getId(),
+ 'basename' => $basename,
+ 'children' => $this->getChildren($node->getDirectoryListing(), $depth, $currentDepth + 1),
+ ];
+ $displayName = $node->getName();
+ if ($basename !== $displayName) {
+ $entry['displayName'] = $displayName;
+ }
+ $children[] = $entry;
+ }
+ return $children;
+ }
/**
- * Returns the current logged-in user's storage stats.
+ * Returns the folder tree of the user
+ *
+ * @param string $path The path relative to the user folder
+ * @param int $depth The depth of the tree
*
- * @NoAdminRequired
+ * @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
+ *
+ * 200: Folder tree returned successfully
+ * 400: Invalid folder path
+ * 401: Unauthorized
+ * 404: Folder not found
+ */
+ #[NoAdminRequired]
+ #[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
+ public function getFolderTree(string $path = '/', int $depth = 1): JSONResponse {
+ $user = $this->userSession->getUser();
+ if (!($user instanceof IUser)) {
+ return new JSONResponse([
+ 'message' => $this->l10n->t('Failed to authorize'),
+ ], Http::STATUS_UNAUTHORIZED);
+ }
+ try {
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $userFolderPath = $userFolder->getPath();
+ $fullPath = implode('/', [$userFolderPath, trim($path, '/')]);
+ $node = $this->rootFolder->get($fullPath);
+ if (!($node instanceof Folder)) {
+ return new JSONResponse([
+ 'message' => $this->l10n->t('Invalid folder path'),
+ ], Http::STATUS_BAD_REQUEST);
+ }
+ $nodes = $node->getDirectoryListing();
+ $tree = $this->getChildren($nodes, $depth);
+ } catch (NotFoundException $e) {
+ return new JSONResponse([
+ 'message' => $this->l10n->t('Folder not found'),
+ ], Http::STATUS_NOT_FOUND);
+ } catch (Throwable $th) {
+ $this->logger->error($th->getMessage(), ['exception' => $th]);
+ $tree = [];
+ }
+ return new JSONResponse($tree);
+ }
+
+ /**
+ * Returns the current logged-in user's storage stats.
*
* @param ?string $dir the directory to get the storage stats from
* @return JSONResponse
*/
+ #[NoAdminRequired]
public function getStorageStats($dir = '/'): JSONResponse {
$storageInfo = \OC_Helper::getStorageInfo($dir ?: '/');
- return new JSONResponse(['message' => 'ok', 'data' => $storageInfo]);
+ $response = new JSONResponse(['message' => 'ok', 'data' => $storageInfo]);
+ $response->cacheFor(5 * 60);
+ return $response;
}
/**
- * Change the default sort mode
- *
- * @NoAdminRequired
+ * Set a user view config
*
- * @param string $mode
- * @param string $direction
- * @return Response
- * @throws \OCP\PreConditionNotMetException
+ * @param string $view
+ * @param string $key
+ * @param string|bool $value
+ * @return JSONResponse
*/
- public function updateFileSorting($mode, $direction) {
- $allowedMode = ['name', 'size', 'mtime'];
- $allowedDirection = ['asc', 'desc'];
- if (!in_array($mode, $allowedMode) || !in_array($direction, $allowedDirection)) {
- $response = new Response();
- $response->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY);
- return $response;
+ #[NoAdminRequired]
+ public function setViewConfig(string $view, string $key, $value): JSONResponse {
+ try {
+ $this->viewConfig->setConfig($view, $key, (string)$value);
+ } catch (\InvalidArgumentException $e) {
+ return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
- $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting', $mode);
- $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting_direction', $direction);
- return new Response();
+
+ return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfig($view)]);
}
+
/**
- * Toggle default files user config
+ * Get the user view config
*
- * @NoAdminRequired
+ * @return JSONResponse
+ */
+ #[NoAdminRequired]
+ public function getViewConfigs(): JSONResponse {
+ return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfigs()]);
+ }
+
+ /**
+ * Set a user config
*
* @param string $key
* @param string|bool $value
* @return JSONResponse
*/
+ #[NoAdminRequired]
public function setConfig(string $key, $value): JSONResponse {
try {
$this->userConfig->setConfig($key, (string)$value);
@@ -317,10 +388,9 @@ class ApiController extends Controller {
/**
* Get the user config
*
- * @NoAdminRequired
- *
* @return JSONResponse
*/
+ #[NoAdminRequired]
public function getConfigs(): JSONResponse {
return new JSONResponse(['message' => 'ok', 'data' => $this->userConfig->getConfigs()]);
}
@@ -328,12 +398,11 @@ class ApiController extends Controller {
/**
* Toggle default for showing/hiding hidden files
*
- * @NoAdminRequired
- *
* @param bool $value
* @return Response
- * @throws \OCP\PreConditionNotMetException
+ * @throws PreConditionNotMetException
*/
+ #[NoAdminRequired]
public function showHiddenFiles(bool $value): Response {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', $value ? '1' : '0');
return new Response();
@@ -342,12 +411,11 @@ class ApiController extends Controller {
/**
* Toggle default for cropping preview images
*
- * @NoAdminRequired
- *
* @param bool $value
* @return Response
- * @throws \OCP\PreConditionNotMetException
+ * @throws PreConditionNotMetException
*/
+ #[NoAdminRequired]
public function cropImagePreviews(bool $value): Response {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', $value ? '1' : '0');
return new Response();
@@ -356,12 +424,11 @@ class ApiController extends Controller {
/**
* Toggle default for files grid view
*
- * @NoAdminRequired
- *
* @param bool $show
* @return Response
- * @throws \OCP\PreConditionNotMetException
+ * @throws PreConditionNotMetException
*/
+ #[NoAdminRequired]
public function showGridView(bool $show): Response {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', $show ? '1' : '0');
return new Response();
@@ -369,51 +436,27 @@ class ApiController extends Controller {
/**
* Get default settings for the grid view
- *
- * @NoAdminRequired
*/
+ #[NoAdminRequired]
public function getGridView() {
$status = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', '0') === '1';
return new JSONResponse(['gridview' => $status]);
}
- /**
- * Toggle default for showing/hiding xxx folder
- *
- * @NoAdminRequired
- *
- * @param int $show
- * @param string $key the key of the folder
- *
- * @return Response
- * @throws \OCP\PreConditionNotMetException
- */
- public function toggleShowFolder(int $show, string $key): Response {
- if ($show !== 0 && $show !== 1) {
- return new DataResponse([
- 'message' => 'Invalid show value. Only 0 and 1 are allowed.'
- ], Http::STATUS_BAD_REQUEST);
- }
-
- $userId = $this->userSession->getUser()->getUID();
-
- // Set the new value and return it
- // Using a prefix prevents the user from setting arbitrary keys
- $this->config->setUserValue($userId, 'files', 'show_' . $key, (string)$show);
- return new JSONResponse([$key => $show]);
- }
-
- /**
- * Get sorting-order for custom sorting
- *
- * @NoAdminRequired
- *
- * @param string $folderpath
- * @return string
- * @throws \OCP\Files\NotFoundException
- */
- public function getNodeType($folderpath) {
- $node = $this->userFolder->get($folderpath);
- return $node->getType();
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
+ public function serviceWorker(): StreamResponse {
+ $response = new StreamResponse(__DIR__ . '/../../../../dist/preview-service-worker.js');
+ $response->setHeaders([
+ 'Content-Type' => 'application/javascript',
+ 'Service-Worker-Allowed' => '/'
+ ]);
+ $policy = new ContentSecurityPolicy();
+ $policy->addAllowedWorkerSrcDomain("'self'");
+ $policy->addAllowedScriptDomain("'self'");
+ $policy->addAllowedConnectDomain("'self'");
+ $response->setContentSecurityPolicy($policy);
+ return $response;
}
}
diff --git a/apps/files/lib/Controller/ConversionApiController.php b/apps/files/lib/Controller/ConversionApiController.php
new file mode 100644
index 00000000000..40a42d6ca4c
--- /dev/null
+++ b/apps/files/lib/Controller/ConversionApiController.php
@@ -0,0 +1,109 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Controller;
+
+use OC\Files\Utils\PathHelper;
+use OC\ForbiddenException;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\ApiRoute;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\UserRateLimit;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCS\OCSBadRequestException;
+use OCP\AppFramework\OCS\OCSException;
+use OCP\AppFramework\OCS\OCSForbiddenException;
+use OCP\AppFramework\OCS\OCSNotFoundException;
+use OCP\AppFramework\OCSController;
+use OCP\Files\Conversion\IConversionManager;
+use OCP\Files\File;
+use OCP\Files\GenericFileException;
+use OCP\Files\IRootFolder;
+use OCP\IL10N;
+use OCP\IRequest;
+use function OCP\Log\logger;
+
+class ConversionApiController extends OCSController {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IConversionManager $fileConversionManager,
+ private IRootFolder $rootFolder,
+ private IL10N $l10n,
+ private ?string $userId,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Converts a file from one MIME type to another
+ *
+ * @param int $fileId ID of the file to be converted
+ * @param string $targetMimeType The MIME type to which you want to convert the file
+ * @param string|null $destination The target path of the converted file. Written to a temporary file if left empty
+ *
+ * @return DataResponse<Http::STATUS_CREATED, array{path: string, fileId: int}, array{}>
+ *
+ * 201: File was converted and written to the destination or temporary file
+ *
+ * @throws OCSException The file was unable to be converted
+ * @throws OCSNotFoundException The file to be converted was not found
+ */
+ #[NoAdminRequired]
+ #[UserRateLimit(limit: 25, period: 120)]
+ #[ApiRoute(verb: 'POST', url: '/api/v1/convert')]
+ public function convert(int $fileId, string $targetMimeType, ?string $destination = null): DataResponse {
+ $userFolder = $this->rootFolder->getUserFolder($this->userId);
+ $file = $userFolder->getFirstNodeById($fileId);
+
+ // Also throw a 404 if the file is not readable to not leak information
+ if (!($file instanceof File) || $file->isReadable() === false) {
+ throw new OCSNotFoundException($this->l10n->t('The file cannot be found'));
+ }
+
+ if ($destination !== null) {
+ $destination = PathHelper::normalizePath($destination);
+ $parentDir = dirname($destination);
+
+ if (!$userFolder->nodeExists($parentDir)) {
+ throw new OCSNotFoundException($this->l10n->t('The destination path does not exist: %1$s', [$parentDir]));
+ }
+
+ if (!$userFolder->get($parentDir)->isCreatable()) {
+ throw new OCSForbiddenException($this->l10n->t('You do not have permission to create a file at the specified location'));
+ }
+
+ $destination = $userFolder->getFullPath($destination);
+ }
+
+ try {
+ $convertedFile = $this->fileConversionManager->convert($file, $targetMimeType, $destination);
+ } catch (ForbiddenException $e) {
+ throw new OCSForbiddenException($e->getMessage());
+ } catch (GenericFileException $e) {
+ throw new OCSBadRequestException($e->getMessage());
+ } catch (\Exception $e) {
+ logger('files')->error($e->getMessage(), ['exception' => $e]);
+ throw new OCSException($this->l10n->t('The file could not be converted.'));
+ }
+
+ $convertedFileRelativePath = $userFolder->getRelativePath($convertedFile);
+ if ($convertedFileRelativePath === null) {
+ throw new OCSNotFoundException($this->l10n->t('Could not get relative path to converted file'));
+ }
+
+ $file = $userFolder->get($convertedFileRelativePath);
+ $fileId = $file->getId();
+
+ return new DataResponse([
+ 'path' => $convertedFileRelativePath,
+ 'fileId' => $fileId,
+ ], Http::STATUS_CREATED);
+ }
+}
diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php
index c67150be8d4..c8addc33e98 100644
--- a/apps/files/lib/Controller/DirectEditingController.php
+++ b/apps/files/lib/Controller/DirectEditingController.php
@@ -1,70 +1,47 @@
<?php
+
/**
- * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Controller;
use Exception;
use OCA\Files\Service\DirectEditingService;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\DirectEditing\IManager;
use OCP\DirectEditing\RegisterDirectEditorEvent;
use OCP\EventDispatcher\IEventDispatcher;
-use OCP\ILogger;
use OCP\IRequest;
use OCP\IURLGenerator;
+use Psr\Log\LoggerInterface;
class DirectEditingController extends OCSController {
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
-
- /** @var IManager */
- private $directEditingManager;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
- /** @var ILogger */
- private $logger;
-
- /** @var DirectEditingService */
- private $directEditingService;
-
- public function __construct($appName, IRequest $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge,
- IEventDispatcher $eventDispatcher, IURLGenerator $urlGenerator, IManager $manager, DirectEditingService $directEditingService, ILogger $logger) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ string $corsMethods,
+ string $corsAllowedHeaders,
+ int $corsMaxAge,
+ private IEventDispatcher $eventDispatcher,
+ private IURLGenerator $urlGenerator,
+ private IManager $directEditingManager,
+ private DirectEditingService $directEditingService,
+ private LoggerInterface $logger,
+ ) {
parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge);
-
- $this->eventDispatcher = $eventDispatcher;
- $this->directEditingManager = $manager;
- $this->directEditingService = $directEditingService;
- $this->logger = $logger;
- $this->urlGenerator = $urlGenerator;
}
/**
- * @NoAdminRequired
+ * Get the direct editing capabilities
+ * @return DataResponse<Http::STATUS_OK, array{editors: array<string, array{id: string, name: string, mimetypes: list<string>, optionalMimetypes: list<string>, secure: bool}>, creators: array<string, array{id: string, editor: string, name: string, extension: string, templates: bool, mimetypes: list<string>}>}, array{}>
+ *
+ * 200: Direct editing capabilities returned
*/
+ #[NoAdminRequired]
public function info(): DataResponse {
$response = new DataResponse($this->directEditingService->getDirectEditingCapabilitites());
$response->setETag($this->directEditingService->getDirectEditingETag());
@@ -72,9 +49,20 @@ class DirectEditingController extends OCSController {
}
/**
- * @NoAdminRequired
+ * Create a file for direct editing
+ *
+ * @param string $path Path of the file
+ * @param string $editorId ID of the editor
+ * @param string $creatorId ID of the creator
+ * @param ?string $templateId ID of the template
+ *
+ * @return DataResponse<Http::STATUS_OK, array{url: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
+ *
+ * 200: URL for direct editing returned
+ * 403: Opening file is not allowed
*/
- public function create(string $path, string $editorId, string $creatorId, string $templateId = null): DataResponse {
+ #[NoAdminRequired]
+ public function create(string $path, string $editorId, string $creatorId, ?string $templateId = null): DataResponse {
if (!$this->directEditingManager->isEnabled()) {
return new DataResponse(['message' => 'Direct editing is not enabled'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
@@ -86,27 +74,47 @@ class DirectEditingController extends OCSController {
'url' => $this->urlGenerator->linkToRouteAbsolute('files.DirectEditingView.edit', ['token' => $token])
]);
} catch (Exception $e) {
- $this->logger->logException($e, ['message' => 'Exception when creating a new file through direct editing']);
+ $this->logger->error(
+ 'Exception when creating a new file through direct editing',
+ [
+ 'exception' => $e
+ ],
+ );
return new DataResponse(['message' => 'Failed to create file: ' . $e->getMessage()], Http::STATUS_FORBIDDEN);
}
}
/**
- * @NoAdminRequired
+ * Open a file for direct editing
+ *
+ * @param string $path Path of the file
+ * @param ?string $editorId ID of the editor
+ * @param ?int $fileId ID of the file
+ *
+ * @return DataResponse<Http::STATUS_OK, array{url: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
+ *
+ * 200: URL for direct editing returned
+ * 403: Opening file is not allowed
*/
- public function open(string $path, string $editorId = null): DataResponse {
+ #[NoAdminRequired]
+ public function open(string $path, ?string $editorId = null, ?int $fileId = null): DataResponse {
if (!$this->directEditingManager->isEnabled()) {
return new DataResponse(['message' => 'Direct editing is not enabled'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
$this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager));
try {
- $token = $this->directEditingManager->open($path, $editorId);
+ $token = $this->directEditingManager->open($path, $editorId, $fileId);
return new DataResponse([
'url' => $this->urlGenerator->linkToRouteAbsolute('files.DirectEditingView.edit', ['token' => $token])
]);
} catch (Exception $e) {
- $this->logger->logException($e, ['message' => 'Exception when opening a file through direct editing']);
+ $this->logger->error(
+ 'Exception when opening a file through direct editing',
+ [
+ 'exception' => $e
+ ],
+ );
return new DataResponse(['message' => 'Failed to open file: ' . $e->getMessage()], Http::STATUS_FORBIDDEN);
}
}
@@ -114,8 +122,16 @@ class DirectEditingController extends OCSController {
/**
- * @NoAdminRequired
+ * Get the templates for direct editing
+ *
+ * @param string $editorId ID of the editor
+ * @param string $creatorId ID of the creator
+ *
+ * @return DataResponse<Http::STATUS_OK, array{templates: array<string, array{id: string, title: string, preview: ?string, extension: string, mimetype: string}>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
+ *
+ * 200: Templates returned
*/
+ #[NoAdminRequired]
public function templates(string $editorId, string $creatorId): DataResponse {
if (!$this->directEditingManager->isEnabled()) {
return new DataResponse(['message' => 'Direct editing is not enabled'], Http::STATUS_INTERNAL_SERVER_ERROR);
@@ -125,7 +141,12 @@ class DirectEditingController extends OCSController {
try {
return new DataResponse($this->directEditingManager->getTemplates($editorId, $creatorId));
} catch (Exception $e) {
- $this->logger->logException($e);
+ $this->logger->error(
+ $e->getMessage(),
+ [
+ 'exception' => $e
+ ],
+ );
return new DataResponse(['message' => 'Failed to obtain template list: ' . $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
diff --git a/apps/files/lib/Controller/DirectEditingViewController.php b/apps/files/lib/Controller/DirectEditingViewController.php
index 06bde8d63d7..b13e68f7766 100644
--- a/apps/files/lib/Controller/DirectEditingViewController.php
+++ b/apps/files/lib/Controller/DirectEditingViewController.php
@@ -1,69 +1,50 @@
<?php
+
/**
- * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Controller;
use Exception;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\DirectEditing\IManager;
use OCP\DirectEditing\RegisterDirectEditorEvent;
use OCP\EventDispatcher\IEventDispatcher;
-use OCP\ILogger;
use OCP\IRequest;
+use Psr\Log\LoggerInterface;
+#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class DirectEditingViewController extends Controller {
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
-
- /** @var IManager */
- private $directEditingManager;
-
- /** @var ILogger */
- private $logger;
-
- public function __construct($appName, IRequest $request, IEventDispatcher $eventDispatcher, IManager $manager, ILogger $logger) {
+ public function __construct(
+ $appName,
+ IRequest $request,
+ private IEventDispatcher $eventDispatcher,
+ private IManager $directEditingManager,
+ private LoggerInterface $logger,
+ ) {
parent::__construct($appName, $request);
-
- $this->eventDispatcher = $eventDispatcher;
- $this->directEditingManager = $manager;
- $this->logger = $logger;
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
* @return Response
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[UseSession]
public function edit(string $token): Response {
$this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager));
try {
return $this->directEditingManager->edit($token);
} catch (Exception $e) {
- $this->logger->logException($e);
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
return new NotFoundResponse();
}
}
diff --git a/apps/files/lib/Controller/OpenLocalEditorController.php b/apps/files/lib/Controller/OpenLocalEditorController.php
index 7d784196361..b000304eef6 100644
--- a/apps/files/lib/Controller/OpenLocalEditorController.php
+++ b/apps/files/lib/Controller/OpenLocalEditorController.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Controller;
@@ -30,6 +13,9 @@ use OCA\Files\Db\OpenLocalEditor;
use OCA\Files\Db\OpenLocalEditorMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -43,34 +29,29 @@ class OpenLocalEditorController extends OCSController {
public const TOKEN_DURATION = 600; // 10 Minutes
public const TOKEN_RETRIES = 50;
- protected ITimeFactory $timeFactory;
- protected OpenLocalEditorMapper $mapper;
- protected ISecureRandom $secureRandom;
- protected LoggerInterface $logger;
- protected ?string $userId;
-
public function __construct(
string $appName,
IRequest $request,
- ITimeFactory $timeFactory,
- OpenLocalEditorMapper $mapper,
- ISecureRandom $secureRandom,
- LoggerInterface $logger,
- ?string $userId
+ protected ITimeFactory $timeFactory,
+ protected OpenLocalEditorMapper $mapper,
+ protected ISecureRandom $secureRandom,
+ protected LoggerInterface $logger,
+ protected ?string $userId,
) {
parent::__construct($appName, $request);
-
- $this->timeFactory = $timeFactory;
- $this->mapper = $mapper;
- $this->secureRandom = $secureRandom;
- $this->logger = $logger;
- $this->userId = $userId;
}
/**
- * @NoAdminRequired
- * @UserRateThrottle(limit=10, period=120)
+ * Create a local editor
+ *
+ * @param string $path Path of the file
+ *
+ * @return DataResponse<Http::STATUS_OK, array{userId: ?string, pathHash: string, expirationTime: int, token: string}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, list<empty>, array{}>
+ *
+ * 200: Local editor returned
*/
+ #[NoAdminRequired]
+ #[UserRateLimit(limit: 10, period: 120)]
public function create(string $path): DataResponse {
$pathHash = sha1($path);
@@ -105,9 +86,18 @@ class OpenLocalEditorController extends OCSController {
}
/**
- * @NoAdminRequired
- * @BruteForceProtection(action=openLocalEditor)
+ * Validate a local editor
+ *
+ * @param string $path Path of the file
+ * @param string $token Token of the local editor
+ *
+ * @return DataResponse<Http::STATUS_OK, array{userId: string, pathHash: string, expirationTime: int, token: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>
+ *
+ * 200: Local editor validated successfully
+ * 404: Local editor not found
*/
+ #[NoAdminRequired]
+ #[BruteForceProtection(action: 'openLocalEditor')]
public function validate(string $path, string $token): DataResponse {
$pathHash = sha1($path);
diff --git a/apps/files/lib/Controller/TemplateController.php b/apps/files/lib/Controller/TemplateController.php
index d04d86760e6..ee4c86941c7 100644
--- a/apps/files/lib/Controller/TemplateController.php
+++ b/apps/files/lib/Controller/TemplateController.php
@@ -3,72 +3,123 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Controller;
+use OCA\Files\ResponseDefinitions;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCSController;
use OCP\Files\GenericFileException;
use OCP\Files\Template\ITemplateManager;
+use OCP\Files\Template\Template;
+use OCP\Files\Template\TemplateFileCreator;
use OCP\IRequest;
+/**
+ * @psalm-import-type FilesTemplateFile from ResponseDefinitions
+ * @psalm-import-type FilesTemplateFileCreator from ResponseDefinitions
+ * @psalm-import-type FilesTemplateFileCreatorWithTemplates from ResponseDefinitions
+ * @psalm-import-type FilesTemplateField from ResponseDefinitions
+ * @psalm-import-type FilesTemplate from ResponseDefinitions
+ */
class TemplateController extends OCSController {
- protected $templateManager;
-
- public function __construct($appName, IRequest $request, ITemplateManager $templateManager) {
+ public function __construct(
+ $appName,
+ IRequest $request,
+ protected ITemplateManager $templateManager,
+ ) {
parent::__construct($appName, $request);
- $this->templateManager = $templateManager;
}
/**
- * @NoAdminRequired
+ * List the available templates
+ *
+ * @return DataResponse<Http::STATUS_OK, list<FilesTemplateFileCreatorWithTemplates>, array{}>
+ *
+ * 200: Available templates returned
*/
+ #[NoAdminRequired]
public function list(): DataResponse {
- return new DataResponse($this->templateManager->listTemplates());
+ /* Convert embedded Template instances to arrays to match return type */
+ return new DataResponse(array_map(static function (array $templateFileCreator) {
+ $templateFileCreator['templates'] = array_map(static fn (Template $template) => $template->jsonSerialize(), $templateFileCreator['templates']);
+ return $templateFileCreator;
+ }, $this->templateManager->listTemplates()));
+ }
+
+ /**
+ * List the fields for the template specified by the given file ID
+ *
+ * @param int $fileId File ID of the template
+ * @return DataResponse<Http::STATUS_OK, array<string, FilesTemplateField>, array{}>
+ *
+ * 200: Fields returned
+ */
+ #[NoAdminRequired]
+ public function listTemplateFields(int $fileId): DataResponse {
+ $fields = $this->templateManager->listTemplateFields($fileId);
+
+ return new DataResponse(
+ array_merge([], ...$fields),
+ Http::STATUS_OK
+ );
}
/**
- * @NoAdminRequired
- * @throws OCSForbiddenException
+ * Create a template
+ *
+ * @param string $filePath Path of the file
+ * @param string $templatePath Name of the template
+ * @param string $templateType Type of the template
+ * @param list<FilesTemplateField> $templateFields Fields of the template
+ *
+ * @return DataResponse<Http::STATUS_OK, FilesTemplateFile, array{}>
+ * @throws OCSForbiddenException Creating template is not allowed
+ *
+ * 200: Template created successfully
*/
- public function create(string $filePath, string $templatePath = '', string $templateType = 'user'): DataResponse {
+ #[NoAdminRequired]
+ public function create(
+ string $filePath,
+ string $templatePath = '',
+ string $templateType = 'user',
+ array $templateFields = [],
+ ): DataResponse {
try {
- return new DataResponse($this->templateManager->createFromTemplate($filePath, $templatePath, $templateType));
+ return new DataResponse($this->templateManager->createFromTemplate(
+ $filePath,
+ $templatePath,
+ $templateType,
+ $templateFields));
} catch (GenericFileException $e) {
throw new OCSForbiddenException($e->getMessage());
}
}
/**
- * @NoAdminRequired
+ * Initialize the template directory
+ *
+ * @param string $templatePath Path of the template directory
+ * @param bool $copySystemTemplates Whether to copy the system templates to the template directory
+ *
+ * @return DataResponse<Http::STATUS_OK, array{template_path: string, templates: list<FilesTemplateFileCreator>}, array{}>
+ * @throws OCSForbiddenException Initializing the template directory is not allowed
+ *
+ * 200: Template directory initialized successfully
*/
+ #[NoAdminRequired]
public function path(string $templatePath = '', bool $copySystemTemplates = false) {
try {
+ /** @var string $templatePath */
$templatePath = $this->templateManager->initializeTemplateDirectory($templatePath, null, $copySystemTemplates);
return new DataResponse([
'template_path' => $templatePath,
- 'templates' => $this->templateManager->listCreators()
+ 'templates' => array_values(array_map(fn (TemplateFileCreator $creator) => $creator->jsonSerialize(), $this->templateManager->listCreators())),
]);
} catch (\Exception $e) {
throw new OCSForbiddenException($e->getMessage());
diff --git a/apps/files/lib/Controller/TransferOwnershipController.php b/apps/files/lib/Controller/TransferOwnershipController.php
index 5abd65444bf..51a25400efb 100644
--- a/apps/files/lib/Controller/TransferOwnershipController.php
+++ b/apps/files/lib/Controller/TransferOwnershipController.php
@@ -3,40 +3,22 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Controller;
use OCA\Files\BackgroundJob\TransferOwnership;
use OCA\Files\Db\TransferOwnership as TransferOwnershipEntity;
use OCA\Files\Db\TransferOwnershipMapper;
-use OCP\Files\IHomeStorage;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
+use OCP\Files\IHomeStorage;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IUserManager;
@@ -44,45 +26,34 @@ use OCP\Notification\IManager as NotificationManager;
class TransferOwnershipController extends OCSController {
- /** @var string */
- private $userId;
- /** @var NotificationManager */
- private $notificationManager;
- /** @var ITimeFactory */
- private $timeFactory;
- /** @var IJobList */
- private $jobList;
- /** @var TransferOwnershipMapper */
- private $mapper;
- /** @var IUserManager */
- private $userManager;
- /** @var IRootFolder */
- private $rootFolder;
-
- public function __construct(string $appName,
- IRequest $request,
- string $userId,
- NotificationManager $notificationManager,
- ITimeFactory $timeFactory,
- IJobList $jobList,
- TransferOwnershipMapper $mapper,
- IUserManager $userManager,
- IRootFolder $rootFolder) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private string $userId,
+ private NotificationManager $notificationManager,
+ private ITimeFactory $timeFactory,
+ private IJobList $jobList,
+ private TransferOwnershipMapper $mapper,
+ private IUserManager $userManager,
+ private IRootFolder $rootFolder,
+ ) {
parent::__construct($appName, $request);
-
- $this->userId = $userId;
- $this->notificationManager = $notificationManager;
- $this->timeFactory = $timeFactory;
- $this->jobList = $jobList;
- $this->mapper = $mapper;
- $this->userManager = $userManager;
- $this->rootFolder = $rootFolder;
}
/**
- * @NoAdminRequired
+ * Transfer the ownership to another user
+ *
+ * @param string $recipient Username of the recipient
+ * @param string $path Path of the file
+ *
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN, list<empty>, array{}>
+ *
+ * 200: Ownership transferred successfully
+ * 400: Transferring ownership is not possible
+ * 403: Transferring ownership is not allowed
*/
+ #[NoAdminRequired]
public function transfer(string $recipient, string $path): DataResponse {
$recipientUser = $this->userManager->get($recipient);
@@ -126,8 +97,17 @@ class TransferOwnershipController extends OCSController {
}
/**
- * @NoAdminRequired
+ * Accept an ownership transfer
+ *
+ * @param int $id ID of the ownership transfer
+ *
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
+ *
+ * 200: Ownership transfer accepted successfully
+ * 403: Accepting ownership transfer is not allowed
+ * 404: Ownership transfer not found
*/
+ #[NoAdminRequired]
public function accept(int $id): DataResponse {
try {
$transferOwnership = $this->mapper->getById($id);
@@ -139,28 +119,30 @@ class TransferOwnershipController extends OCSController {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
+ $this->jobList->add(TransferOwnership::class, [
+ 'id' => $transferOwnership->getId(),
+ ]);
+
$notification = $this->notificationManager->createNotification();
$notification->setApp('files')
->setObject('transfer', (string)$id);
$this->notificationManager->markProcessed($notification);
- $newTransferOwnership = new TransferOwnershipEntity();
- $newTransferOwnership->setNodeName($transferOwnership->getNodeName());
- $newTransferOwnership->setFileId($transferOwnership->getFileId());
- $newTransferOwnership->setSourceUser($transferOwnership->getSourceUser());
- $newTransferOwnership->setTargetUser($transferOwnership->getTargetUser());
- $this->mapper->insert($newTransferOwnership);
-
- $this->jobList->add(TransferOwnership::class, [
- 'id' => $newTransferOwnership->getId(),
- ]);
-
return new DataResponse([], Http::STATUS_OK);
}
/**
- * @NoAdminRequired
+ * Reject an ownership transfer
+ *
+ * @param int $id ID of the ownership transfer
+ *
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
+ *
+ * 200: Ownership transfer rejected successfully
+ * 403: Rejecting ownership transfer is not allowed
+ * 404: Ownership transfer not found
*/
+ #[NoAdminRequired]
public function reject(int $id): DataResponse {
try {
$transferOwnership = $this->mapper->getById($id);
@@ -177,20 +159,10 @@ class TransferOwnershipController extends OCSController {
->setObject('transfer', (string)$id);
$this->notificationManager->markProcessed($notification);
- $notification = $this->notificationManager->createNotification();
- $notification->setUser($transferOwnership->getSourceUser())
- ->setApp($this->appName)
- ->setDateTime($this->timeFactory->getDateTime())
- ->setSubject('transferownershipRequestDenied', [
- 'sourceUser' => $transferOwnership->getSourceUser(),
- 'targetUser' => $transferOwnership->getTargetUser(),
- 'nodeName' => $transferOwnership->getNodeName()
- ])
- ->setObject('transfer', (string)$transferOwnership->getId());
- $this->notificationManager->notify($notification);
-
$this->mapper->delete($transferOwnership);
+ // A "request denied" notification will be created by Notifier::dismissNotification
+
return new DataResponse([], Http::STATUS_OK);
}
}
diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php
index b607764e602..ecf21cef313 100644
--- a/apps/files/lib/Controller/ViewController.php
+++ b/apps/files/lib/Controller/ViewController.php
@@ -1,53 +1,32 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author fnuesse <felix.nuesse@t-online.de>
- * @author fnuesse <fnuesse@techfak.uni-bielefeld.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Max Kovalenko <mxss1998@yandex.ru>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Nina Pypchenko <22447785+nina-py@users.noreply.github.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\Controller;
-use OCA\Files\Activity\Helper;
+use OC\Files\FilenameValidator;
+use OC\Files\Filesystem;
use OCA\Files\AppInfo\Application;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\Files\Event\LoadSearchPlugins;
use OCA\Files\Event\LoadSidebar;
use OCA\Files\Service\UserConfig;
+use OCA\Files\Service\ViewConfig;
use OCA\Viewer\Event\LoadViewer;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
+use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
@@ -59,359 +38,269 @@ use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
-use OCP\Share\IManager;
+use OCP\Util;
/**
- * Class ViewController
- *
* @package OCA\Files\Controller
*/
+#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class ViewController extends Controller {
- private IURLGenerator $urlGenerator;
- private IL10N $l10n;
- private IConfig $config;
- private IEventDispatcher $eventDispatcher;
- private IUserSession $userSession;
- private IAppManager $appManager;
- private IRootFolder $rootFolder;
- private Helper $activityHelper;
- private IInitialState $initialState;
- private ITemplateManager $templateManager;
- private IManager $shareManager;
- private UserConfig $userConfig;
-
- public function __construct(string $appName,
+
+ public function __construct(
+ string $appName,
IRequest $request,
- IURLGenerator $urlGenerator,
- IL10N $l10n,
- IConfig $config,
- IEventDispatcher $eventDispatcher,
- IUserSession $userSession,
- IAppManager $appManager,
- IRootFolder $rootFolder,
- Helper $activityHelper,
- IInitialState $initialState,
- ITemplateManager $templateManager,
- IManager $shareManager,
- UserConfig $userConfig
+ private IURLGenerator $urlGenerator,
+ private IL10N $l10n,
+ private IConfig $config,
+ private IEventDispatcher $eventDispatcher,
+ private IUserSession $userSession,
+ private IAppManager $appManager,
+ private IRootFolder $rootFolder,
+ private IInitialState $initialState,
+ private ITemplateManager $templateManager,
+ private UserConfig $userConfig,
+ private ViewConfig $viewConfig,
+ private FilenameValidator $filenameValidator,
+ private IRegistry $twoFactorRegistry,
) {
parent::__construct($appName, $request);
- $this->urlGenerator = $urlGenerator;
- $this->l10n = $l10n;
- $this->config = $config;
- $this->eventDispatcher = $eventDispatcher;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
- $this->rootFolder = $rootFolder;
- $this->activityHelper = $activityHelper;
- $this->initialState = $initialState;
- $this->templateManager = $templateManager;
- $this->shareManager = $shareManager;
- $this->userConfig = $userConfig;
- }
-
- /**
- * @param string $appName
- * @param string $scriptName
- * @return string
- */
- protected function renderScript($appName, $scriptName) {
- $content = '';
- $appPath = \OC_App::getAppPath($appName);
- $scriptPath = $appPath . '/' . $scriptName;
- if (file_exists($scriptPath)) {
- // TODO: sanitize path / script name ?
- ob_start();
- include $scriptPath;
- $content = ob_get_contents();
- @ob_end_clean();
- }
-
- return $content;
}
/**
* FIXME: Replace with non static code
*
* @return array
- * @throws \OCP\Files\NotFoundException
+ * @throws NotFoundException
*/
protected function getStorageInfo(string $dir = '/') {
- \OC_Util::setupFS();
- $rootInfo = \OC\Files\Filesystem::getFileInfo('/', false);
+ $rootInfo = Filesystem::getFileInfo('/', false);
return \OC_Helper::getStorageInfo($dir, $rootInfo ?: null);
}
/**
- * @NoCSRFRequired
- * @NoAdminRequired
- *
* @param string $fileid
* @return TemplateResponse|RedirectResponse
- * @throws NotFoundException
*/
- public function showFile(string $fileid = null, int $openfile = 1): Response {
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function showFile(?string $fileid = null, ?string $opendetails = null, ?string $openfile = null): Response {
+ if (!$fileid) {
+ return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index'));
+ }
+
// This is the entry point from the `/f/{fileid}` URL which is hardcoded in the server.
try {
- return $this->redirectToFile($fileid, $openfile !== 0);
+ return $this->redirectToFile((int)$fileid, $opendetails, $openfile);
} catch (NotFoundException $e) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true]));
+ // Keep the fileid even if not found, it will be used
+ // to detect the file could not be found and warn the user
+ return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', ['fileid' => $fileid, 'view' => 'files']));
}
}
+
+ /**
+ * @param string $dir
+ * @param string $view
+ * @param string $fileid
+ * @return TemplateResponse|RedirectResponse
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function indexView($dir = '', $view = '', $fileid = null) {
+ return $this->index($dir, $view, $fileid);
+ }
+
/**
- * @NoCSRFRequired
- * @NoAdminRequired
- * @UseSession
- *
* @param string $dir
* @param string $view
* @param string $fileid
- * @param bool $fileNotFound
- * @param string $openfile - the openfile URL parameter if it was present in the initial request
* @return TemplateResponse|RedirectResponse
- * @throws NotFoundException
*/
- public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false, $openfile = null) {
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function indexViewFileid($dir = '', $view = '', $fileid = null) {
+ return $this->index($dir, $view, $fileid);
+ }
- if ($fileid !== null && $dir === '') {
+ /**
+ * @param string $dir
+ * @param string $view
+ * @param string $fileid
+ * @return TemplateResponse|RedirectResponse
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function index($dir = '', $view = '', $fileid = null) {
+ if ($fileid !== null && $view !== 'trashbin') {
try {
- return $this->redirectToFile($fileid);
+ return $this->redirectToFileIfInTrashbin((int)$fileid);
} catch (NotFoundException $e) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true]));
}
}
- $nav = new \OCP\Template('files', 'appnavigation', '');
-
// Load the files we need
- \OCP\Util::addStyle('files', 'merged');
- \OCP\Util::addScript('files', 'merged-index', 'files');
- \OCP\Util::addScript('files', 'main');
+ Util::addInitScript('files', 'init');
+ Util::addScript('files', 'main');
- // mostly for the home storage's free space
- // FIXME: Make non static
- $storageInfo = $this->getStorageInfo();
-
- $userId = $this->userSession->getUser()->getUID();
-
- // Get all the user favorites to create a submenu
- try {
- $favElements = $this->activityHelper->getFavoriteFilePaths($userId);
- } catch (\RuntimeException $e) {
- $favElements['folders'] = [];
- }
-
- $collapseClasses = '';
- if (count($favElements['folders']) > 0) {
- $collapseClasses = 'collapsible';
- }
-
- $favoritesSublistArray = [];
-
- $navBarPositionPosition = 6;
- foreach ($favElements['folders'] as $favElement) {
- $element = [
- 'id' => str_replace('/', '-', $favElement),
- 'dir' => $favElement,
- 'order' => $navBarPositionPosition,
- 'name' => basename($favElement),
- 'icon' => 'folder',
- 'params' => [
- 'view' => 'files',
- 'dir' => $favElement,
- ],
- ];
-
- array_push($favoritesSublistArray, $element);
- $navBarPositionPosition++;
- }
-
- $navItems = \OCA\Files\App::getNavigationManager()->getAll();
-
- // add the favorites entry in menu
- $navItems['favorites']['sublist'] = $favoritesSublistArray;
- $navItems['favorites']['classes'] = $collapseClasses;
-
- // parse every menu and add the expanded user value
- foreach ($navItems as $key => $item) {
- $navItems[$key]['expanded'] = $this->config->getUserValue($userId, 'files', 'show_' . $item['id'], '0') === '1';
+ $user = $this->userSession->getUser();
+ $userId = $user->getUID();
+
+ // If the file doesn't exists in the folder and
+ // exists in only one occurrence, redirect to that file
+ // in the correct folder
+ if ($fileid && $dir !== '') {
+ $baseFolder = $this->rootFolder->getUserFolder($userId);
+ $nodes = $baseFolder->getById((int)$fileid);
+ if (!empty($nodes)) {
+ $nodePath = $baseFolder->getRelativePath($nodes[0]->getPath());
+ $relativePath = $nodePath ? dirname($nodePath) : '';
+ // If the requested path does not contain the file id
+ // or if the requested path is not the file id itself
+ if (count($nodes) === 1 && $relativePath !== $dir && $nodePath !== $dir) {
+ return $this->redirectToFile((int)$fileid);
+ }
+ }
}
- $nav->assign('navigationItems', $navItems);
-
- $contentItems = [];
-
try {
// If view is files, we use the directory, otherwise we use the root storage
- $storageInfo = $this->getStorageInfo(($view === 'files' && $dir) ? $dir : '/');
- } catch(\Exception $e) {
+ $storageInfo = $this->getStorageInfo(($view === 'files' && $dir) ? $dir : '/');
+ } catch (\Exception $e) {
$storageInfo = $this->getStorageInfo();
}
$this->initialState->provideInitialState('storageStats', $storageInfo);
- $this->initialState->provideInitialState('navigation', $navItems);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
+ $this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
- // render the container content for every navigation item
- foreach ($navItems as $item) {
- $content = '';
- if (isset($item['script'])) {
- $content = $this->renderScript($item['appname'], $item['script']);
- }
- // parse submenus
- if (isset($item['sublist'])) {
- foreach ($item['sublist'] as $subitem) {
- $subcontent = '';
- if (isset($subitem['script'])) {
- $subcontent = $this->renderScript($subitem['appname'], $subitem['script']);
- }
- $contentItems[$subitem['id']] = [
- 'id' => $subitem['id'],
- 'content' => $subcontent
- ];
- }
- }
- $contentItems[$item['id']] = [
- 'id' => $item['id'],
- 'content' => $content
- ];
- }
+ // File sorting user config
+ $filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
+ $this->initialState->provideInitialState('filesSortingConfig', $filesSortingConfig);
+
+ // Forbidden file characters (deprecated use capabilities)
+ // TODO: Remove with next release of `@nextcloud/files`
+ $forbiddenCharacters = $this->filenameValidator->getForbiddenCharacters();
+ $this->initialState->provideInitialState('forbiddenCharacters', $forbiddenCharacters);
- $this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
$event = new LoadAdditionalScriptsEvent();
$this->eventDispatcher->dispatchTyped($event);
+ $this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
+ $this->eventDispatcher->dispatchTyped(new LoadSearchPlugins());
// Load Viewer scripts
if (class_exists(LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new LoadViewer());
}
+ $this->initialState->provideInitialState('templates_enabled', ($this->config->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton') !== '') || ($this->config->getSystemValueString('templatedirectory', \OC::$SERVERROOT . '/core/skeleton/Templates') !== ''));
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
- $params = [];
- $params['usedSpacePercent'] = (int) $storageInfo['relative'];
- $params['owner'] = $storageInfo['owner'] ?? '';
- $params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? '';
- $params['isPublic'] = false;
- $params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no';
- $params['defaultFileSorting'] = $this->config->getUserValue($userId, 'files', 'file_sorting', 'name');
- $params['defaultFileSortingDirection'] = $this->config->getUserValue($userId, 'files', 'file_sorting_direction', 'asc');
- $params['showgridview'] = $this->config->getUserValue($userId, 'files', 'show_grid', false);
- $showHidden = (bool) $this->config->getUserValue($userId, 'files', 'show_hidden', false);
- $params['showHiddenFiles'] = $showHidden ? 1 : 0;
- $cropImagePreviews = (bool) $this->config->getUserValue($userId, 'files', 'crop_image_previews', true);
- $params['cropImagePreviews'] = $cropImagePreviews ? 1 : 0;
- $params['fileNotFound'] = $fileNotFound ? 1 : 0;
- $params['appNavigation'] = $nav;
- $params['appContents'] = $contentItems;
- $params['hiddenFields'] = $event->getHiddenFields();
+ $isTwoFactorEnabled = false;
+ foreach ($this->twoFactorRegistry->getProviderStates($user) as $providerId => $providerState) {
+ if ($providerId !== 'backup_codes' && $providerState === true) {
+ $isTwoFactorEnabled = true;
+ }
+ }
+
+ $this->initialState->provideInitialState('isTwoFactorEnabled', $isTwoFactorEnabled);
$response = new TemplateResponse(
Application::APP_ID,
'index',
- $params
);
$policy = new ContentSecurityPolicy();
$policy->addAllowedFrameDomain('\'self\'');
+ // Allow preview service worker
+ $policy->addAllowedWorkerSrcDomain('\'self\'');
$response->setContentSecurityPolicy($policy);
- $this->provideInitialState($dir, $openfile);
-
return $response;
}
/**
- * Add openFileInfo in initialState if $openfile is set.
- * @param string $dir - the ?dir= URL param
- * @param string $openfile - the ?openfile= URL param
- * @return void
+ * Redirects to the trashbin file list and highlight the given file id
+ *
+ * @param int $fileId file id to show
+ * @return RedirectResponse redirect response or not found response
+ * @throws NotFoundException
*/
- private function provideInitialState(string $dir, ?string $openfile): void {
- if ($openfile === null) {
- return;
- }
-
- $user = $this->userSession->getUser();
-
- if ($user === null) {
- return;
- }
-
- $uid = $user->getUID();
- $userFolder = $this->rootFolder->getUserFolder($uid);
- $nodes = $userFolder->getById((int) $openfile);
- $node = array_shift($nodes);
-
- if ($node === null) {
- return;
- }
+ private function redirectToFileIfInTrashbin($fileId): RedirectResponse {
+ $uid = $this->userSession->getUser()->getUID();
+ $baseFolder = $this->rootFolder->getUserFolder($uid);
+ $node = $baseFolder->getFirstNodeById($fileId);
+ $params = [];
- // properly format full path and make sure
- // we're relative to the user home folder
- $isRoot = $node === $userFolder;
- $path = $userFolder->getRelativePath($node->getPath());
- $directory = $userFolder->getRelativePath($node->getParent()->getPath());
+ if (!$node && $this->appManager->isEnabledForUser('files_trashbin')) {
+ /** @var Folder */
+ $baseFolder = $this->rootFolder->get($uid . '/files_trashbin/files/');
+ $node = $baseFolder->getFirstNodeById($fileId);
+ $params['view'] = 'trashbin';
- // Prevent opening a file from another folder.
- if ($dir !== $directory) {
- return;
+ if ($node) {
+ $params['fileid'] = $fileId;
+ if ($node instanceof Folder) {
+ // set the full path to enter the folder
+ $params['dir'] = $baseFolder->getRelativePath($node->getPath());
+ } else {
+ // set parent path as dir
+ $params['dir'] = $baseFolder->getRelativePath($node->getParent()->getPath());
+ }
+ return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', $params));
+ }
}
-
- $this->initialState->provideInitialState(
- 'openFileInfo', [
- 'id' => $node->getId(),
- 'name' => $isRoot ? '' : $node->getName(),
- 'path' => $path,
- 'directory' => $directory,
- 'mime' => $node->getMimetype(),
- 'type' => $node->getType(),
- 'permissions' => $node->getPermissions(),
- ]
- );
+ throw new NotFoundException();
}
/**
* Redirects to the file list and highlight the given file id
*
- * @param string $fileId file id to show
- * @param bool $setOpenfile - whether or not to set the openfile URL parameter
+ * @param int $fileId file id to show
+ * @param string|null $openDetails open details parameter
+ * @param string|null $openFile open file parameter
* @return RedirectResponse redirect response or not found response
- * @throws \OCP\Files\NotFoundException
+ * @throws NotFoundException
*/
- private function redirectToFile($fileId, bool $setOpenfile = false) {
+ private function redirectToFile(int $fileId, ?string $openDetails = null, ?string $openFile = null): RedirectResponse {
$uid = $this->userSession->getUser()->getUID();
$baseFolder = $this->rootFolder->getUserFolder($uid);
- $files = $baseFolder->getById($fileId);
- $params = [];
+ $node = $baseFolder->getFirstNodeById($fileId);
+ $params = ['view' => 'files'];
- if (empty($files) && $this->appManager->isEnabledForUser('files_trashbin')) {
- $baseFolder = $this->rootFolder->get($uid . '/files_trashbin/files/');
- $files = $baseFolder->getById($fileId);
- $params['view'] = 'trashbin';
+ try {
+ $this->redirectToFileIfInTrashbin($fileId);
+ } catch (NotFoundException $e) {
}
- if (!empty($files)) {
- $file = current($files);
- if ($file instanceof Folder) {
+ if ($node) {
+ $params['fileid'] = $fileId;
+ if ($node instanceof Folder) {
// set the full path to enter the folder
- $params['dir'] = $baseFolder->getRelativePath($file->getPath());
+ $params['dir'] = $baseFolder->getRelativePath($node->getPath());
} else {
// set parent path as dir
- $params['dir'] = $baseFolder->getRelativePath($file->getParent()->getPath());
- // and scroll to the entry
- $params['scrollto'] = $file->getName();
+ $params['dir'] = $baseFolder->getRelativePath($node->getParent()->getPath());
+ // open the file by default (opening the viewer)
+ $params['openfile'] = 'true';
+ }
- if ($setOpenfile) {
- // forward the openfile URL parameter.
- $params['openfile'] = $fileId;
- }
+ // Forward open parameters if any.
+ // - openfile is true by default
+ // - opendetails is undefined by default
+ // - both will be evaluated as truthy
+ if ($openDetails !== null) {
+ $params['opendetails'] = $openDetails !== 'false' ? 'true' : 'false';
}
- return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', $params));
+ if ($openFile !== null) {
+ $params['openfile'] = $openFile !== 'false' ? 'true' : 'false';
+ }
+
+ return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', $params));
}
- throw new \OCP\Files\NotFoundException();
+
+ throw new NotFoundException();
}
}
diff --git a/apps/files/lib/Dashboard/FavoriteWidget.php b/apps/files/lib/Dashboard/FavoriteWidget.php
new file mode 100644
index 00000000000..b68b8a56b2e
--- /dev/null
+++ b/apps/files/lib/Dashboard/FavoriteWidget.php
@@ -0,0 +1,141 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Dashboard;
+
+use OCA\Files\AppInfo\Application;
+use OCP\Dashboard\IAPIWidgetV2;
+use OCP\Dashboard\IButtonWidget;
+use OCP\Dashboard\IIconWidget;
+use OCP\Dashboard\IOptionWidget;
+use OCP\Dashboard\Model\WidgetButton;
+use OCP\Dashboard\Model\WidgetItem;
+use OCP\Dashboard\Model\WidgetItems;
+use OCP\Dashboard\Model\WidgetOptions;
+use OCP\Files\File;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\IRootFolder;
+use OCP\IL10N;
+use OCP\IPreview;
+use OCP\ITagManager;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+
+class FavoriteWidget implements IIconWidget, IAPIWidgetV2, IButtonWidget, IOptionWidget {
+
+ public function __construct(
+ private readonly IL10N $l10n,
+ private readonly IURLGenerator $urlGenerator,
+ private readonly IMimeTypeDetector $mimeTypeDetector,
+ private readonly IUserManager $userManager,
+ private readonly ITagManager $tagManager,
+ private readonly IRootFolder $rootFolder,
+ private readonly IPreview $previewManager,
+ ) {
+ }
+
+ public function getId(): string {
+ return Application::APP_ID . '-favorites';
+ }
+
+ public function getTitle(): string {
+ return $this->l10n->t('Favorite files');
+ }
+
+ public function getOrder(): int {
+ return 0;
+ }
+
+ public function getIconClass(): string {
+ return 'icon-star-dark';
+ }
+
+ public function getIconUrl(): string {
+ return $this->urlGenerator->getAbsoluteURL(
+ $this->urlGenerator->imagePath('core', 'actions/star.svg')
+ );
+ }
+
+ public function getUrl(): ?string {
+ return $this->urlGenerator->linkToRouteAbsolute('files.View.indexView', ['view' => 'favorites']);
+ }
+
+ public function load(): void {
+ }
+
+ public function getItems(string $userId, int $limit = 7): array {
+ $user = $this->userManager->get($userId);
+
+ if (!$user) {
+ return [];
+ }
+ $tags = $this->tagManager->load('files', [], false, $userId);
+ $favorites = $tags->getFavorites();
+ if (empty($favorites)) {
+ return [];
+ }
+ $favoriteNodes = [];
+ $userFolder = $this->rootFolder->getUserFolder($userId);
+ $count = 0;
+ foreach ($favorites as $favorite) {
+ $node = $userFolder->getFirstNodeById($favorite);
+ if ($node) {
+ $url = $this->urlGenerator->linkToRouteAbsolute(
+ 'files.view.showFile', ['fileid' => $node->getId()]
+ );
+ if ($node instanceof File) {
+ $icon = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', [
+ 'x' => 256,
+ 'y' => 256,
+ 'fileId' => $node->getId(),
+ 'c' => $node->getEtag(),
+ 'mimeFallback' => true,
+ ]);
+ } else {
+ $icon = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'filetypes/folder.svg'));
+ }
+ $favoriteNodes[] = new WidgetItem(
+ $node->getName(),
+ '',
+ $url,
+ $icon,
+ (string)$node->getCreationTime()
+ );
+ $count++;
+ if ($count >= $limit) {
+ break;
+ }
+ }
+ }
+
+ return $favoriteNodes;
+ }
+
+ public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems {
+ $items = $this->getItems($userId, $limit);
+ return new WidgetItems(
+ $items,
+ count($items) === 0 ? $this->l10n->t('No favorites') : '',
+ );
+ }
+
+ public function getWidgetButtons(string $userId): array {
+ return [
+ new WidgetButton(
+ WidgetButton::TYPE_MORE,
+ $this->urlGenerator->linkToRouteAbsolute('files.View.indexView', ['view' => 'favorites']),
+ $this->l10n->t('More favorites')
+ ),
+ ];
+ }
+
+ public function getWidgetOptions(): WidgetOptions {
+ return new WidgetOptions(roundItemIcons: false);
+ }
+}
diff --git a/apps/files/lib/Db/OpenLocalEditor.php b/apps/files/lib/Db/OpenLocalEditor.php
index 81bb55e47f7..da7f5d13206 100644
--- a/apps/files/lib/Db/OpenLocalEditor.php
+++ b/apps/files/lib/Db/OpenLocalEditor.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Db;
diff --git a/apps/files/lib/Db/OpenLocalEditorMapper.php b/apps/files/lib/Db/OpenLocalEditorMapper.php
index 71573264c74..6ae8b79c258 100644
--- a/apps/files/lib/Db/OpenLocalEditorMapper.php
+++ b/apps/files/lib/Db/OpenLocalEditorMapper.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Db;
@@ -32,6 +15,9 @@ use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\IDBConnection;
+/**
+ * @template-extends QBMapper<OpenLocalEditor>
+ */
class OpenLocalEditorMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'open_local_editor', OpenLocalEditor::class);
diff --git a/apps/files/lib/Db/TransferOwnership.php b/apps/files/lib/Db/TransferOwnership.php
index 1ab94361ac9..ae78c19b76d 100644
--- a/apps/files/lib/Db/TransferOwnership.php
+++ b/apps/files/lib/Db/TransferOwnership.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Db;
diff --git a/apps/files/lib/Db/TransferOwnershipMapper.php b/apps/files/lib/Db/TransferOwnershipMapper.php
index f55f9e733f9..8b29399f768 100644
--- a/apps/files/lib/Db/TransferOwnershipMapper.php
+++ b/apps/files/lib/Db/TransferOwnershipMapper.php
@@ -3,32 +3,17 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Db;
use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
+/**
+ * @template-extends QBMapper<TransferOwnership>
+ */
class TransferOwnershipMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'user_transfer_owner', TransferOwnership::class);
diff --git a/apps/files/lib/DirectEditingCapabilities.php b/apps/files/lib/DirectEditingCapabilities.php
index 782f7019ac7..5bceef9305f 100644
--- a/apps/files/lib/DirectEditingCapabilities.php
+++ b/apps/files/lib/DirectEditingCapabilities.php
@@ -1,25 +1,9 @@
<?php
+
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files;
@@ -29,21 +13,22 @@ use OCP\Capabilities\IInitialStateExcludedCapability;
use OCP\IURLGenerator;
class DirectEditingCapabilities implements ICapability, IInitialStateExcludedCapability {
-
- protected DirectEditingService $directEditingService;
- protected IURLGenerator $urlGenerator;
-
- public function __construct(DirectEditingService $directEditingService, IURLGenerator $urlGenerator) {
- $this->directEditingService = $directEditingService;
- $this->urlGenerator = $urlGenerator;
+ public function __construct(
+ protected DirectEditingService $directEditingService,
+ protected IURLGenerator $urlGenerator,
+ ) {
}
+ /**
+ * @return array{files: array{directEditing: array{url: string, etag: string, supportsFileId: bool}}}
+ */
public function getCapabilities() {
return [
'files' => [
'directEditing' => [
'url' => $this->urlGenerator->linkToOCSRouteAbsolute('files.DirectEditing.info'),
- 'etag' => $this->directEditingService->getDirectEditingETag()
+ 'etag' => $this->directEditingService->getDirectEditingETag(),
+ 'supportsFileId' => true,
]
],
];
diff --git a/apps/files/lib/Event/LoadAdditionalScriptsEvent.php b/apps/files/lib/Event/LoadAdditionalScriptsEvent.php
index 5291a776e81..d1cf7f4016e 100644
--- a/apps/files/lib/Event/LoadAdditionalScriptsEvent.php
+++ b/apps/files/lib/Event/LoadAdditionalScriptsEvent.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Event;
@@ -31,18 +12,8 @@ use OCP\EventDispatcher\Event;
/**
* This event is triggered when the files app is rendered.
- * It can be used to add additional scripts to the files app.
*
* @since 17.0.0
*/
class LoadAdditionalScriptsEvent extends Event {
- private $hiddenFields = [];
-
- public function addHiddenField(string $name, string $value): void {
- $this->hiddenFields[$name] = $value;
- }
-
- public function getHiddenFields(): array {
- return $this->hiddenFields;
- }
}
diff --git a/apps/files/lib/Event/LoadSearchPlugins.php b/apps/files/lib/Event/LoadSearchPlugins.php
new file mode 100644
index 00000000000..9c6c81fcca5
--- /dev/null
+++ b/apps/files/lib/Event/LoadSearchPlugins.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Event;
+
+use OCP\EventDispatcher\Event;
+
+class LoadSearchPlugins extends Event {
+}
diff --git a/apps/files/lib/Event/LoadSidebar.php b/apps/files/lib/Event/LoadSidebar.php
index 8dc30e33632..01db57bb562 100644
--- a/apps/files/lib/Event/LoadSidebar.php
+++ b/apps/files/lib/Event/LoadSidebar.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Event;
diff --git a/apps/files/lib/Exception/TransferOwnershipException.php b/apps/files/lib/Exception/TransferOwnershipException.php
index 1849976b6db..531c5d513da 100644
--- a/apps/files/lib/Exception/TransferOwnershipException.php
+++ b/apps/files/lib/Exception/TransferOwnershipException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Exception;
diff --git a/apps/files/lib/Helper.php b/apps/files/lib/Helper.php
index 1d9591cafe0..b1439ac7fa5 100644
--- a/apps/files/lib/Helper.php
+++ b/apps/files/lib/Helper.php
@@ -1,101 +1,26 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author brumsel <brumsel@losecatcher.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Michael Jobst <mjobst+github@tecratech.de>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files;
+use OC\Files\Filesystem;
use OCP\Files\FileInfo;
-use OCP\ITagManager;
+use OCP\Util;
/**
* Helper class for manipulating file information
*/
class Helper {
/**
- * @param string $dir
- * @return array
- * @throws \OCP\Files\NotFoundException
- */
- public static function buildFileStorageStatistics($dir) {
- // information about storage capacities
- $storageInfo = \OC_Helper::getStorageInfo($dir);
- $l = \OC::$server->getL10N('files');
- $maxUploadFileSize = \OCP\Util::maxUploadFilesize($dir, $storageInfo['free']);
- $maxHumanFileSize = \OCP\Util::humanFileSize($maxUploadFileSize);
- $maxHumanFileSize = $l->t('Upload (max. %s)', [$maxHumanFileSize]);
-
- return [
- 'uploadMaxFilesize' => $maxUploadFileSize,
- 'maxHumanFilesize' => $maxHumanFileSize,
- 'freeSpace' => $storageInfo['free'],
- 'quota' => $storageInfo['quota'],
- 'total' => $storageInfo['total'],
- 'used' => $storageInfo['used'],
- 'usedSpacePercent' => $storageInfo['relative'],
- 'owner' => $storageInfo['owner'],
- 'ownerDisplayName' => $storageInfo['ownerDisplayName'],
- 'mountType' => $storageInfo['mountType'],
- 'mountPoint' => $storageInfo['mountPoint'],
- ];
- }
-
- /**
- * Determine icon for a given file
- *
- * @param \OCP\Files\FileInfo $file file info
- * @return string icon URL
- */
- public static function determineIcon($file) {
- if ($file['type'] === 'dir') {
- $icon = \OC::$server->getMimeTypeDetector()->mimeTypeIcon('dir');
- // TODO: move this part to the client side, using mountType
- if ($file->isShared()) {
- $icon = \OC::$server->getMimeTypeDetector()->mimeTypeIcon('dir-shared');
- } elseif ($file->isMounted()) {
- $icon = \OC::$server->getMimeTypeDetector()->mimeTypeIcon('dir-external');
- }
- } else {
- $icon = \OC::$server->getMimeTypeDetector()->mimeTypeIcon($file->getMimetype());
- }
-
- return substr($icon, 0, -3) . 'svg';
- }
-
- /**
* Comparator function to sort files alphabetically and have
* the directories appear first
*
- * @param \OCP\Files\FileInfo $a file
- * @param \OCP\Files\FileInfo $b file
+ * @param FileInfo $a file
+ * @param FileInfo $b file
* @return int -1 if $a must come before $b, 1 otherwise
*/
public static function compareFileNames(FileInfo $a, FileInfo $b) {
@@ -106,15 +31,15 @@ class Helper {
} elseif ($aType !== 'dir' and $bType === 'dir') {
return 1;
} else {
- return \OCP\Util::naturalSortCompare($a->getName(), $b->getName());
+ return Util::naturalSortCompare($a->getName(), $b->getName());
}
}
/**
* Comparator function to sort files by date
*
- * @param \OCP\Files\FileInfo $a file
- * @param \OCP\Files\FileInfo $b file
+ * @param FileInfo $a file
+ * @param FileInfo $b file
* @return int -1 if $a must come before $b, 1 otherwise
*/
public static function compareTimestamp(FileInfo $a, FileInfo $b) {
@@ -126,8 +51,8 @@ class Helper {
/**
* Comparator function to sort files by size
*
- * @param \OCP\Files\FileInfo $a file
- * @param \OCP\Files\FileInfo $b file
+ * @param FileInfo $a file
+ * @param FileInfo $b file
* @return int -1 if $a must come before $b, 1 otherwise
*/
public static function compareSize(FileInfo $a, FileInfo $b) {
@@ -139,22 +64,24 @@ class Helper {
/**
* Formats the file info to be returned as JSON to the client.
*
- * @param \OCP\Files\FileInfo $i
+ * @param FileInfo $i
* @return array formatted file info
*/
public static function formatFileInfo(FileInfo $i) {
$entry = [];
- $entry['id'] = $i['fileid'];
- $entry['parentId'] = $i['parent'];
- $entry['mtime'] = $i['mtime'] * 1000;
+ $entry['id'] = $i->getId();
+ $entry['parentId'] = $i->getParentId();
+ $entry['mtime'] = $i->getMtime() * 1000;
// only pick out the needed attributes
$entry['name'] = $i->getName();
- $entry['permissions'] = $i['permissions'];
- $entry['mimetype'] = $i['mimetype'];
- $entry['size'] = $i['size'];
- $entry['type'] = $i['type'];
- $entry['etag'] = $i['etag'];
+ $entry['permissions'] = $i->getPermissions();
+ $entry['mimetype'] = $i->getMimetype();
+ $entry['size'] = $i->getSize();
+ $entry['type'] = $i->getType();
+ $entry['etag'] = $i->getEtag();
+ // TODO: this is using the private implementation of FileInfo
+ // the array access is not part of the public interface
if (isset($i['tags'])) {
$entry['tags'] = $i['tags'];
}
@@ -164,6 +91,10 @@ class Helper {
if (isset($i['is_share_mount_point'])) {
$entry['isShareMountPoint'] = $i['is_share_mount_point'];
}
+ if (isset($i['extraData'])) {
+ $entry['extraData'] = $i['extraData'];
+ }
+
$mountType = null;
$mount = $i->getMountPoint();
$mountType = $mount->getMountType();
@@ -173,27 +104,10 @@ class Helper {
}
$entry['mountType'] = $mountType;
}
- if (isset($i['extraData'])) {
- $entry['extraData'] = $i['extraData'];
- }
return $entry;
}
/**
- * Format file info for JSON
- * @param \OCP\Files\FileInfo[] $fileInfos file infos
- * @return array
- */
- public static function formatFileInfos($fileInfos) {
- $files = [];
- foreach ($fileInfos as $i) {
- $files[] = self::formatFileInfo($i);
- }
-
- return $files;
- }
-
- /**
* Retrieves the contents of the given directory and
* returns it as a sorted array of FileInfo.
*
@@ -201,61 +115,21 @@ class Helper {
* @param string $sortAttribute attribute to sort on
* @param bool $sortDescending true for descending sort, false otherwise
* @param string $mimetypeFilter limit returned content to this mimetype or mimepart
- * @return \OCP\Files\FileInfo[] files
+ * @return FileInfo[] files
*/
public static function getFiles($dir, $sortAttribute = 'name', $sortDescending = false, $mimetypeFilter = '') {
- $content = \OC\Files\Filesystem::getDirectoryContent($dir, $mimetypeFilter);
+ $content = Filesystem::getDirectoryContent($dir, $mimetypeFilter);
return self::sortFiles($content, $sortAttribute, $sortDescending);
}
/**
- * Populate the result set with file tags
- *
- * @param array $fileList
- * @param string $fileIdentifier identifier attribute name for values in $fileList
- * @param ITagManager $tagManager
- * @return array file list populated with tags
- */
- public static function populateTags(array $fileList, $fileIdentifier, ITagManager $tagManager) {
- $ids = [];
- foreach ($fileList as $fileData) {
- $ids[] = $fileData[$fileIdentifier];
- }
- $tagger = $tagManager->load('files');
- $tags = $tagger->getTagsForObjects($ids);
-
- if (!is_array($tags)) {
- throw new \UnexpectedValueException('$tags must be an array');
- }
-
- // Set empty tag array
- foreach ($fileList as $key => $fileData) {
- $fileList[$key]['tags'] = [];
- }
-
- if (!empty($tags)) {
- foreach ($tags as $fileId => $fileTags) {
- foreach ($fileList as $key => $fileData) {
- if ($fileId !== $fileData[$fileIdentifier]) {
- continue;
- }
-
- $fileList[$key]['tags'] = $fileTags;
- }
- }
- }
-
- return $fileList;
- }
-
- /**
* Sort the given file info array
*
- * @param \OCP\Files\FileInfo[] $files files to sort
+ * @param FileInfo[] $files files to sort
* @param string $sortAttribute attribute to sort on
* @param bool $sortDescending true for descending sort, false otherwise
- * @return \OCP\Files\FileInfo[] sorted files
+ * @return FileInfo[] sorted files
*/
public static function sortFiles($files, $sortAttribute = 'name', $sortDescending = false) {
$sortFunc = 'compareFileNames';
diff --git a/apps/files/lib/Listener/LegacyLoadAdditionalScriptsAdapter.php b/apps/files/lib/Listener/LegacyLoadAdditionalScriptsAdapter.php
deleted file mode 100644
index f60f9d73d78..00000000000
--- a/apps/files/lib/Listener/LegacyLoadAdditionalScriptsAdapter.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\Files\Listener;
-
-use OC\EventDispatcher\SymfonyAdapter;
-use OCA\Files\Event\LoadAdditionalScriptsEvent;
-use OCP\EventDispatcher\Event;
-use OCP\EventDispatcher\IEventListener;
-use Symfony\Component\EventDispatcher\GenericEvent;
-
-class LegacyLoadAdditionalScriptsAdapter implements IEventListener {
-
- /** @var SymfonyAdapter */
- private $dispatcher;
-
- public function __construct(SymfonyAdapter $dispatcher) {
- $this->dispatcher = $dispatcher;
- }
-
- public function handle(Event $event): void {
- if (!($event instanceof LoadAdditionalScriptsEvent)) {
- return;
- }
-
- $legacyEvent = new GenericEvent(null, ['hiddenFields' => []]);
- $this->dispatcher->dispatch('OCA\Files::loadAdditionalScripts', $legacyEvent);
-
- $hiddenFields = $legacyEvent->getArgument('hiddenFields');
- foreach ($hiddenFields as $name => $value) {
- $event->addHiddenField($name, $value);
- }
- }
-}
diff --git a/apps/files/lib/Listener/LoadSearchPluginsListener.php b/apps/files/lib/Listener/LoadSearchPluginsListener.php
new file mode 100644
index 00000000000..4cc4ca22f83
--- /dev/null
+++ b/apps/files/lib/Listener/LoadSearchPluginsListener.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Listener;
+
+use OCA\Files\Event\LoadSearchPlugins;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Util;
+
+/** @template-implements IEventListener<LoadSearchPlugins> */
+class LoadSearchPluginsListener implements IEventListener {
+ public function handle(Event $event): void {
+ if (!$event instanceof LoadSearchPlugins) {
+ return;
+ }
+
+ Util::addScript('files', 'search');
+ }
+}
diff --git a/apps/files/lib/Listener/LoadSidebarListener.php b/apps/files/lib/Listener/LoadSidebarListener.php
index 15f24279e0a..78b48ab1ce0 100644
--- a/apps/files/lib/Listener/LoadSidebarListener.php
+++ b/apps/files/lib/Listener/LoadSidebarListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Listener;
@@ -31,6 +14,7 @@ use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
+/** @template-implements IEventListener<LoadSidebar> */
class LoadSidebarListener implements IEventListener {
public function handle(Event $event): void {
if (!($event instanceof LoadSidebar)) {
@@ -38,8 +22,5 @@ class LoadSidebarListener implements IEventListener {
}
Util::addScript(Application::APP_ID, 'sidebar');
- // needed by the Sidebar legacy tabs
- // TODO: remove when all tabs migrated to the new api
- Util::addScript('files', 'fileinfomodel');
}
}
diff --git a/apps/files/lib/Listener/NodeAddedToFavoriteListener.php b/apps/files/lib/Listener/NodeAddedToFavoriteListener.php
new file mode 100644
index 00000000000..827c1851d3d
--- /dev/null
+++ b/apps/files/lib/Listener/NodeAddedToFavoriteListener.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Listener;
+
+use OCA\Files\Activity\FavoriteProvider;
+use OCP\Activity\IManager as IActivityManager;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Events\NodeAddedToFavorite;
+
+/** @template-implements IEventListener<NodeAddedToFavorite> */
+class NodeAddedToFavoriteListener implements IEventListener {
+ public function __construct(
+ private IActivityManager $activityManager,
+ ) {
+ }
+ public function handle(Event $event):void {
+ if (!($event instanceof NodeAddedToFavorite)) {
+ return;
+ }
+ $activityEvent = $this->activityManager->generateEvent();
+ try {
+ $activityEvent->setApp('files')
+ ->setObject('files', $event->getFileId(), $event->getPath())
+ ->setType('favorite')
+ ->setAuthor($event->getUser()->getUID())
+ ->setAffectedUser($event->getUser()->getUID())
+ ->setTimestamp(time())
+ ->setSubject(
+ FavoriteProvider::SUBJECT_ADDED,
+ ['id' => $event->getFileId(), 'path' => $event->getPath()]
+ );
+ $this->activityManager->publish($activityEvent);
+ } catch (\InvalidArgumentException $e) {
+ } catch (\BadMethodCallException $e) {
+ }
+ }
+}
diff --git a/apps/files/lib/Listener/NodeRemovedFromFavoriteListener.php b/apps/files/lib/Listener/NodeRemovedFromFavoriteListener.php
new file mode 100644
index 00000000000..fe39d4af540
--- /dev/null
+++ b/apps/files/lib/Listener/NodeRemovedFromFavoriteListener.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Listener;
+
+use OCA\Files\Activity\FavoriteProvider;
+use OCP\Activity\IManager as IActivityManager;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Events\NodeRemovedFromFavorite;
+
+/** @template-implements IEventListener<NodeRemovedFromFavorite> */
+class NodeRemovedFromFavoriteListener implements IEventListener {
+ public function __construct(
+ private IActivityManager $activityManager,
+ ) {
+ }
+ public function handle(Event $event):void {
+ if (!($event instanceof NodeRemovedFromFavorite)) {
+ return;
+ }
+ $activityEvent = $this->activityManager->generateEvent();
+ try {
+ $activityEvent->setApp('files')
+ ->setObject('files', $event->getFileId(), $event->getPath())
+ ->setType('favorite')
+ ->setAuthor($event->getUser()->getUID())
+ ->setAffectedUser($event->getUser()->getUID())
+ ->setTimestamp(time())
+ ->setSubject(
+ FavoriteProvider::SUBJECT_REMOVED,
+ ['id' => $event->getFileId(), 'path' => $event->getPath()]
+ );
+ $this->activityManager->publish($activityEvent);
+ } catch (\InvalidArgumentException $e) {
+ } catch (\BadMethodCallException $e) {
+ }
+ }
+}
diff --git a/apps/files/lib/Listener/RenderReferenceEventListener.php b/apps/files/lib/Listener/RenderReferenceEventListener.php
new file mode 100644
index 00000000000..b7470e5acf5
--- /dev/null
+++ b/apps/files/lib/Listener/RenderReferenceEventListener.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Listener;
+
+use OCP\Collaboration\Reference\RenderReferenceEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Util;
+
+/** @template-implements IEventListener<RenderReferenceEvent> */
+class RenderReferenceEventListener implements IEventListener {
+ public function handle(Event $event): void {
+ if (!$event instanceof RenderReferenceEvent) {
+ return;
+ }
+
+ Util::addScript('files', 'reference-files');
+ }
+}
diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php
new file mode 100644
index 00000000000..b6773e8c452
--- /dev/null
+++ b/apps/files/lib/Listener/SyncLivePhotosListener.php
@@ -0,0 +1,254 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Listener;
+
+use Exception;
+use OC\Files\Node\NonExistingFile;
+use OC\Files\Node\NonExistingFolder;
+use OC\Files\View;
+use OC\FilesMetadata\Model\FilesMetadata;
+use OCA\Files\Service\LivePhotosService;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Exceptions\AbortedEventException;
+use OCP\Files\Cache\CacheEntryRemovedEvent;
+use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
+use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
+use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
+use OCP\Files\Events\Node\NodeCopiedEvent;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\FilesMetadata\IFilesMetadataManager;
+
+/**
+ * @template-implements IEventListener<Event>
+ */
+class SyncLivePhotosListener implements IEventListener {
+ /** @var Array<int> */
+ private array $pendingRenames = [];
+ /** @var Array<int, bool> */
+ private array $pendingDeletion = [];
+ /** @var Array<int> */
+ private array $pendingCopies = [];
+
+ public function __construct(
+ private ?Folder $userFolder,
+ private IFilesMetadataManager $filesMetadataManager,
+ private LivePhotosService $livePhotosService,
+ private IRootFolder $rootFolder,
+ private View $view,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($this->userFolder === null) {
+ return;
+ }
+
+ if ($event instanceof BeforeNodeCopiedEvent || $event instanceof NodeCopiedEvent) {
+ $this->handleCopyRecursive($event, $event->getSource(), $event->getTarget());
+ } else {
+ $peerFileId = null;
+
+ if ($event instanceof BeforeNodeRenamedEvent) {
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getSource()->getId());
+ } elseif ($event instanceof BeforeNodeDeletedEvent) {
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getNode()->getId());
+ } elseif ($event instanceof CacheEntryRemovedEvent) {
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getFileId());
+ }
+
+ if ($peerFileId === null) {
+ return; // Not a live photo.
+ }
+
+ // Check the user's folder.
+ $peerFile = $this->userFolder->getFirstNodeById($peerFileId);
+
+ if ($peerFile === null) {
+ return; // Peer file not found.
+ }
+
+ if ($event instanceof BeforeNodeRenamedEvent) {
+ $this->runMoveOrCopyChecks($event->getSource(), $event->getTarget(), $peerFile);
+ $this->handleMove($event->getSource(), $event->getTarget(), $peerFile);
+ } elseif ($event instanceof BeforeNodeDeletedEvent) {
+ $this->handleDeletion($event, $peerFile);
+ } elseif ($event instanceof CacheEntryRemovedEvent) {
+ $peerFile->delete();
+ }
+ }
+ }
+
+ private function runMoveOrCopyChecks(Node $sourceFile, Node $targetFile, Node $peerFile): void {
+ $targetParent = $targetFile->getParent();
+ $sourceExtension = $sourceFile->getExtension();
+ $peerFileExtension = $peerFile->getExtension();
+ $targetName = $targetFile->getName();
+ $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension;
+
+ if (!str_ends_with($targetName, '.' . $sourceExtension)) {
+ throw new AbortedEventException('Cannot change the extension of a Live Photo');
+ }
+
+ try {
+ $targetParent->get($targetName);
+ throw new AbortedEventException('A file already exist at destination path of the Live Photo');
+ } catch (NotFoundException) {
+ }
+
+ if (!($targetParent instanceof NonExistingFolder)) {
+ try {
+ $targetParent->get($peerTargetName);
+ throw new AbortedEventException('A file already exist at destination path of the Live Photo');
+ } catch (NotFoundException) {
+ }
+ }
+ }
+
+ /**
+ * During rename events, which also include move operations,
+ * we rename the peer file using the same name.
+ * The event listener being singleton, we can store the current state
+ * of pending renames inside the 'pendingRenames' property,
+ * to prevent infinite recursive.
+ */
+ private function handleMove(Node $sourceFile, Node $targetFile, Node $peerFile): void {
+ $targetParent = $targetFile->getParent();
+ $sourceExtension = $sourceFile->getExtension();
+ $peerFileExtension = $peerFile->getExtension();
+ $targetName = $targetFile->getName();
+ $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension;
+
+ // in case the rename was initiated from this listener, we stop right now
+ if (in_array($peerFile->getId(), $this->pendingRenames)) {
+ return;
+ }
+
+ $this->pendingRenames[] = $sourceFile->getId();
+ try {
+ $peerFile->move($targetParent->getPath() . '/' . $peerTargetName);
+ } catch (\Throwable $ex) {
+ throw new AbortedEventException($ex->getMessage());
+ }
+
+ $this->pendingRenames = array_diff($this->pendingRenames, [$sourceFile->getId()]);
+ }
+
+
+ /**
+ * handle copy, we already know if it is doable from BeforeNodeCopiedEvent, so we just copy the linked file
+ */
+ private function handleCopy(File $sourceFile, File $targetFile, File $peerFile): void {
+ $sourceExtension = $sourceFile->getExtension();
+ $peerFileExtension = $peerFile->getExtension();
+ $targetParent = $targetFile->getParent();
+ $targetName = $targetFile->getName();
+ $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension;
+
+ if ($targetParent->nodeExists($peerTargetName)) {
+ // If the copy was a folder copy, then the peer file already exists.
+ $targetPeerFile = $targetParent->get($peerTargetName);
+ } else {
+ // If the copy was a file copy, then we need to create the peer file.
+ $targetPeerFile = $peerFile->copy($targetParent->getPath() . '/' . $peerTargetName);
+ }
+
+ /** @var FilesMetadata $targetMetadata */
+ $targetMetadata = $this->filesMetadataManager->getMetadata($targetFile->getId(), true);
+ $targetMetadata->setStorageId($targetFile->getStorage()->getCache()->getNumericStorageId());
+ $targetMetadata->setString('files-live-photo', (string)$targetPeerFile->getId());
+ $this->filesMetadataManager->saveMetadata($targetMetadata);
+ /** @var FilesMetadata $peerMetadata */
+ $peerMetadata = $this->filesMetadataManager->getMetadata($targetPeerFile->getId(), true);
+ $peerMetadata->setStorageId($targetPeerFile->getStorage()->getCache()->getNumericStorageId());
+ $peerMetadata->setString('files-live-photo', (string)$targetFile->getId());
+ $this->filesMetadataManager->saveMetadata($peerMetadata);
+ }
+
+ /**
+ * During deletion event, we trigger another recursive delete on the peer file.
+ * Delete operations on the .mov file directly are currently blocked.
+ * The event listener being singleton, we can store the current state
+ * of pending deletions inside the 'pendingDeletions' property,
+ * to prevent infinite recursivity.
+ */
+ private function handleDeletion(BeforeNodeDeletedEvent $event, Node $peerFile): void {
+ $deletedFile = $event->getNode();
+ if ($deletedFile->getMimetype() === 'video/quicktime') {
+ if (isset($this->pendingDeletion[$peerFile->getId()])) {
+ unset($this->pendingDeletion[$peerFile->getId()]);
+ return;
+ } else {
+ throw new AbortedEventException('Cannot delete the video part of a live photo');
+ }
+ } else {
+ $this->pendingDeletion[$deletedFile->getId()] = true;
+ try {
+ $peerFile->delete();
+ } catch (\Throwable $ex) {
+ throw new AbortedEventException($ex->getMessage());
+ }
+ }
+ return;
+ }
+
+ /*
+ * Recursively get all the peer ids of a live photo.
+ * Needed when coping a folder.
+ *
+ * @param BeforeNodeCopiedEvent|NodeCopiedEvent $event
+ */
+ private function handleCopyRecursive(Event $event, Node $sourceNode, Node $targetNode): void {
+ if ($sourceNode instanceof Folder && $targetNode instanceof Folder) {
+ foreach ($sourceNode->getDirectoryListing() as $sourceChild) {
+ if ($event instanceof BeforeNodeCopiedEvent) {
+ if ($sourceChild instanceof Folder) {
+ $targetChild = new NonExistingFolder($this->rootFolder, $this->view, $targetNode->getPath() . '/' . $sourceChild->getName(), null, $targetNode);
+ } else {
+ $targetChild = new NonExistingFile($this->rootFolder, $this->view, $targetNode->getPath() . '/' . $sourceChild->getName(), null, $targetNode);
+ }
+ } elseif ($event instanceof NodeCopiedEvent) {
+ $targetChild = $targetNode->get($sourceChild->getName());
+ } else {
+ throw new Exception('Event is type is not supported');
+ }
+
+ $this->handleCopyRecursive($event, $sourceChild, $targetChild);
+ }
+ } elseif ($sourceNode instanceof File && $targetNode instanceof File) {
+ // in case the copy was initiated from this listener, we stop right now
+ if (in_array($sourceNode->getId(), $this->pendingCopies)) {
+ return;
+ }
+
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($sourceNode->getId());
+ if ($peerFileId === null) {
+ return;
+ }
+ $peerFile = $this->userFolder->getFirstNodeById($peerFileId);
+ if ($peerFile === null) {
+ return;
+ }
+
+ $this->pendingCopies[] = $peerFileId;
+ if ($event instanceof BeforeNodeCopiedEvent) {
+ $this->runMoveOrCopyChecks($sourceNode, $targetNode, $peerFile);
+ } elseif ($event instanceof NodeCopiedEvent) {
+ $this->handleCopy($sourceNode, $targetNode, $peerFile);
+ }
+ $this->pendingCopies = array_diff($this->pendingCopies, [$peerFileId]);
+ } else {
+ throw new Exception('Source and target type are not matching');
+ }
+ }
+}
diff --git a/apps/files/lib/Migration/Version11301Date20191205150729.php b/apps/files/lib/Migration/Version11301Date20191205150729.php
index e7bda7ef38a..2e3d72c7ece 100644
--- a/apps/files/lib/Migration/Version11301Date20191205150729.php
+++ b/apps/files/lib/Migration/Version11301Date20191205150729.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Migration;
diff --git a/apps/files/lib/Migration/Version12101Date20221011153334.php b/apps/files/lib/Migration/Version12101Date20221011153334.php
index 0c1093f50a6..ed4d8bef90b 100644
--- a/apps/files/lib/Migration/Version12101Date20221011153334.php
+++ b/apps/files/lib/Migration/Version12101Date20221011153334.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Migration;
@@ -38,7 +21,7 @@ class Version12101Date20221011153334 extends SimpleMigrationStep {
$schema = $schemaClosure();
$table = $schema->createTable('open_local_editor');
- $table->addColumn('id',Types::BIGINT, [
+ $table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
diff --git a/apps/files/lib/Migration/Version2003Date20241021095629.php b/apps/files/lib/Migration/Version2003Date20241021095629.php
new file mode 100644
index 00000000000..30d05fa12ad
--- /dev/null
+++ b/apps/files/lib/Migration/Version2003Date20241021095629.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Migration;
+
+use Closure;
+use OCA\Files\Service\ChunkedUploadConfig;
+use OCP\DB\ISchemaWrapper;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+use OCP\Server;
+
+class Version2003Date20241021095629 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ */
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ $maxChunkSize = Server::get(IConfig::class)->getAppValue('files', 'max_chunk_size');
+ if ($maxChunkSize === '') {
+ // Skip if no value was configured before
+ return;
+ }
+
+ ChunkedUploadConfig::setMaxChunkSize((int)$maxChunkSize);
+ Server::get(IConfig::class)->deleteAppValue('files', 'max_chunk_size');
+ }
+}
diff --git a/apps/files/lib/Notification/Notifier.php b/apps/files/lib/Notification/Notifier.php
index 90784749b27..6acc312c126 100644
--- a/apps/files/lib/Notification/Notifier.php
+++ b/apps/files/lib/Notification/Notifier.php
@@ -3,33 +3,16 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sascha Wiswedel <sascha.wiswedel@nextcloud.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Notification;
+use OCA\Files\BackgroundJob\TransferOwnership;
use OCA\Files\Db\TransferOwnershipMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
@@ -39,34 +22,18 @@ use OCP\Notification\IDismissableNotifier;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
+use OCP\Notification\UnknownNotificationException;
class Notifier implements INotifier, IDismissableNotifier {
- /** @var IFactory */
- protected $l10nFactory;
-
- /** @var IURLGenerator */
- protected $urlGenerator;
- /** @var TransferOwnershipMapper */
- private $mapper;
- /** @var IManager */
- private $notificationManager;
- /** @var IUserManager */
- private $userManager;
- /** @var ITimeFactory */
- private $timeFactory;
-
- public function __construct(IFactory $l10nFactory,
- IURLGenerator $urlGenerator,
- TransferOwnershipMapper $mapper,
- IManager $notificationManager,
- IUserManager $userManager,
- ITimeFactory $timeFactory) {
- $this->l10nFactory = $l10nFactory;
- $this->urlGenerator = $urlGenerator;
- $this->mapper = $mapper;
- $this->notificationManager = $notificationManager;
- $this->userManager = $userManager;
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ protected IFactory $l10nFactory,
+ protected IURLGenerator $urlGenerator,
+ private TransferOwnershipMapper $mapper,
+ private IManager $notificationManager,
+ private IUserManager $userManager,
+ private IJobList $jobList,
+ private ITimeFactory $timeFactory,
+ ) {
}
public function getID(): string {
@@ -81,30 +48,26 @@ class Notifier implements INotifier, IDismissableNotifier {
* @param INotification $notification
* @param string $languageCode The code of the language that should be used to prepare the notification
* @return INotification
- * @throws \InvalidArgumentException When the notification was not prepared by a notifier
+ * @throws UnknownNotificationException When the notification was not prepared by a notifier
*/
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== 'files') {
- throw new \InvalidArgumentException('Unhandled app');
+ throw new UnknownNotificationException('Unhandled app');
}
- if ($notification->getSubject() === 'transferownershipRequest') {
- return $this->handleTransferownershipRequest($notification, $languageCode);
- }
- if ($notification->getSubject() === 'transferOwnershipFailedSource') {
- return $this->handleTransferOwnershipFailedSource($notification, $languageCode);
- }
- if ($notification->getSubject() === 'transferOwnershipFailedTarget') {
- return $this->handleTransferOwnershipFailedTarget($notification, $languageCode);
- }
- if ($notification->getSubject() === 'transferOwnershipDoneSource') {
- return $this->handleTransferOwnershipDoneSource($notification, $languageCode);
- }
- if ($notification->getSubject() === 'transferOwnershipDoneTarget') {
- return $this->handleTransferOwnershipDoneTarget($notification, $languageCode);
- }
-
- throw new \InvalidArgumentException('Unhandled subject');
+ $imagePath = $this->urlGenerator->imagePath('files', 'folder-move.svg');
+ $iconUrl = $this->urlGenerator->getAbsoluteURL($imagePath);
+ $notification->setIcon($iconUrl);
+
+ return match($notification->getSubject()) {
+ 'transferownershipRequest' => $this->handleTransferownershipRequest($notification, $languageCode),
+ 'transferownershipRequestDenied' => $this->handleTransferOwnershipRequestDenied($notification, $languageCode),
+ 'transferOwnershipFailedSource' => $this->handleTransferOwnershipFailedSource($notification, $languageCode),
+ 'transferOwnershipFailedTarget' => $this->handleTransferOwnershipFailedTarget($notification, $languageCode),
+ 'transferOwnershipDoneSource' => $this->handleTransferOwnershipDoneSource($notification, $languageCode),
+ 'transferOwnershipDoneTarget' => $this->handleTransferOwnershipDoneTarget($notification, $languageCode),
+ default => throw new UnknownNotificationException('Unhandled subject')
+ };
}
public function handleTransferownershipRequest(INotification $notification, string $languageCode): INotification {
@@ -163,6 +126,29 @@ class Notifier implements INotifier, IDismissableNotifier {
return $notification;
}
+ public function handleTransferOwnershipRequestDenied(INotification $notification, string $languageCode): INotification {
+ $l = $this->l10nFactory->get('files', $languageCode);
+ $param = $notification->getSubjectParameters();
+
+ $targetUser = $this->getUser($param['targetUser']);
+ $notification->setRichSubject($l->t('Ownership transfer denied'))
+ ->setRichMessage(
+ $l->t('Your ownership transfer of {path} was denied by {user}.'),
+ [
+ 'path' => [
+ 'type' => 'highlight',
+ 'id' => $param['targetUser'] . '::' . $param['nodeName'],
+ 'name' => $param['nodeName'],
+ ],
+ 'user' => [
+ 'type' => 'user',
+ 'id' => $targetUser->getUID(),
+ 'name' => $targetUser->getDisplayName(),
+ ],
+ ]);
+ return $notification;
+ }
+
public function handleTransferOwnershipFailedSource(INotification $notification, string $languageCode): INotification {
$l = $this->l10nFactory->get('files', $languageCode);
$param = $notification->getSubjectParameters();
@@ -260,7 +246,10 @@ class Notifier implements INotifier, IDismissableNotifier {
public function dismissNotification(INotification $notification): void {
if ($notification->getApp() !== 'files') {
- throw new \InvalidArgumentException('Unhandled app');
+ throw new UnknownNotificationException('Unhandled app');
+ }
+ if ($notification->getSubject() !== 'transferownershipRequest') {
+ throw new UnknownNotificationException('Unhandled notification type');
}
// TODO: This should all be moved to a service that also the transferownershipController uses.
@@ -270,6 +259,12 @@ class Notifier implements INotifier, IDismissableNotifier {
return;
}
+ if ($this->jobList->has(TransferOwnership::class, [
+ 'id' => $transferOwnership->getId(),
+ ])) {
+ return;
+ }
+
$notification = $this->notificationManager->createNotification();
$notification->setUser($transferOwnership->getSourceUser())
->setApp('files')
diff --git a/apps/files/lib/ResponseDefinitions.php b/apps/files/lib/ResponseDefinitions.php
new file mode 100644
index 00000000000..c5d094e7bd8
--- /dev/null
+++ b/apps/files/lib/ResponseDefinitions.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files;
+
+/**
+ * @psalm-type FilesTemplateFile = array{
+ * basename: string,
+ * etag: string,
+ * fileid: int,
+ * filename: ?string,
+ * lastmod: int,
+ * mime: string,
+ * size: int,
+ * type: string,
+ * hasPreview: bool,
+ * }
+ *
+ * @psalm-type FilesTemplateField = array{
+ * index: string,
+ * type: string,
+ * alias: ?string,
+ * tag: ?string,
+ * id: ?int,
+ * content?: string,
+ * checked?: bool,
+ * }
+ *
+ * @psalm-type FilesTemplate = array{
+ * templateType: string,
+ * templateId: string,
+ * basename: string,
+ * etag: string,
+ * fileid: int,
+ * filename: string,
+ * lastmod: int,
+ * mime: string,
+ * size: int|float,
+ * type: string,
+ * hasPreview: bool,
+ * previewUrl: ?string,
+ * fields: list<FilesTemplateField>,
+ * }
+ *
+ * @psalm-type FilesTemplateFileCreator = array{
+ * app: string,
+ * label: string,
+ * extension: string,
+ * iconClass: ?string,
+ * iconSvgInline: ?string,
+ * mimetypes: list<string>,
+ * ratio: ?float,
+ * actionLabel: string,
+ * }
+ *
+ * @psalm-type FilesTemplateFileCreatorWithTemplates = FilesTemplateFileCreator&array{
+ * templates: list<FilesTemplate>,
+ * }
+ *
+ * @psalm-type FilesFolderTree = list<array{
+ * id: int,
+ * basename: string,
+ * displayName?: string,
+ * children: list<array{}>,
+ * }>
+ *
+ */
+class ResponseDefinitions {
+}
diff --git a/apps/files/lib/Search/FilesSearchProvider.php b/apps/files/lib/Search/FilesSearchProvider.php
index ba2d4bafa30..f71d58c6fae 100644
--- a/apps/files/lib/Search/FilesSearchProvider.php
+++ b/apps/files/lib/Search/FilesSearchProvider.php
@@ -3,73 +3,45 @@
declare(strict_types=1);
/**
- * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Search;
+use InvalidArgumentException;
+use OC\Files\Search\SearchBinaryOperator;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchOrder;
use OC\Files\Search\SearchQuery;
+use OC\Search\Filter\GroupFilter;
+use OC\Search\Filter\UserFilter;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
-use OCP\Files\Search\ISearchComparison;
use OCP\Files\Node;
+use OCP\Files\Search\ISearchComparison;
+use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\IL10N;
+use OCP\IPreview;
use OCP\IURLGenerator;
use OCP\IUser;
-use OCP\Search\IProvider;
+use OCP\Search\FilterDefinition;
+use OCP\Search\IFilter;
+use OCP\Search\IFilteringProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry;
+use OCP\Share\IShare;
-class FilesSearchProvider implements IProvider {
-
- /** @var IL10N */
- private $l10n;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
- /** @var IMimeTypeDetector */
- private $mimeTypeDetector;
-
- /** @var IRootFolder */
- private $rootFolder;
-
+class FilesSearchProvider implements IFilteringProvider {
public function __construct(
- IL10N $l10n,
- IURLGenerator $urlGenerator,
- IMimeTypeDetector $mimeTypeDetector,
- IRootFolder $rootFolder
+ private IL10N $l10n,
+ private IURLGenerator $urlGenerator,
+ private IMimeTypeDetector $mimeTypeDetector,
+ private IRootFolder $rootFolder,
+ private IPreview $previewManager,
) {
- $this->l10n = $l10n;
- $this->urlGenerator = $urlGenerator;
- $this->mimeTypeDetector = $mimeTypeDetector;
- $this->rootFolder = $rootFolder;
}
/**
@@ -97,26 +69,49 @@ class FilesSearchProvider implements IProvider {
return 5;
}
- /**
- * @inheritDoc
- */
+ public function getSupportedFilters(): array {
+ return [
+ 'term',
+ 'since',
+ 'until',
+ 'person',
+ 'min-size',
+ 'max-size',
+ 'mime',
+ 'type',
+ 'path',
+ 'is-favorite',
+ 'title-only',
+ ];
+ }
+
+ public function getAlternateIds(): array {
+ return [];
+ }
+
+ public function getCustomFilters(): array {
+ return [
+ new FilterDefinition('min-size', FilterDefinition::TYPE_INT),
+ new FilterDefinition('max-size', FilterDefinition::TYPE_INT),
+ new FilterDefinition('mime', FilterDefinition::TYPE_STRING),
+ new FilterDefinition('type', FilterDefinition::TYPE_STRING),
+ new FilterDefinition('path', FilterDefinition::TYPE_STRING),
+ new FilterDefinition('is-favorite', FilterDefinition::TYPE_BOOL),
+ ];
+ }
+
public function search(IUser $user, ISearchQuery $query): SearchResult {
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
- $fileQuery = new SearchQuery(
- new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query->getTerm() . '%'),
- $query->getLimit(),
- (int)$query->getCursor(),
- $query->getSortOrder() === ISearchQuery::SORT_DATE_DESC ? [
- new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime'),
- ] : [],
- $user
- );
-
+ $fileQuery = $this->buildSearchQuery($query, $user);
return SearchResult::paginated(
$this->l10n->t('Files'),
array_map(function (Node $result) use ($userFolder) {
- // Generate thumbnail url
- $thumbnailUrl = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->getId()]);
+ $thumbnailUrl = $this->previewManager->isMimeSupported($result->getMimetype())
+ ? $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->getId()])
+ : '';
+ $icon = $result->getMimetype() === FileInfo::MIMETYPE_FOLDER
+ ? 'icon-folder'
+ : $this->mimeTypeDetector->mimeTypeIcon($result->getMimetype());
$path = $userFolder->getRelativePath($result->getPath());
// Use shortened link to centralize the various
@@ -131,7 +126,7 @@ class FilesSearchProvider implements IProvider {
$result->getName(),
$this->formatSubline($path),
$this->urlGenerator->getAbsoluteURL($link),
- $result->getMimetype() === FileInfo::MIMETYPE_FOLDER ? 'icon-folder' : $this->mimeTypeDetector->mimeTypeIcon($result->getMimetype())
+ $icon,
);
$searchResultEntry->addAttribute('fileId', (string)$result->getId());
$searchResultEntry->addAttribute('path', $path);
@@ -141,6 +136,54 @@ class FilesSearchProvider implements IProvider {
);
}
+ private function buildSearchQuery(ISearchQuery $query, IUser $user): SearchQuery {
+ $comparisons = [];
+ foreach ($query->getFilters() as $name => $filter) {
+ $comparisons[] = match ($name) {
+ 'term' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $filter->get() . '%'),
+ 'since' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()),
+ 'until' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()),
+ 'min-size' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', $filter->get()),
+ 'max-size' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'size', $filter->get()),
+ 'mime' => new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $filter->get()),
+ 'type' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $filter->get() . '/%'),
+ 'path' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', 'files/' . ltrim($filter->get(), '/') . '%'),
+ 'person' => $this->buildPersonSearchQuery($filter),
+ default => throw new InvalidArgumentException('Unsupported comparison'),
+ };
+ }
+
+ return new SearchQuery(
+ new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, $comparisons),
+ $query->getLimit(),
+ (int)$query->getCursor(),
+ $query->getSortOrder() === ISearchQuery::SORT_DATE_DESC
+ ? [new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime')]
+ : [],
+ $user
+ );
+ }
+
+ private function buildPersonSearchQuery(IFilter $person): ISearchOperator {
+ if ($person instanceof UserFilter) {
+ return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_OR, [
+ new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [
+ new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getUID()),
+ new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_USER),
+ ]),
+ new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'owner', $person->get()->getUID()),
+ ]);
+ }
+ if ($person instanceof GroupFilter) {
+ return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [
+ new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getGID()),
+ new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_GROUP),
+ ]);
+ }
+
+ throw new InvalidArgumentException('Unsupported filter type');
+ }
+
/**
* Format subline for files
*
diff --git a/apps/files/lib/Service/ChunkedUploadConfig.php b/apps/files/lib/Service/ChunkedUploadConfig.php
new file mode 100644
index 00000000000..29661750f8b
--- /dev/null
+++ b/apps/files/lib/Service/ChunkedUploadConfig.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Service;
+
+use OCP\IConfig;
+use OCP\Server;
+
+class ChunkedUploadConfig {
+ private const KEY_MAX_SIZE = 'files.chunked_upload.max_size';
+ private const KEY_MAX_PARALLEL_COUNT = 'files.chunked_upload.max_parallel_count';
+
+ public static function getMaxChunkSize(): int {
+ return Server::get(IConfig::class)->getSystemValueInt(self::KEY_MAX_SIZE, 100 * 1024 * 1024);
+ }
+
+ public static function setMaxChunkSize(int $maxChunkSize): void {
+ Server::get(IConfig::class)->setSystemValue(self::KEY_MAX_SIZE, $maxChunkSize);
+ }
+
+ public static function getMaxParallelCount(): int {
+ return Server::get(IConfig::class)->getSystemValueInt(self::KEY_MAX_PARALLEL_COUNT, 5);
+ }
+}
diff --git a/apps/files/lib/Service/DirectEditingService.php b/apps/files/lib/Service/DirectEditingService.php
index 1ee3a53d85a..3d756ee56fa 100644
--- a/apps/files/lib/Service/DirectEditingService.php
+++ b/apps/files/lib/Service/DirectEditingService.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Service;
@@ -32,14 +15,10 @@ use OCP\EventDispatcher\IEventDispatcher;
class DirectEditingService {
- /** @var IManager */
- private $directEditingManager;
- /** @var IEventDispatcher */
- private $eventDispatcher;
-
- public function __construct(IEventDispatcher $eventDispatcher, IManager $directEditingManager) {
- $this->directEditingManager = $directEditingManager;
- $this->eventDispatcher = $eventDispatcher;
+ public function __construct(
+ private IEventDispatcher $eventDispatcher,
+ private IManager $directEditingManager,
+ ) {
}
public function getDirectEditingETag(): string {
diff --git a/apps/files/lib/Service/LivePhotosService.php b/apps/files/lib/Service/LivePhotosService.php
new file mode 100644
index 00000000000..3ac6601d5dc
--- /dev/null
+++ b/apps/files/lib/Service/LivePhotosService.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Service;
+
+use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
+use OCP\FilesMetadata\IFilesMetadataManager;
+
+class LivePhotosService {
+ public function __construct(
+ private IFilesMetadataManager $filesMetadataManager,
+ ) {
+ }
+
+ /**
+ * Get the associated live photo for a given file id
+ */
+ public function getLivePhotoPeerId(int $fileId): ?int {
+ try {
+ $metadata = $this->filesMetadataManager->getMetadata($fileId);
+ } catch (FilesMetadataNotFoundException $ex) {
+ return null;
+ }
+
+ if (!$metadata->hasKey('files-live-photo')) {
+ return null;
+ }
+
+ return (int)$metadata->getString('files-live-photo');
+ }
+}
diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php
index 661a7e66e10..afef5d2093d 100644
--- a/apps/files/lib/Service/OwnershipTransferService.php
+++ b/apps/files/lib/Service/OwnershipTransferService.php
@@ -3,47 +3,33 @@
declare(strict_types=1);
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sascha Wiswedel <sascha.wiswedel@nextcloud.com>
- * @author Tobia De Koninck <LEDfan@users.noreply.github.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Service;
use Closure;
+use Exception;
use OC\Files\Filesystem;
use OC\Files\View;
+use OC\User\NoUserException;
+use OCA\Encryption\Util;
use OCA\Files\Exception\TransferOwnershipException;
+use OCA\Files_External\Config\ConfigAdapter;
use OCP\Encryption\IManager as IEncryptionManager;
+use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Config\IUserMountCache;
+use OCP\Files\File;
use OCP\Files\FileInfo;
-use OCP\Files\IHomeStorage;
use OCP\Files\InvalidPathException;
+use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
+use OCP\Files\NotFoundException;
use OCP\IUser;
use OCP\IUserManager;
+use OCP\L10N\IFactory;
+use OCP\Server;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
use Symfony\Component\Console\Helper\ProgressBar;
@@ -58,31 +44,15 @@ use function rtrim;
class OwnershipTransferService {
- /** @var IEncryptionManager */
- private $encryptionManager;
-
- /** @var IShareManager */
- private $shareManager;
-
- /** @var IMountManager */
- private $mountManager;
-
- /** @var IUserMountCache */
- private $userMountCache;
-
- /** @var IUserManager */
- private $userManager;
-
- public function __construct(IEncryptionManager $manager,
- IShareManager $shareManager,
- IMountManager $mountManager,
- IUserMountCache $userMountCache,
- IUserManager $userManager) {
- $this->encryptionManager = $manager;
- $this->shareManager = $shareManager;
- $this->mountManager = $mountManager;
- $this->userMountCache = $userMountCache;
- $this->userManager = $userManager;
+ public function __construct(
+ private IEncryptionManager $encryptionManager,
+ private IShareManager $shareManager,
+ private IMountManager $mountManager,
+ private IUserMountCache $userMountCache,
+ private IUserManager $userManager,
+ private IFactory $l10nFactory,
+ private IRootFolder $rootFolder,
+ ) {
}
/**
@@ -93,15 +63,17 @@ class OwnershipTransferService {
* @param OutputInterface|null $output
* @param bool $move
* @throws TransferOwnershipException
- * @throws \OC\User\NoUserException
+ * @throws NoUserException
*/
- public function transfer(IUser $sourceUser,
- IUser $destinationUser,
- string $path,
- ?OutputInterface $output = null,
- bool $move = false,
- bool $firstLogin = false,
- bool $transferIncomingShares = false): void {
+ public function transfer(
+ IUser $sourceUser,
+ IUser $destinationUser,
+ string $path,
+ ?OutputInterface $output = null,
+ bool $move = false,
+ bool $firstLogin = false,
+ bool $includeExternalStorage = false,
+ ): void {
$output = $output ?? new NullOutput();
$sourceUid = $sourceUser->getUID();
$destinationUid = $destinationUser->getUID();
@@ -110,12 +82,17 @@ class OwnershipTransferService {
// If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready
if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0)
|| !$this->encryptionManager->isReadyForUser($destinationUid)) {
- throw new TransferOwnershipException("The target user is not ready to accept files. The user has at least to have logged in once.", 2);
+ throw new TransferOwnershipException('The target user is not ready to accept files. The user has at least to have logged in once.', 2);
}
// setup filesystem
// Requesting the user folder will set it up if the user hasn't logged in before
- \OC::$server->getUserFolder($destinationUser->getUID());
+ // We need a setupFS for the full filesystem setup before as otherwise we will just return
+ // a lazy root folder which does not create the destination users folder
+ \OC_Util::setupFS($sourceUser->getUID());
+ \OC_Util::setupFS($destinationUser->getUID());
+ $this->rootFolder->getUserFolder($sourceUser->getUID());
+ $this->rootFolder->getUserFolder($destinationUser->getUID());
Filesystem::initMountPoints($sourceUid);
Filesystem::initMountPoints($destinationUid);
@@ -124,19 +101,15 @@ class OwnershipTransferService {
if ($move) {
$finalTarget = "$destinationUid/files/";
} else {
+ $l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser));
$date = date('Y-m-d H-i-s');
- // Remove some characters which are prone to cause errors
- $cleanUserName = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $sourceUser->getDisplayName());
- // Replace multiple dashes with one dash
- $cleanUserName = preg_replace('/-{2,}/s', '-', $cleanUserName);
- $cleanUserName = $cleanUserName ?: $sourceUid;
-
- $finalTarget = "$destinationUid/files/transferred from $cleanUserName on $date";
+ $cleanUserName = $this->sanitizeFolderName($sourceUser->getDisplayName()) ?: $sourceUid;
+ $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
try {
$view->verifyPath(dirname($finalTarget), basename($finalTarget));
} catch (InvalidPathException $e) {
- $finalTarget = "$destinationUid/files/transferred from $sourceUid on $date";
+ $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
}
}
@@ -150,7 +123,7 @@ class OwnershipTransferService {
}
if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) {
- throw new TransferOwnershipException("Destination path does not exists or is not empty", 1);
+ throw new TransferOwnershipException('Destination path does not exists or is not empty', 1);
}
@@ -171,49 +144,62 @@ class OwnershipTransferService {
$sourcePath
);
+ $sourceSize = $view->getFileInfo($sourcePath)->getSize();
+
// transfer the files
$this->transferFiles(
$sourceUid,
$sourcePath,
$finalTarget,
$view,
- $output
+ $output,
+ $includeExternalStorage,
);
+ $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize();
+ // transfer the incoming shares
+ $sourceShares = $this->collectIncomingShares(
+ $sourceUid,
+ $output,
+ $sourcePath,
+ );
+ $destinationShares = $this->collectIncomingShares(
+ $destinationUid,
+ $output,
+ null,
+ );
+ $this->transferIncomingShares(
+ $sourceUid,
+ $destinationUid,
+ $sourceShares,
+ $destinationShares,
+ $output,
+ $path,
+ $finalTarget,
+ $move
+ );
+
+ $destinationPath = $finalTarget . '/' . $path;
// restore the shares
$this->restoreShares(
$sourceUid,
$destinationUid,
+ $destinationPath,
$shares,
$output
);
-
- // transfer the incoming shares
- if ($transferIncomingShares === true) {
- $sourceShares = $this->collectIncomingShares(
- $sourceUid,
- $output,
- $view
- );
- $destinationShares = $this->collectIncomingShares(
- $destinationUid,
- $output,
- $view,
- true
- );
- $this->transferIncomingShares(
- $sourceUid,
- $destinationUid,
- $sourceShares,
- $destinationShares,
- $output,
- $path,
- $finalTarget,
- $move
- );
+ if ($sizeDifference !== 0) {
+ $output->writeln("Transferred folder have a size difference of: $sizeDifference Bytes which means the transfer may be incomplete. Please check the logs if there was any issue during the transfer operation.");
}
}
+ private function sanitizeFolderName(string $name): string {
+ // Remove some characters which are prone to cause errors
+ $name = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $name);
+ // Replace multiple dashes with one dash
+ return preg_replace('/-{2,}/s', '-', $name);
+ }
+
private function walkFiles(View $view, $path, Closure $callBack) {
foreach ($view->getDirectoryContent($path) as $fileInfo) {
if (!$callBack($fileInfo)) {
@@ -228,85 +214,130 @@ class OwnershipTransferService {
/**
* @param OutputInterface $output
*
- * @throws \Exception
+ * @throws TransferOwnershipException
*/
- protected function analyse(string $sourceUid,
- string $destinationUid,
- string $sourcePath,
- View $view,
- OutputInterface $output): void {
+ protected function analyse(
+ string $sourceUid,
+ string $destinationUid,
+ string $sourcePath,
+ View $view,
+ OutputInterface $output,
+ bool $includeExternalStorage = false,
+ ): void {
$output->writeln('Validating quota');
- $size = $view->getFileInfo($sourcePath, false)->getSize(false);
+ $sourceFileInfo = $view->getFileInfo($sourcePath, false);
+ if ($sourceFileInfo === false) {
+ throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1);
+ }
+ $size = $sourceFileInfo->getSize(false);
$freeSpace = $view->free_space($destinationUid . '/files/');
if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) {
- $output->writeln('<error>Target user does not have enough free space available.</error>');
- throw new \Exception('Execution terminated.');
+ throw new TransferOwnershipException('Target user does not have enough free space available.', 1);
}
$output->writeln("Analysing files of $sourceUid ...");
$progress = new ProgressBar($output);
$progress->start();
+ if ($this->encryptionManager->isEnabled()) {
+ $masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled();
+ } else {
+ $masterKeyEnabled = false;
+ }
$encryptedFiles = [];
- $this->walkFiles($view, $sourcePath,
- function (FileInfo $fileInfo) use ($progress) {
- if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
- // only analyze into folders from main storage,
- if (!$fileInfo->getStorage()->instanceOfStorage(IHomeStorage::class)) {
- return false;
- }
- return true;
- }
- $progress->advance();
- if ($fileInfo->isEncrypted()) {
- $encryptedFiles[] = $fileInfo;
- }
- return true;
- });
+ if ($sourceFileInfo->getType() === FileInfo::TYPE_FOLDER) {
+ if ($sourceFileInfo->isEncrypted()) {
+ /* Encrypted folder means e2ee encrypted */
+ $encryptedFiles[] = $sourceFileInfo;
+ } else {
+ $this->walkFiles($view, $sourcePath,
+ function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
+ if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
+ $mount = $fileInfo->getMountPoint();
+ // only analyze into folders from main storage,
+ if (
+ $mount->getMountProvider() instanceof IHomeMountProvider
+ || ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter)
+ ) {
+ if ($fileInfo->isEncrypted()) {
+ /* Encrypted folder means e2ee encrypted, we cannot transfer it */
+ $encryptedFiles[] = $fileInfo;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ $progress->advance();
+ if ($fileInfo->isEncrypted() && !$masterKeyEnabled) {
+ /* Encrypted file means SSE, we can only transfer it if master key is enabled */
+ $encryptedFiles[] = $fileInfo;
+ }
+ return true;
+ });
+ }
+ } elseif ($sourceFileInfo->isEncrypted() && !$masterKeyEnabled) {
+ /* Encrypted file means SSE, we can only transfer it if master key is enabled */
+ $encryptedFiles[] = $sourceFileInfo;
+ }
$progress->finish();
$output->writeln('');
// no file is allowed to be encrypted
if (!empty($encryptedFiles)) {
- $output->writeln("<error>Some files are encrypted - please decrypt them first.</error>");
+ $output->writeln('<error>Some files are encrypted - please decrypt them first.</error>');
foreach ($encryptedFiles as $encryptedFile) {
/** @var FileInfo $encryptedFile */
- $output->writeln(" " . $encryptedFile->getPath());
+ $output->writeln(' ' . $encryptedFile->getPath());
}
- throw new \Exception('Execution terminated.');
+ throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
}
}
- private function collectUsersShares(string $sourceUid,
- OutputInterface $output,
- View $view,
- string $path): array {
+ /**
+ * @return array<array{share: IShare, suffix: string}>
+ */
+ private function collectUsersShares(
+ string $sourceUid,
+ OutputInterface $output,
+ View $view,
+ string $path,
+ ): array {
$output->writeln("Collecting all share information for files and folders of $sourceUid ...");
$shares = [];
$progress = new ProgressBar($output);
- foreach ([IShare::TYPE_GROUP, IShare::TYPE_USER, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_ROOM, IShare::TYPE_EMAIL, IShare::TYPE_CIRCLE, IShare::TYPE_DECK] as $shareType) {
+ $normalizedPath = Filesystem::normalizePath($path);
+
+ $supportedShareTypes = [
+ IShare::TYPE_GROUP,
+ IShare::TYPE_USER,
+ IShare::TYPE_LINK,
+ IShare::TYPE_REMOTE,
+ IShare::TYPE_ROOM,
+ IShare::TYPE_EMAIL,
+ IShare::TYPE_CIRCLE,
+ IShare::TYPE_DECK,
+ IShare::TYPE_SCIENCEMESH,
+ ];
+
+ foreach ($supportedShareTypes as $shareType) {
$offset = 0;
while (true) {
- $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset);
+ $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false);
$progress->advance(count($sharePage));
if (empty($sharePage)) {
break;
}
if ($path !== "$sourceUid/files") {
- $sharePage = array_filter($sharePage, function (IShare $share) use ($view, $path) {
+ $sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
try {
- $relativePath = $view->getPath($share->getNodeId());
- $singleFileTranfer = $view->is_file($path);
- if ($singleFileTranfer) {
- return Filesystem::normalizePath($relativePath) === Filesystem::normalizePath($path);
- }
+ $sourceNode = $share->getNode();
+ $relativePath = $view->getRelativePath($sourceNode->getPath());
- return mb_strpos(
- Filesystem::normalizePath($relativePath . '/', false),
- Filesystem::normalizePath($path . '/', false)) === 0;
- } catch (\Exception $e) {
+ return str_starts_with($relativePath . '/', $normalizedPath . '/');
+ } catch (Exception $e) {
return false;
}
});
@@ -318,17 +349,32 @@ class OwnershipTransferService {
$progress->finish();
$output->writeln('');
- return $shares;
+
+ return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
+ try {
+ $nodePath = $view->getRelativePath($share->getNode()->getPath());
+ } catch (NotFoundException $e) {
+ $output->writeln("<error>Failed to find path for shared file {$share->getNodeId()} for user $sourceUid, skipping</error>");
+ return null;
+ }
+
+ return [
+ 'share' => $share,
+ 'suffix' => substr(Filesystem::normalizePath($nodePath), strlen($normalizedPath)),
+ ];
+ }, $shares)));
}
- private function collectIncomingShares(string $sourceUid,
- OutputInterface $output,
- View $view,
- bool $addKeys = false): array {
+ private function collectIncomingShares(
+ string $sourceUid,
+ OutputInterface $output,
+ ?string $path,
+ ): array {
$output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");
$shares = [];
$progress = new ProgressBar($output);
+ $normalizedPath = Filesystem::normalizePath($path);
$offset = 0;
while (true) {
@@ -337,14 +383,19 @@ class OwnershipTransferService {
if (empty($sharePage)) {
break;
}
- if ($addKeys) {
- foreach ($sharePage as $singleShare) {
- $shares[$singleShare->getNodeId()] = $singleShare;
- }
- } else {
- foreach ($sharePage as $singleShare) {
- $shares[] = $singleShare;
- }
+
+ if ($path !== null && $path !== "$sourceUid/files") {
+ $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
+ try {
+ return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
+ } catch (Exception) {
+ return false;
+ }
+ });
+ }
+
+ foreach ($sharePage as $share) {
+ $shares[$share->getNodeId()] = $share;
}
$offset += 50;
@@ -359,11 +410,14 @@ class OwnershipTransferService {
/**
* @throws TransferOwnershipException
*/
- protected function transferFiles(string $sourceUid,
- string $sourcePath,
- string $finalTarget,
- View $view,
- OutputInterface $output): void {
+ protected function transferFiles(
+ string $sourceUid,
+ string $sourcePath,
+ string $finalTarget,
+ View $view,
+ OutputInterface $output,
+ bool $includeExternalStorage,
+ ): void {
$output->writeln("Transferring files to $finalTarget ...");
// This change will help user to transfer the folder specified using --path option.
@@ -372,26 +426,69 @@ class OwnershipTransferService {
$view->mkdir($finalTarget);
$finalTarget = $finalTarget . '/' . basename($sourcePath);
}
- if ($view->rename($sourcePath, $finalTarget) === false) {
- throw new TransferOwnershipException("Could not transfer files.", 1);
+ $sourceInfo = $view->getFileInfo($sourcePath);
+
+ /// handle the external storages mounted at the root, or the admin specifying an external storage with --path
+ if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) {
+ $this->moveMountContents($view, $sourcePath, $finalTarget);
+ } else {
+ if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
+ throw new TransferOwnershipException('Could not transfer files.', 1);
+ }
+ }
+
+ if ($includeExternalStorage) {
+ $nestedMounts = $this->mountManager->findIn($sourcePath);
+ foreach ($nestedMounts as $mount) {
+ if ($mount->getMountProvider() === ConfigAdapter::class) {
+ $relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath));
+ $this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath);
+ }
+ }
}
+
if (!is_dir("$sourceUid/files")) {
// because the files folder is moved away we need to recreate it
$view->mkdir("$sourceUid/files");
}
}
- private function restoreShares(string $sourceUid,
- string $destinationUid,
- array $shares,
- OutputInterface $output) {
- $output->writeln("Restoring shares ...");
+ private function moveMountContents(View $rootView, string $source, string $target) {
+ if ($rootView->copy($source, $target)) {
+ // just doing `rmdir` on the mountpoint would cause it to try and unmount the storage
+ // we need to empty the contents instead
+ $content = $rootView->getDirectoryContent($source);
+ foreach ($content as $item) {
+ if ($item->getType() === FileInfo::TYPE_FOLDER) {
+ $rootView->rmdir($item->getPath());
+ } else {
+ $rootView->unlink($item->getPath());
+ }
+ }
+ } else {
+ throw new TransferOwnershipException("Could not transfer $source to $target");
+ }
+ }
+
+ /**
+ * @param string $targetLocation New location of the transfered node
+ * @param array<array{share: IShare, suffix: string}> $shares previously collected share information
+ */
+ private function restoreShares(
+ string $sourceUid,
+ string $destinationUid,
+ string $targetLocation,
+ array $shares,
+ OutputInterface $output,
+ ):void {
+ $output->writeln('Restoring shares ...');
$progress = new ProgressBar($output, count($shares));
- foreach ($shares as $share) {
+ foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
try {
- if ($share->getShareType() === IShare::TYPE_USER &&
- $share->getSharedWith() === $destinationUid) {
+ $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
+ if ($share->getShareType() === IShare::TYPE_USER
+ && $share->getSharedWith() === $destinationUid) {
// Unmount the shares before deleting, so we don't try to get the storage later on.
$shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
if ($shareMountPoint) {
@@ -406,8 +503,8 @@ class OwnershipTransferService {
$share->setSharedBy($destinationUid);
}
- if ($share->getShareType() === IShare::TYPE_USER &&
- !$this->userManager->userExists($share->getSharedWith())) {
+ if ($share->getShareType() === IShare::TYPE_USER
+ && !$this->userManager->userExists($share->getSharedWith())) {
// stray share with deleted user
$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>');
$this->shareManager->deleteShare($share);
@@ -416,12 +513,25 @@ class OwnershipTransferService {
// trigger refetching of the node so that the new owner and mountpoint are taken into account
// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
$this->userMountCache->clear();
- $share->setNodeId($share->getNode()->getId());
- $this->shareManager->updateShare($share);
+ try {
+ // Try to get the "old" id.
+ // Normally the ID is preserved,
+ // but for transferes between different storages the ID might change
+ $newNodeId = $share->getNode()->getId();
+ } catch (NotFoundException) {
+ // ID has changed due to transfer between different storages
+ // Try to get the new ID from the target path and suffix of the share
+ $node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
+ $newNodeId = $node->getId();
+ $output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
+ }
+ $share->setNodeId($newNodeId);
+
+ $this->shareManager->updateShare($share, onlyValid: false);
}
}
- } catch (\OCP\Files\NotFoundException $e) {
+ } catch (NotFoundException $e) {
$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
} catch (\Throwable $e) {
$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
@@ -433,18 +543,18 @@ class OwnershipTransferService {
}
private function transferIncomingShares(string $sourceUid,
- string $destinationUid,
- array $sourceShares,
- array $destinationShares,
- OutputInterface $output,
- string $path,
- string $finalTarget,
- bool $move): void {
- $output->writeln("Restoring incoming shares ...");
+ string $destinationUid,
+ array $sourceShares,
+ array $destinationShares,
+ OutputInterface $output,
+ string $path,
+ string $finalTarget,
+ bool $move): void {
+ $output->writeln('Restoring incoming shares ...');
$progress = new ProgressBar($output, count($sourceShares));
$prefix = "$destinationUid/files";
$finalShareTarget = '';
- if (substr($finalTarget, 0, strlen($prefix)) === $prefix) {
+ if (str_starts_with($finalTarget, $prefix)) {
$finalShareTarget = substr($finalTarget, strlen($prefix));
}
foreach ($sourceShares as $share) {
@@ -454,13 +564,13 @@ class OwnershipTransferService {
if (trim($path, '/') !== '') {
$pathToCheck = '/' . trim($path) . '/';
}
- if (substr($share->getTarget(), 0, strlen($pathToCheck)) !== $pathToCheck) {
+ if (!str_starts_with($share->getTarget(), $pathToCheck)) {
continue;
}
$shareTarget = $share->getTarget();
$shareTarget = $finalShareTarget . $shareTarget;
- if ($share->getShareType() === IShare::TYPE_USER &&
- $share->getSharedBy() === $destinationUid) {
+ if ($share->getShareType() === IShare::TYPE_USER
+ && $share->getSharedBy() === $destinationUid) {
$this->shareManager->deleteShare($share);
} elseif (isset($destinationShares[$share->getNodeId()])) {
$destinationShare = $destinationShares[$share->getNodeId()];
@@ -501,7 +611,7 @@ class OwnershipTransferService {
$this->shareManager->moveShare($share, $destinationUid);
continue;
}
- } catch (\OCP\Files\NotFoundException $e) {
+ } catch (NotFoundException $e) {
$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
} catch (\Throwable $e) {
$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
diff --git a/apps/files/lib/Service/SettingsService.php b/apps/files/lib/Service/SettingsService.php
new file mode 100644
index 00000000000..d07e907a5f6
--- /dev/null
+++ b/apps/files/lib/Service/SettingsService.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Service;
+
+use OC\Files\FilenameValidator;
+use OCP\IConfig;
+use Psr\Log\LoggerInterface;
+
+class SettingsService {
+
+ protected const WINDOWS_EXTENSION = [
+ ' ',
+ '.',
+ ];
+
+ protected const WINDOWS_BASENAMES = [
+ 'con', 'prn', 'aux', 'nul', 'com0', 'com1', 'com2', 'com3', 'com4', 'com5',
+ 'com6', 'com7', 'com8', 'com9', 'com¹', 'com²', 'com³', 'lpt0', 'lpt1', 'lpt2',
+ 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9', 'lpt¹', 'lpt²', 'lpt³',
+ ];
+
+ protected const WINDOWS_CHARACTERS = [
+ '<', '>', ':',
+ '"', '|', '?',
+ '*',
+ ];
+
+ public function __construct(
+ private IConfig $config,
+ private FilenameValidator $filenameValidator,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function hasFilesWindowsSupport(): bool {
+ return empty(array_diff(self::WINDOWS_BASENAMES, $this->filenameValidator->getForbiddenBasenames()))
+ && empty(array_diff(self::WINDOWS_CHARACTERS, $this->filenameValidator->getForbiddenCharacters()))
+ && empty(array_diff(self::WINDOWS_EXTENSION, $this->filenameValidator->getForbiddenExtensions()));
+ }
+
+ public function setFilesWindowsSupport(bool $enabled = true): void {
+ if ($enabled) {
+ $basenames = array_unique(array_merge(self::WINDOWS_BASENAMES, $this->filenameValidator->getForbiddenBasenames()));
+ $characters = array_unique(array_merge(self::WINDOWS_CHARACTERS, $this->filenameValidator->getForbiddenCharacters()));
+ $extensions = array_unique(array_merge(self::WINDOWS_EXTENSION, $this->filenameValidator->getForbiddenExtensions()));
+ } else {
+ $basenames = array_unique(array_values(array_diff($this->filenameValidator->getForbiddenBasenames(), self::WINDOWS_BASENAMES)));
+ $characters = array_unique(array_values(array_diff($this->filenameValidator->getForbiddenCharacters(), self::WINDOWS_CHARACTERS)));
+ $extensions = array_unique(array_values(array_diff($this->filenameValidator->getForbiddenExtensions(), self::WINDOWS_EXTENSION)));
+ }
+ $values = [
+ 'forbidden_filename_basenames' => empty($basenames) ? null : $basenames,
+ 'forbidden_filename_characters' => empty($characters) ? null : $characters,
+ 'forbidden_filename_extensions' => empty($extensions) ? null : $extensions,
+ ];
+ $this->config->setSystemValues($values);
+ }
+}
diff --git a/apps/files/lib/Service/TagService.php b/apps/files/lib/Service/TagService.php
index 29b6fbc2840..63c54d01fd0 100644
--- a/apps/files/lib/Service/TagService.php
+++ b/apps/files/lib/Service/TagService.php
@@ -1,73 +1,29 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files\Service;
-use OCA\Files\Activity\FavoriteProvider;
use OCP\Activity\IManager;
use OCP\Files\Folder;
+use OCP\Files\NotFoundException;
use OCP\ITags;
-use OCP\IUser;
use OCP\IUserSession;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
/**
* Service class to manage tags on files.
*/
class TagService {
- /** @var IUserSession */
- private $userSession;
- /** @var IManager */
- private $activityManager;
- /** @var ITags */
- private $tagger;
- /** @var Folder */
- private $homeFolder;
- /** @var EventDispatcherInterface */
- private $dispatcher;
-
- /**
- * @param IUserSession $userSession
- * @param IManager $activityManager
- * @param ITags $tagger
- * @param Folder $homeFolder
- * @param EventDispatcherInterface $dispatcher
- */
public function __construct(
- IUserSession $userSession,
- IManager $activityManager,
- ITags $tagger,
- Folder $homeFolder,
- EventDispatcherInterface $dispatcher
+ private IUserSession $userSession,
+ private IManager $activityManager,
+ private ?ITags $tagger,
+ private ?Folder $homeFolder,
) {
- $this->userSession = $userSession;
- $this->activityManager = $activityManager;
- $this->tagger = $tagger;
- $this->homeFolder = $homeFolder;
- $this->dispatcher = $dispatcher;
}
/**
@@ -76,11 +32,18 @@ class TagService {
* replace the actual tag selection.
*
* @param string $path path
- * @param array $tags array of tags
+ * @param array $tags array of tags
* @return array list of tags
- * @throws \OCP\Files\NotFoundException if the file does not exist
+ * @throws NotFoundException if the file does not exist
*/
public function updateFileTags($path, $tags) {
+ if ($this->tagger === null) {
+ throw new \RuntimeException('No tagger set');
+ }
+ if ($this->homeFolder === null) {
+ throw new \RuntimeException('No homeFolder set');
+ }
+
$fileId = $this->homeFolder->get($path)->getId();
$currentTags = $this->tagger->getTagsForObjects([$fileId]);
@@ -91,16 +54,10 @@ class TagService {
$newTags = array_diff($tags, $currentTags);
foreach ($newTags as $tag) {
- if ($tag === ITags::TAG_FAVORITE) {
- $this->addActivity(true, $fileId, $path);
- }
$this->tagger->tagAs($fileId, $tag);
}
$deletedTags = array_diff($currentTags, $tags);
foreach ($deletedTags as $tag) {
- if ($tag === ITags::TAG_FAVORITE) {
- $this->addActivity(false, $fileId, $path);
- }
$this->tagger->unTag($fileId, $tag);
}
@@ -108,40 +65,4 @@ class TagService {
// list is up to date, in case of concurrent changes ?
return $tags;
}
-
- /**
- * @param bool $addToFavorite
- * @param int $fileId
- * @param string $path
- */
- protected function addActivity($addToFavorite, $fileId, $path) {
- $user = $this->userSession->getUser();
- if (!$user instanceof IUser) {
- return;
- }
-
- $eventName = $addToFavorite ? 'addFavorite' : 'removeFavorite';
- $this->dispatcher->dispatch(self::class . '::' . $eventName, new GenericEvent(null, [
- 'userId' => $user->getUID(),
- 'fileId' => $fileId,
- 'path' => $path,
- ]));
-
- $event = $this->activityManager->generateEvent();
- try {
- $event->setApp('files')
- ->setObject('files', $fileId, $path)
- ->setType('favorite')
- ->setAuthor($user->getUID())
- ->setAffectedUser($user->getUID())
- ->setTimestamp(time())
- ->setSubject(
- $addToFavorite ? FavoriteProvider::SUBJECT_ADDED : FavoriteProvider::SUBJECT_REMOVED,
- ['id' => $fileId, 'path' => $path]
- );
- $this->activityManager->publish($event);
- } catch (\InvalidArgumentException $e) {
- } catch (\BadMethodCallException $e) {
- }
- }
}
diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php
index e405b02c07a..dcf30b7796d 100644
--- a/apps/files/lib/Service/UserConfig.php
+++ b/apps/files/lib/Service/UserConfig.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Service;
@@ -28,7 +12,7 @@ use OCP\IUser;
use OCP\IUserSession;
class UserConfig {
- const ALLOWED_CONFIGS = [
+ public const ALLOWED_CONFIGS = [
[
// Whether to crop the files previews or not in the files list
'key' => 'crop_image_previews',
@@ -36,18 +20,72 @@ class UserConfig {
'allowed' => [true, false],
],
[
+ // The view to start the files app in
+ 'key' => 'default_view',
+ 'default' => 'files',
+ 'allowed' => ['files', 'personal'],
+ ],
+ [
+ // Whether to show the folder tree
+ 'key' => 'folder_tree',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
+ [
+ // Whether to show the files list in grid view or not
+ 'key' => 'grid_view',
+ 'default' => false,
+ 'allowed' => [true, false],
+ ],
+ [
+ // Whether to show the "confirm file deletion" warning
+ 'key' => 'show_dialog_deletion',
+ 'default' => false,
+ 'allowed' => [true, false],
+ ],
+ [
+ // Whether to show the "confirm file extension change" warning
+ 'key' => 'show_dialog_file_extension',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
+ [
+ // Whether to show the files extensions in the files list or not
+ 'key' => 'show_files_extensions',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
+ [
// Whether to show the hidden files or not in the files list
'key' => 'show_hidden',
'default' => false,
'allowed' => [true, false],
],
+ [
+ // Whether to show the mime column or not
+ 'key' => 'show_mime_column',
+ 'default' => false,
+ 'allowed' => [true, false],
+ ],
+ [
+ // Whether to sort favorites first in the list or not
+ 'key' => 'sort_favorites_first',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
+ [
+ // Whether to sort folders before files in the list or not
+ 'key' => 'sort_folders_first',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
];
-
- protected IConfig $config;
protected ?IUser $user = null;
- public function __construct(IConfig $config, IUserSession $userSession) {
- $this->config = $config;
+ public function __construct(
+ protected IConfig $config,
+ IUserSession $userSession,
+ ) {
$this->user = $userSession->getUser();
}
@@ -56,7 +94,7 @@ class UserConfig {
* @return string[]
*/
public function getAllowedConfigKeys(): array {
- return array_map(function($config) {
+ return array_map(function ($config) {
return $config['key'];
}, self::ALLOWED_CONFIGS);
}
@@ -107,7 +145,7 @@ class UserConfig {
if (!in_array($key, $this->getAllowedConfigKeys())) {
throw new \InvalidArgumentException('Unknown config key');
}
-
+
if (!in_array($value, $this->getAllowedConfigValues($key))) {
throw new \InvalidArgumentException('Invalid config value');
}
@@ -130,10 +168,10 @@ class UserConfig {
}
$userId = $this->user->getUID();
- $userConfigs = array_map(function(string $key) use ($userId) {
+ $userConfigs = array_map(function (string $key) use ($userId) {
$value = $this->config->getUserValue($userId, Application::APP_ID, $key, $this->getDefaultConfigValue($key));
// If the default is expected to be a boolean, we need to cast the value
- if (is_bool($this->getDefaultConfigValue($key))) {
+ if (is_bool($this->getDefaultConfigValue($key)) && is_string($value)) {
return $value === '1';
}
return $value;
diff --git a/apps/files/lib/Service/ViewConfig.php b/apps/files/lib/Service/ViewConfig.php
new file mode 100644
index 00000000000..cf8bebd5372
--- /dev/null
+++ b/apps/files/lib/Service/ViewConfig.php
@@ -0,0 +1,168 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Service;
+
+use OCA\Files\AppInfo\Application;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserSession;
+
+class ViewConfig {
+ public const CONFIG_KEY = 'files_views_configs';
+ public const ALLOWED_CONFIGS = [
+ [
+ // The default sorting key for the files list view
+ 'key' => 'sorting_mode',
+ // null by default as views can provide default sorting key
+ // and will fallback to it if user hasn't change it
+ 'default' => null,
+ ],
+ [
+ // The default sorting direction for the files list view
+ 'key' => 'sorting_direction',
+ 'default' => 'asc',
+ 'allowed' => ['asc', 'desc'],
+ ],
+ [
+ // If the navigation entry for this view is expanded or not
+ 'key' => 'expanded',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
+ ];
+ protected ?IUser $user = null;
+
+ public function __construct(
+ protected IConfig $config,
+ IUserSession $userSession,
+ ) {
+ $this->user = $userSession->getUser();
+ }
+
+ /**
+ * Get the list of all allowed user config keys
+ * @return string[]
+ */
+ public function getAllowedConfigKeys(): array {
+ return array_map(function ($config) {
+ return $config['key'];
+ }, self::ALLOWED_CONFIGS);
+ }
+
+ /**
+ * Get the list of allowed config values for a given key
+ *
+ * @param string $key a valid config key
+ * @return array
+ */
+ private function getAllowedConfigValues(string $key): array {
+ foreach (self::ALLOWED_CONFIGS as $config) {
+ if ($config['key'] === $key) {
+ return $config['allowed'] ?? [];
+ }
+ }
+ return [];
+ }
+
+ /**
+ * Get the default config value for a given key
+ *
+ * @param string $key a valid config key
+ * @return string|bool|null
+ */
+ private function getDefaultConfigValue(string $key) {
+ foreach (self::ALLOWED_CONFIGS as $config) {
+ if ($config['key'] === $key) {
+ return $config['default'];
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Set a user config
+ *
+ * @param string $view
+ * @param string $key
+ * @param string|bool $value
+ * @throws \Exception
+ * @throws \InvalidArgumentException
+ */
+ public function setConfig(string $view, string $key, $value): void {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ if (!$view) {
+ throw new \Exception('Unknown view');
+ }
+
+ if (!in_array($key, $this->getAllowedConfigKeys())) {
+ throw new \InvalidArgumentException('Unknown config key');
+ }
+
+ if (!in_array($value, $this->getAllowedConfigValues($key))
+ && !empty($this->getAllowedConfigValues($key))) {
+ throw new \InvalidArgumentException('Invalid config value');
+ }
+
+ // Cast boolean values
+ if (is_bool($this->getDefaultConfigValue($key))) {
+ $value = $value === '1';
+ }
+
+ $config = $this->getConfigs();
+ $config[$view][$key] = $value;
+
+ $this->config->setUserValue($this->user->getUID(), Application::APP_ID, self::CONFIG_KEY, json_encode($config));
+ }
+
+ /**
+ * Get the current user configs array for a given view
+ *
+ * @return array
+ */
+ public function getConfig(string $view): array {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ $userId = $this->user->getUID();
+ $configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true);
+
+ if (!isset($configs[$view])) {
+ $configs[$view] = [];
+ }
+
+ // Extend undefined values with defaults
+ return array_reduce(self::ALLOWED_CONFIGS, function ($carry, $config) use ($view, $configs) {
+ $key = $config['key'];
+ $carry[$key] = $configs[$view][$key] ?? $this->getDefaultConfigValue($key);
+ return $carry;
+ }, []);
+ }
+
+ /**
+ * Get the current user configs array
+ *
+ * @return array
+ */
+ public function getConfigs(): array {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ $userId = $this->user->getUID();
+ $configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true);
+ $views = array_keys($configs);
+
+ return array_reduce($views, function ($carry, $view) use ($configs) {
+ $carry[$view] = $this->getConfig($view);
+ return $carry;
+ }, []);
+ }
+}
diff --git a/apps/files/lib/Settings/DeclarativeAdminSettings.php b/apps/files/lib/Settings/DeclarativeAdminSettings.php
new file mode 100644
index 00000000000..bbf97cc4d32
--- /dev/null
+++ b/apps/files/lib/Settings/DeclarativeAdminSettings.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Settings;
+
+use OCA\Files\Service\SettingsService;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Settings\DeclarativeSettingsTypes;
+use OCP\Settings\IDeclarativeSettingsFormWithHandlers;
+
+class DeclarativeAdminSettings implements IDeclarativeSettingsFormWithHandlers {
+
+ public function __construct(
+ private IL10N $l,
+ private SettingsService $service,
+ private IURLGenerator $urlGenerator,
+ ) {
+ }
+
+ public function getValue(string $fieldId, IUser $user): mixed {
+ return match($fieldId) {
+ 'windows_support' => $this->service->hasFilesWindowsSupport(),
+ default => throw new \InvalidArgumentException('Unexpected field id ' . $fieldId),
+ };
+ }
+
+ public function setValue(string $fieldId, mixed $value, IUser $user): void {
+ switch ($fieldId) {
+ case 'windows_support':
+ $this->service->setFilesWindowsSupport((bool)$value);
+ break;
+ }
+ }
+
+ public function getSchema(): array {
+ return [
+ 'id' => 'files-filename-support',
+ 'priority' => 10,
+ 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN,
+ 'section_id' => 'server',
+ 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL,
+ 'title' => $this->l->t('Files compatibility'),
+ 'doc_url' => $this->urlGenerator->linkToDocs('admin-windows-compatible-filenames'),
+ 'description' => (
+ $this->l->t('Allow to restrict filenames to ensure files can be synced with all clients. By default all filenames valid on POSIX (e.g. Linux or macOS) are allowed.')
+ . "\n" . $this->l->t('After enabling the Windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner.')
+ . "\n" . $this->l->t('It is also possible to migrate files automatically after enabling this setting, please refer to the documentation about the occ command.')
+ ),
+
+ 'fields' => [
+ [
+ 'id' => 'windows_support',
+ 'title' => $this->l->t('Enforce Windows compatibility'),
+ 'description' => $this->l->t('This will block filenames not valid on Windows systems, like using reserved names or special characters. But this will not enforce compatibility of case sensitivity.'),
+ 'type' => DeclarativeSettingsTypes::CHECKBOX,
+ 'default' => false,
+ ],
+ ],
+ ];
+ }
+}
diff --git a/apps/files/lib/Settings/PersonalSettings.php b/apps/files/lib/Settings/PersonalSettings.php
index c62a1b9d62d..fe43265bc13 100644
--- a/apps/files/lib/Settings/PersonalSettings.php
+++ b/apps/files/lib/Settings/PersonalSettings.php
@@ -3,34 +3,19 @@
declare(strict_types=1);
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Settings;
use OCA\Files\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Settings\ISettings;
+use OCP\Util;
class PersonalSettings implements ISettings {
public function getForm(): TemplateResponse {
+ Util::addScript(Application::APP_ID, 'settings-personal');
return new TemplateResponse(Application::APP_ID, 'settings-personal');
}