Completed
Push — master ( 1c518a...24c6a1 )
by John
30:27 queued 07:45
created
apps/dav/lib/Connector/Sabre/ServerFactory.php 2 patches
Indentation   +196 added lines, -196 removed lines patch added patch discarded remove patch
@@ -43,200 +43,200 @@
 block discarded – undo
43 43
 
44 44
 class ServerFactory {
45 45
 
46
-	public function __construct(
47
-		private IConfig $config,
48
-		private LoggerInterface $logger,
49
-		private IDBConnection $databaseConnection,
50
-		private IUserSession $userSession,
51
-		private IMountManager $mountManager,
52
-		private ITagManager $tagManager,
53
-		private IRequest $request,
54
-		private IPreview $previewManager,
55
-		private IEventDispatcher $eventDispatcher,
56
-		private IL10N $l10n,
57
-	) {
58
-	}
59
-
60
-	/**
61
-	 * @param callable $viewCallBack callback that should return the view for the dav endpoint
62
-	 */
63
-	public function createServer(
64
-		bool $isPublicShare,
65
-		string $baseUri,
66
-		string $requestUri,
67
-		Plugin $authPlugin,
68
-		callable $viewCallBack,
69
-	): Server {
70
-		// Fire up server
71
-		if ($isPublicShare) {
72
-			$rootCollection = new SimpleCollection('root');
73
-			$tree = new CachingTree($rootCollection);
74
-		} else {
75
-			$rootCollection = null;
76
-			$tree = new ObjectTree();
77
-		}
78
-		$server = new Server($tree);
79
-		// Set URL explicitly due to reverse-proxy situations
80
-		$server->httpRequest->setUrl($requestUri);
81
-		$server->setBaseUri($baseUri);
82
-
83
-		// Load plugins
84
-		$server->addPlugin(new MaintenancePlugin($this->config, $this->l10n));
85
-		$server->addPlugin(new BlockLegacyClientPlugin(
86
-			$this->config,
87
-			\OCP\Server::get(ThemingDefaults::class),
88
-		));
89
-		$server->addPlugin(new AnonymousOptionsPlugin());
90
-		$server->addPlugin($authPlugin);
91
-		// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
92
-		$server->addPlugin(new DummyGetResponsePlugin());
93
-		$server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger));
94
-		$server->addPlugin(new LockPlugin());
95
-
96
-		$server->addPlugin(new RequestIdHeaderPlugin($this->request));
97
-
98
-		$server->addPlugin(new ZipFolderPlugin(
99
-			$tree,
100
-			$this->logger,
101
-			$this->eventDispatcher,
102
-		));
103
-
104
-		// Some WebDAV clients do require Class 2 WebDAV support (locking), since
105
-		// we do not provide locking we emulate it using a fake locking plugin.
106
-		if ($this->request->isUserAgent([
107
-			'/WebDAVFS/',
108
-			'/OneNote/',
109
-			'/Microsoft-WebDAV-MiniRedir/',
110
-		])) {
111
-			$server->addPlugin(new FakeLockerPlugin());
112
-		}
113
-
114
-		if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) {
115
-			$server->addPlugin(new BrowserErrorPagePlugin());
116
-		}
117
-
118
-		// wait with registering these until auth is handled and the filesystem is setup
119
-		$server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void {
120
-			// ensure the skeleton is copied
121
-			$userFolder = \OC::$server->getUserFolder();
122
-
123
-			/** @var View $view */
124
-			$view = $viewCallBack($server);
125
-			if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) {
126
-				$rootInfo = $userFolder;
127
-			} else {
128
-				$rootInfo = $view->getFileInfo('');
129
-			}
130
-
131
-			// Create Nextcloud Dir
132
-			if ($rootInfo->getType() === 'dir') {
133
-				$root = new Directory($view, $rootInfo, $tree);
134
-			} else {
135
-				$root = new File($view, $rootInfo);
136
-			}
137
-
138
-			if ($isPublicShare) {
139
-				$userPrincipalBackend = new Principal(
140
-					\OCP\Server::get(IUserManager::class),
141
-					\OCP\Server::get(IGroupManager::class),
142
-					\OCP\Server::get(IAccountManager::class),
143
-					\OCP\Server::get(\OCP\Share\IManager::class),
144
-					\OCP\Server::get(IUserSession::class),
145
-					\OCP\Server::get(IAppManager::class),
146
-					\OCP\Server::get(ProxyMapper::class),
147
-					\OCP\Server::get(KnownUserService::class),
148
-					\OCP\Server::get(IConfig::class),
149
-					\OC::$server->getL10NFactory(),
150
-				);
151
-
152
-				// Mount the share collection at /public.php/dav/shares/<share token>
153
-				$rootCollection->addChild(new \OCA\DAV\Files\Sharing\RootCollection(
154
-					$root,
155
-					$userPrincipalBackend,
156
-					'principals/shares',
157
-				));
158
-
159
-				// Mount the upload collection at /public.php/dav/uploads/<share token>
160
-				$rootCollection->addChild(new \OCA\DAV\Upload\RootCollection(
161
-					$userPrincipalBackend,
162
-					'principals/shares',
163
-					\OCP\Server::get(CleanupService::class),
164
-					\OCP\Server::get(IRootFolder::class),
165
-					\OCP\Server::get(IUserSession::class),
166
-					\OCP\Server::get(\OCP\Share\IManager::class),
167
-				));
168
-			} else {
169
-				/** @var ObjectTree $tree */
170
-				$tree->init($root, $view, $this->mountManager);
171
-			}
172
-
173
-			$server->addPlugin(
174
-				new FilesPlugin(
175
-					$tree,
176
-					$this->config,
177
-					$this->request,
178
-					$this->previewManager,
179
-					$this->userSession,
180
-					\OCP\Server::get(IFilenameValidator::class),
181
-					\OCP\Server::get(IAccountManager::class),
182
-					false,
183
-					!$this->config->getSystemValue('debug', false)
184
-				)
185
-			);
186
-			$server->addPlugin(new QuotaPlugin($view, true));
187
-			$server->addPlugin(new ChecksumUpdatePlugin());
188
-
189
-			// Allow view-only plugin for webdav requests
190
-			$server->addPlugin(new ViewOnlyPlugin(
191
-				$userFolder,
192
-			));
193
-
194
-			if ($this->userSession->isLoggedIn()) {
195
-				$server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession));
196
-				$server->addPlugin(new SharesPlugin(
197
-					$tree,
198
-					$this->userSession,
199
-					$userFolder,
200
-					\OCP\Server::get(\OCP\Share\IManager::class)
201
-				));
202
-				$server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession));
203
-				$server->addPlugin(new FilesReportPlugin(
204
-					$tree,
205
-					$view,
206
-					\OCP\Server::get(ISystemTagManager::class),
207
-					\OCP\Server::get(ISystemTagObjectMapper::class),
208
-					\OCP\Server::get(ITagManager::class),
209
-					$this->userSession,
210
-					\OCP\Server::get(IGroupManager::class),
211
-					$userFolder,
212
-					\OCP\Server::get(IAppManager::class)
213
-				));
214
-				// custom properties plugin must be the last one
215
-				$server->addPlugin(
216
-					new \Sabre\DAV\PropertyStorage\Plugin(
217
-						new CustomPropertiesBackend(
218
-							$server,
219
-							$tree,
220
-							$this->databaseConnection,
221
-							$this->userSession->getUser(),
222
-							\OCP\Server::get(DefaultCalendarValidator::class),
223
-						)
224
-					)
225
-				);
226
-			}
227
-			$server->addPlugin(new CopyEtagHeaderPlugin());
228
-
229
-			// Load dav plugins from apps
230
-			$event = new SabrePluginEvent($server);
231
-			$this->eventDispatcher->dispatchTyped($event);
232
-			$pluginManager = new PluginManager(
233
-				\OC::$server,
234
-				\OCP\Server::get(IAppManager::class)
235
-			);
236
-			foreach ($pluginManager->getAppPlugins() as $appPlugin) {
237
-				$server->addPlugin($appPlugin);
238
-			}
239
-		}, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request
240
-		return $server;
241
-	}
46
+    public function __construct(
47
+        private IConfig $config,
48
+        private LoggerInterface $logger,
49
+        private IDBConnection $databaseConnection,
50
+        private IUserSession $userSession,
51
+        private IMountManager $mountManager,
52
+        private ITagManager $tagManager,
53
+        private IRequest $request,
54
+        private IPreview $previewManager,
55
+        private IEventDispatcher $eventDispatcher,
56
+        private IL10N $l10n,
57
+    ) {
58
+    }
59
+
60
+    /**
61
+     * @param callable $viewCallBack callback that should return the view for the dav endpoint
62
+     */
63
+    public function createServer(
64
+        bool $isPublicShare,
65
+        string $baseUri,
66
+        string $requestUri,
67
+        Plugin $authPlugin,
68
+        callable $viewCallBack,
69
+    ): Server {
70
+        // Fire up server
71
+        if ($isPublicShare) {
72
+            $rootCollection = new SimpleCollection('root');
73
+            $tree = new CachingTree($rootCollection);
74
+        } else {
75
+            $rootCollection = null;
76
+            $tree = new ObjectTree();
77
+        }
78
+        $server = new Server($tree);
79
+        // Set URL explicitly due to reverse-proxy situations
80
+        $server->httpRequest->setUrl($requestUri);
81
+        $server->setBaseUri($baseUri);
82
+
83
+        // Load plugins
84
+        $server->addPlugin(new MaintenancePlugin($this->config, $this->l10n));
85
+        $server->addPlugin(new BlockLegacyClientPlugin(
86
+            $this->config,
87
+            \OCP\Server::get(ThemingDefaults::class),
88
+        ));
89
+        $server->addPlugin(new AnonymousOptionsPlugin());
90
+        $server->addPlugin($authPlugin);
91
+        // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
92
+        $server->addPlugin(new DummyGetResponsePlugin());
93
+        $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger));
94
+        $server->addPlugin(new LockPlugin());
95
+
96
+        $server->addPlugin(new RequestIdHeaderPlugin($this->request));
97
+
98
+        $server->addPlugin(new ZipFolderPlugin(
99
+            $tree,
100
+            $this->logger,
101
+            $this->eventDispatcher,
102
+        ));
103
+
104
+        // Some WebDAV clients do require Class 2 WebDAV support (locking), since
105
+        // we do not provide locking we emulate it using a fake locking plugin.
106
+        if ($this->request->isUserAgent([
107
+            '/WebDAVFS/',
108
+            '/OneNote/',
109
+            '/Microsoft-WebDAV-MiniRedir/',
110
+        ])) {
111
+            $server->addPlugin(new FakeLockerPlugin());
112
+        }
113
+
114
+        if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) {
115
+            $server->addPlugin(new BrowserErrorPagePlugin());
116
+        }
117
+
118
+        // wait with registering these until auth is handled and the filesystem is setup
119
+        $server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void {
120
+            // ensure the skeleton is copied
121
+            $userFolder = \OC::$server->getUserFolder();
122
+
123
+            /** @var View $view */
124
+            $view = $viewCallBack($server);
125
+            if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) {
126
+                $rootInfo = $userFolder;
127
+            } else {
128
+                $rootInfo = $view->getFileInfo('');
129
+            }
130
+
131
+            // Create Nextcloud Dir
132
+            if ($rootInfo->getType() === 'dir') {
133
+                $root = new Directory($view, $rootInfo, $tree);
134
+            } else {
135
+                $root = new File($view, $rootInfo);
136
+            }
137
+
138
+            if ($isPublicShare) {
139
+                $userPrincipalBackend = new Principal(
140
+                    \OCP\Server::get(IUserManager::class),
141
+                    \OCP\Server::get(IGroupManager::class),
142
+                    \OCP\Server::get(IAccountManager::class),
143
+                    \OCP\Server::get(\OCP\Share\IManager::class),
144
+                    \OCP\Server::get(IUserSession::class),
145
+                    \OCP\Server::get(IAppManager::class),
146
+                    \OCP\Server::get(ProxyMapper::class),
147
+                    \OCP\Server::get(KnownUserService::class),
148
+                    \OCP\Server::get(IConfig::class),
149
+                    \OC::$server->getL10NFactory(),
150
+                );
151
+
152
+                // Mount the share collection at /public.php/dav/shares/<share token>
153
+                $rootCollection->addChild(new \OCA\DAV\Files\Sharing\RootCollection(
154
+                    $root,
155
+                    $userPrincipalBackend,
156
+                    'principals/shares',
157
+                ));
158
+
159
+                // Mount the upload collection at /public.php/dav/uploads/<share token>
160
+                $rootCollection->addChild(new \OCA\DAV\Upload\RootCollection(
161
+                    $userPrincipalBackend,
162
+                    'principals/shares',
163
+                    \OCP\Server::get(CleanupService::class),
164
+                    \OCP\Server::get(IRootFolder::class),
165
+                    \OCP\Server::get(IUserSession::class),
166
+                    \OCP\Server::get(\OCP\Share\IManager::class),
167
+                ));
168
+            } else {
169
+                /** @var ObjectTree $tree */
170
+                $tree->init($root, $view, $this->mountManager);
171
+            }
172
+
173
+            $server->addPlugin(
174
+                new FilesPlugin(
175
+                    $tree,
176
+                    $this->config,
177
+                    $this->request,
178
+                    $this->previewManager,
179
+                    $this->userSession,
180
+                    \OCP\Server::get(IFilenameValidator::class),
181
+                    \OCP\Server::get(IAccountManager::class),
182
+                    false,
183
+                    !$this->config->getSystemValue('debug', false)
184
+                )
185
+            );
186
+            $server->addPlugin(new QuotaPlugin($view, true));
187
+            $server->addPlugin(new ChecksumUpdatePlugin());
188
+
189
+            // Allow view-only plugin for webdav requests
190
+            $server->addPlugin(new ViewOnlyPlugin(
191
+                $userFolder,
192
+            ));
193
+
194
+            if ($this->userSession->isLoggedIn()) {
195
+                $server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession));
196
+                $server->addPlugin(new SharesPlugin(
197
+                    $tree,
198
+                    $this->userSession,
199
+                    $userFolder,
200
+                    \OCP\Server::get(\OCP\Share\IManager::class)
201
+                ));
202
+                $server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession));
203
+                $server->addPlugin(new FilesReportPlugin(
204
+                    $tree,
205
+                    $view,
206
+                    \OCP\Server::get(ISystemTagManager::class),
207
+                    \OCP\Server::get(ISystemTagObjectMapper::class),
208
+                    \OCP\Server::get(ITagManager::class),
209
+                    $this->userSession,
210
+                    \OCP\Server::get(IGroupManager::class),
211
+                    $userFolder,
212
+                    \OCP\Server::get(IAppManager::class)
213
+                ));
214
+                // custom properties plugin must be the last one
215
+                $server->addPlugin(
216
+                    new \Sabre\DAV\PropertyStorage\Plugin(
217
+                        new CustomPropertiesBackend(
218
+                            $server,
219
+                            $tree,
220
+                            $this->databaseConnection,
221
+                            $this->userSession->getUser(),
222
+                            \OCP\Server::get(DefaultCalendarValidator::class),
223
+                        )
224
+                    )
225
+                );
226
+            }
227
+            $server->addPlugin(new CopyEtagHeaderPlugin());
228
+
229
+            // Load dav plugins from apps
230
+            $event = new SabrePluginEvent($server);
231
+            $this->eventDispatcher->dispatchTyped($event);
232
+            $pluginManager = new PluginManager(
233
+                \OC::$server,
234
+                \OCP\Server::get(IAppManager::class)
235
+            );
236
+            foreach ($pluginManager->getAppPlugins() as $appPlugin) {
237
+                $server->addPlugin($appPlugin);
238
+            }
239
+        }, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request
240
+        return $server;
241
+    }
242 242
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -116,7 +116,7 @@
 block discarded – undo
116 116
 		}
117 117
 
118 118
 		// wait with registering these until auth is handled and the filesystem is setup
119
-		$server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void {
119
+		$server->on('beforeMethod:*', function() use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void {
120 120
 			// ensure the skeleton is copied
121 121
 			$userFolder = \OC::$server->getUserFolder();
122 122
 
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/FilesPlugin.php 1 patch
Indentation   +692 added lines, -692 removed lines patch added patch discarded remove patch
@@ -39,696 +39,696 @@
 block discarded – undo
39 39
 use Sabre\HTTP\ResponseInterface;
40 40
 
41 41
 class FilesPlugin extends ServerPlugin {
42
-	// namespace
43
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
44
-	public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
45
-	public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
46
-	public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
47
-	public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
48
-	public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
49
-	public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
50
-	public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
51
-	public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
52
-	public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
53
-	public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
54
-	public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
55
-	public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
56
-	public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
57
-	public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
58
-	public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
59
-	public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
60
-	public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
61
-	public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
62
-	public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
63
-	public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
64
-	public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
65
-	public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
66
-	public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
67
-	public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
68
-	public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
69
-	public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
70
-	public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
71
-	public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
72
-	public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
73
-	public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
74
-
75
-	/** Reference to main server object */
76
-	private ?Server $server = null;
77
-
78
-	/**
79
-	 * @param Tree $tree
80
-	 * @param IConfig $config
81
-	 * @param IRequest $request
82
-	 * @param IPreview $previewManager
83
-	 * @param IUserSession $userSession
84
-	 * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
85
-	 * @param bool $downloadAttachment
86
-	 * @return void
87
-	 */
88
-	public function __construct(
89
-		private Tree $tree,
90
-		private IConfig $config,
91
-		private IRequest $request,
92
-		private IPreview $previewManager,
93
-		private IUserSession $userSession,
94
-		private IFilenameValidator $validator,
95
-		private IAccountManager $accountManager,
96
-		private bool $isPublic = false,
97
-		private bool $downloadAttachment = true,
98
-	) {
99
-	}
100
-
101
-	/**
102
-	 * This initializes the plugin.
103
-	 *
104
-	 * This function is called by \Sabre\DAV\Server, after
105
-	 * addPlugin is called.
106
-	 *
107
-	 * This method should set up the required event subscriptions.
108
-	 *
109
-	 * @return void
110
-	 */
111
-	public function initialize(Server $server) {
112
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
113
-		$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
114
-		$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
115
-		$server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
116
-		$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
117
-		$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
118
-		$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
119
-		$server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
120
-		$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
121
-		$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
122
-		$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
123
-		$server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
124
-		$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
125
-		$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
126
-		$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
127
-		$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
128
-		$server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
129
-		$server->protectedProperties[] = self::SHARE_NOTE;
130
-
131
-		// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
132
-		$allowedProperties = ['{DAV:}getetag'];
133
-		$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
134
-
135
-		$this->server = $server;
136
-		$this->server->on('propFind', [$this, 'handleGetProperties']);
137
-		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
138
-		$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
139
-		$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
140
-		$this->server->on('afterMethod:GET', [$this,'httpGet']);
141
-		$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
142
-		$this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
143
-			$body = $response->getBody();
144
-			if (is_resource($body)) {
145
-				fclose($body);
146
-			}
147
-		});
148
-		$this->server->on('beforeMove', [$this, 'checkMove']);
149
-		$this->server->on('beforeCopy', [$this, 'checkCopy']);
150
-	}
151
-
152
-	/**
153
-	 * Plugin that checks if a copy can actually be performed.
154
-	 *
155
-	 * @param string $source source path
156
-	 * @param string $target target path
157
-	 * @throws NotFound If the source does not exist
158
-	 * @throws InvalidPath If the target is invalid
159
-	 */
160
-	public function checkCopy($source, $target): void {
161
-		$sourceNode = $this->tree->getNodeForPath($source);
162
-		if (!$sourceNode instanceof Node) {
163
-			return;
164
-		}
165
-
166
-		// Ensure source exists
167
-		$sourceNodeFileInfo = $sourceNode->getFileInfo();
168
-		if ($sourceNodeFileInfo === null) {
169
-			throw new NotFound($source . ' does not exist');
170
-		}
171
-		// Ensure the target name is valid
172
-		try {
173
-			[$targetPath, $targetName] = \Sabre\Uri\split($target);
174
-			$this->validator->validateFilename($targetName);
175
-		} catch (InvalidPathException $e) {
176
-			throw new InvalidPath($e->getMessage(), false);
177
-		}
178
-		// Ensure the target path is valid
179
-		$segments = array_slice(explode('/', $targetPath), 2);
180
-		foreach ($segments as $segment) {
181
-			if ($this->validator->isFilenameValid($segment) === false) {
182
-				$l = \OCP\Server::get(IFactory::class)->get('dav');
183
-				throw new InvalidPath($l->t('Invalid target path'));
184
-			}
185
-		}
186
-	}
187
-
188
-	/**
189
-	 * Plugin that checks if a move can actually be performed.
190
-	 *
191
-	 * @param string $source source path
192
-	 * @param string $target target path
193
-	 * @throws Forbidden If the source is not deletable
194
-	 * @throws NotFound If the source does not exist
195
-	 * @throws InvalidPath If the target name is invalid
196
-	 */
197
-	public function checkMove(string $source, string $target): void {
198
-		$sourceNode = $this->tree->getNodeForPath($source);
199
-		if (!$sourceNode instanceof Node) {
200
-			return;
201
-		}
202
-
203
-		// First check copyable (move only needs additional delete permission)
204
-		$this->checkCopy($source, $target);
205
-
206
-		// The source needs to be deletable for moving
207
-		$sourceNodeFileInfo = $sourceNode->getFileInfo();
208
-		if (!$sourceNodeFileInfo->isDeletable()) {
209
-			throw new Forbidden($source . ' cannot be deleted');
210
-		}
211
-
212
-		// The source is not allowed to be the parent of the target
213
-		if (str_starts_with($source, $target . '/')) {
214
-			throw new Forbidden($source . ' cannot be moved to it\'s parent');
215
-		}
216
-	}
217
-
218
-	/**
219
-	 * This sets a cookie to be able to recognize the start of the download
220
-	 * the content must not be longer than 32 characters and must only contain
221
-	 * alphanumeric characters
222
-	 *
223
-	 * @param RequestInterface $request
224
-	 * @param ResponseInterface $response
225
-	 */
226
-	public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
227
-		$queryParams = $request->getQueryParameters();
228
-
229
-		/**
230
-		 * this sets a cookie to be able to recognize the start of the download
231
-		 * the content must not be longer than 32 characters and must only contain
232
-		 * alphanumeric characters
233
-		 */
234
-		if (isset($queryParams['downloadStartSecret'])) {
235
-			$token = $queryParams['downloadStartSecret'];
236
-			if (!isset($token[32])
237
-				&& preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
238
-				// FIXME: use $response->setHeader() instead
239
-				setcookie('ocDownloadStarted', $token, time() + 20, '/');
240
-			}
241
-		}
242
-	}
243
-
244
-	/**
245
-	 * Add headers to file download
246
-	 *
247
-	 * @param RequestInterface $request
248
-	 * @param ResponseInterface $response
249
-	 */
250
-	public function httpGet(RequestInterface $request, ResponseInterface $response) {
251
-		// Only handle valid files
252
-		$node = $this->tree->getNodeForPath($request->getPath());
253
-		if (!($node instanceof IFile)) {
254
-			return;
255
-		}
256
-
257
-		// adds a 'Content-Disposition: attachment' header in case no disposition
258
-		// header has been set before
259
-		if ($this->downloadAttachment &&
260
-			$response->getHeader('Content-Disposition') === null) {
261
-			$filename = $node->getName();
262
-			if ($this->request->isUserAgent(
263
-				[
264
-					Request::USER_AGENT_IE,
265
-					Request::USER_AGENT_ANDROID_MOBILE_CHROME,
266
-					Request::USER_AGENT_FREEBOX,
267
-				])) {
268
-				$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
269
-			} else {
270
-				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
271
-													 . '; filename="' . rawurlencode($filename) . '"');
272
-			}
273
-		}
274
-
275
-		if ($node instanceof File) {
276
-			//Add OC-Checksum header
277
-			$checksum = $node->getChecksum();
278
-			if ($checksum !== null && $checksum !== '') {
279
-				$response->addHeader('OC-Checksum', $checksum);
280
-			}
281
-		}
282
-		$response->addHeader('X-Accel-Buffering', 'no');
283
-	}
284
-
285
-	/**
286
-	 * Adds all ownCloud-specific properties
287
-	 *
288
-	 * @param PropFind $propFind
289
-	 * @param \Sabre\DAV\INode $node
290
-	 * @return void
291
-	 */
292
-	public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
293
-		$httpRequest = $this->server->httpRequest;
294
-
295
-		if ($node instanceof Node) {
296
-			/**
297
-			 * This was disabled, because it made dir listing throw an exception,
298
-			 * so users were unable to navigate into folders where one subitem
299
-			 * is blocked by the files_accesscontrol app, see:
300
-			 * https://github.com/nextcloud/files_accesscontrol/issues/65
301
-			 * if (!$node->getFileInfo()->isReadable()) {
302
-			 *     // avoid detecting files through this means
303
-			 *     throw new NotFound();
304
-			 * }
305
-			 */
306
-
307
-			$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
308
-				return $node->getFileId();
309
-			});
310
-
311
-			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
312
-				return $node->getInternalFileId();
313
-			});
314
-
315
-			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
316
-				$perms = $node->getDavPermissions();
317
-				if ($this->isPublic) {
318
-					// remove mount information
319
-					$perms = str_replace(['S', 'M'], '', $perms);
320
-				}
321
-				return $perms;
322
-			});
323
-
324
-			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
325
-				$user = $this->userSession->getUser();
326
-				if ($user === null) {
327
-					return null;
328
-				}
329
-				return $node->getSharePermissions(
330
-					$user->getUID()
331
-				);
332
-			});
333
-
334
-			$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
335
-				$user = $this->userSession->getUser();
336
-				if ($user === null) {
337
-					return null;
338
-				}
339
-				$ncPermissions = $node->getSharePermissions(
340
-					$user->getUID()
341
-				);
342
-				$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
343
-				return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
344
-			});
345
-
346
-			$propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
347
-				return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
348
-			});
349
-
350
-			$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
351
-				return $node->getETag();
352
-			});
353
-
354
-			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
355
-				$owner = $node->getOwner();
356
-				if (!$owner) {
357
-					return null;
358
-				} else {
359
-					return $owner->getUID();
360
-				}
361
-			});
362
-			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
363
-				$owner = $node->getOwner();
364
-				if (!$owner) {
365
-					return null;
366
-				}
367
-
368
-				// Get current user to see if we're in a public share or not
369
-				$user = $this->userSession->getUser();
370
-
371
-				// If the user is logged in, we can return the display name
372
-				if ($user !== null) {
373
-					return $owner->getDisplayName();
374
-				}
375
-
376
-				// Check if the user published their display name
377
-				$ownerAccount = $this->accountManager->getAccount($owner);
378
-				$ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
379
-
380
-				// Since we are not logged in, we need to have at least the published scope
381
-				if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
382
-					return $owner->getDisplayName();
383
-				}
384
-
385
-				return null;
386
-			});
387
-
388
-			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
389
-				return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
390
-			});
391
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
392
-				return $node->getSize();
393
-			});
394
-			$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
395
-				return $node->getFileInfo()->getMountPoint()->getMountType();
396
-			});
397
-
398
-			/**
399
-			 * This is a special property which is used to determine if a node
400
-			 * is a mount root or not, e.g. a shared folder.
401
-			 * If so, then the node can only be unshared and not deleted.
402
-			 * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
403
-			 */
404
-			$propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
405
-				return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
406
-			});
407
-
408
-			$propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
409
-				$user = $this->userSession->getUser();
410
-				return $node->getNoteFromShare(
411
-					$user?->getUID()
412
-				);
413
-			});
414
-
415
-			$propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
416
-				$storage = $node->getNode()->getStorage();
417
-				if ($storage->instanceOfStorage(ISharedStorage::class)) {
418
-					/** @var ISharedStorage $storage */
419
-					return match($storage->getShare()->getHideDownload()) {
420
-						true => 'true',
421
-						false => 'false',
422
-					};
423
-				} else {
424
-					return null;
425
-				}
426
-			});
427
-
428
-			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
429
-				return $this->config->getSystemValue('data-fingerprint', '');
430
-			});
431
-			$propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
432
-				return (new \DateTimeImmutable())
433
-					->setTimestamp($node->getFileInfo()->getCreationTime())
434
-					->format(\DateTimeInterface::ATOM);
435
-			});
436
-			$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
437
-				return $node->getFileInfo()->getCreationTime();
438
-			});
439
-
440
-			foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
441
-				$propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
442
-			}
443
-
444
-			$propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
445
-				$isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
446
-				$isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
447
-				return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
448
-			});
449
-
450
-			/**
451
-			 * Return file/folder name as displayname. The primary reason to
452
-			 * implement it this way is to avoid costly fallback to
453
-			 * CustomPropertiesBackend (esp. visible when querying all files
454
-			 * in a folder).
455
-			 */
456
-			$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
457
-				return $node->getName();
458
-			});
459
-
460
-			$propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
461
-				return $node->getFileInfo()->getMountPoint()
462
-					instanceof SharingExternalMount;
463
-			});
464
-		}
465
-
466
-		if ($node instanceof File) {
467
-			$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
468
-				try {
469
-					$directDownloadUrl = $node->getDirectDownload();
470
-					if (isset($directDownloadUrl['url'])) {
471
-						return $directDownloadUrl['url'];
472
-					}
473
-				} catch (StorageNotAvailableException $e) {
474
-					return false;
475
-				} catch (ForbiddenException $e) {
476
-					return false;
477
-				}
478
-				return false;
479
-			});
480
-
481
-			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
482
-				$checksum = $node->getChecksum();
483
-				if ($checksum === null || $checksum === '') {
484
-					return null;
485
-				}
486
-
487
-				return new ChecksumList($checksum);
488
-			});
489
-
490
-			$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
491
-				return $node->getFileInfo()->getUploadTime();
492
-			});
493
-		}
494
-
495
-		if ($node instanceof Directory) {
496
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
497
-				return $node->getSize();
498
-			});
499
-
500
-			$requestProperties = $propFind->getRequestedProperties();
501
-
502
-			if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
503
-				|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
504
-				$nbFiles = 0;
505
-				$nbFolders = 0;
506
-				foreach ($node->getChildren() as $child) {
507
-					if ($child instanceof File) {
508
-						$nbFiles++;
509
-					} elseif ($child instanceof Directory) {
510
-						$nbFolders++;
511
-					}
512
-				}
513
-
514
-				$propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
515
-				$propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
516
-			}
517
-		}
518
-	}
519
-
520
-	/**
521
-	 * translate Nextcloud permissions to OCM Permissions
522
-	 *
523
-	 * @param $ncPermissions
524
-	 * @return array
525
-	 */
526
-	protected function ncPermissions2ocmPermissions($ncPermissions) {
527
-		$ocmPermissions = [];
528
-
529
-		if ($ncPermissions & Constants::PERMISSION_SHARE) {
530
-			$ocmPermissions[] = 'share';
531
-		}
532
-
533
-		if ($ncPermissions & Constants::PERMISSION_READ) {
534
-			$ocmPermissions[] = 'read';
535
-		}
536
-
537
-		if (($ncPermissions & Constants::PERMISSION_CREATE) ||
538
-			($ncPermissions & Constants::PERMISSION_UPDATE)) {
539
-			$ocmPermissions[] = 'write';
540
-		}
541
-
542
-		return $ocmPermissions;
543
-	}
544
-
545
-	/**
546
-	 * Update ownCloud-specific properties
547
-	 *
548
-	 * @param string $path
549
-	 * @param PropPatch $propPatch
550
-	 *
551
-	 * @return void
552
-	 */
553
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
554
-		$node = $this->tree->getNodeForPath($path);
555
-		if (!($node instanceof Node)) {
556
-			return;
557
-		}
558
-
559
-		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
560
-			if (empty($time)) {
561
-				return false;
562
-			}
563
-			$node->touch($time);
564
-			return true;
565
-		});
566
-		$propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
567
-			if (empty($etag)) {
568
-				return false;
569
-			}
570
-			return $node->setEtag($etag) !== -1;
571
-		});
572
-		$propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
573
-			if (empty($time)) {
574
-				return false;
575
-			}
576
-			$dateTime = new \DateTimeImmutable($time);
577
-			$node->setCreationTime($dateTime->getTimestamp());
578
-			return true;
579
-		});
580
-		$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
581
-			if (empty($time)) {
582
-				return false;
583
-			}
584
-			$node->setCreationTime((int)$time);
585
-			return true;
586
-		});
587
-
588
-		$this->handleUpdatePropertiesMetadata($propPatch, $node);
589
-
590
-		/**
591
-		 * Disable modification of the displayname property for files and
592
-		 * folders via PROPPATCH. See PROPFIND for more information.
593
-		 */
594
-		$propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
595
-			return 403;
596
-		});
597
-	}
598
-
599
-
600
-	/**
601
-	 * handle the update of metadata from PROPPATCH requests
602
-	 *
603
-	 * @param PropPatch $propPatch
604
-	 * @param Node $node
605
-	 *
606
-	 * @throws FilesMetadataException
607
-	 */
608
-	private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
609
-		$userId = $this->userSession->getUser()?->getUID();
610
-		if ($userId === null) {
611
-			return;
612
-		}
613
-
614
-		$accessRight = $this->getMetadataFileAccessRight($node, $userId);
615
-		$filesMetadataManager = $this->initFilesMetadataManager();
616
-		$knownMetadata = $filesMetadataManager->getKnownMetadata();
617
-
618
-		foreach ($propPatch->getRemainingMutations() as $mutation) {
619
-			if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
620
-				continue;
621
-			}
622
-
623
-			$propPatch->handle(
624
-				$mutation,
625
-				function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
626
-					/** @var FilesMetadata $metadata */
627
-					$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
628
-					$metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
629
-					$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
630
-
631
-					// confirm metadata key is editable via PROPPATCH
632
-					if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
633
-						throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
634
-					}
635
-
636
-					if ($value === null) {
637
-						$metadata->unset($metadataKey);
638
-						$filesMetadataManager->saveMetadata($metadata);
639
-						return true;
640
-					}
641
-
642
-					// If the metadata is unknown, it defaults to string.
643
-					try {
644
-						$type = $knownMetadata->getType($metadataKey);
645
-					} catch (FilesMetadataNotFoundException) {
646
-						$type = IMetadataValueWrapper::TYPE_STRING;
647
-					}
648
-
649
-					switch ($type) {
650
-						case IMetadataValueWrapper::TYPE_STRING:
651
-							$metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
652
-							break;
653
-						case IMetadataValueWrapper::TYPE_INT:
654
-							$metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
655
-							break;
656
-						case IMetadataValueWrapper::TYPE_FLOAT:
657
-							$metadata->setFloat($metadataKey, $value);
658
-							break;
659
-						case IMetadataValueWrapper::TYPE_BOOL:
660
-							$metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
661
-							break;
662
-						case IMetadataValueWrapper::TYPE_ARRAY:
663
-							$metadata->setArray($metadataKey, $value);
664
-							break;
665
-						case IMetadataValueWrapper::TYPE_STRING_LIST:
666
-							$metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
667
-							break;
668
-						case IMetadataValueWrapper::TYPE_INT_LIST:
669
-							$metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
670
-							break;
671
-					}
672
-
673
-					$filesMetadataManager->saveMetadata($metadata);
674
-
675
-					return true;
676
-				}
677
-			);
678
-		}
679
-	}
680
-
681
-	/**
682
-	 * init default internal metadata
683
-	 *
684
-	 * @return IFilesMetadataManager
685
-	 */
686
-	private function initFilesMetadataManager(): IFilesMetadataManager {
687
-		/** @var IFilesMetadataManager $manager */
688
-		$manager = \OCP\Server::get(IFilesMetadataManager::class);
689
-		$manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP);
690
-
691
-		return $manager;
692
-	}
693
-
694
-	/**
695
-	 * based on owner and shares, returns the bottom limit to update related metadata
696
-	 *
697
-	 * @param Node $node
698
-	 * @param string $userId
699
-	 *
700
-	 * @return int
701
-	 */
702
-	private function getMetadataFileAccessRight(Node $node, string $userId): int {
703
-		if ($node->getOwner()?->getUID() === $userId) {
704
-			return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
705
-		} else {
706
-			$filePermissions = $node->getSharePermissions($userId);
707
-			if ($filePermissions & Constants::PERMISSION_UPDATE) {
708
-				return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
709
-			}
710
-		}
711
-
712
-		return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
713
-	}
714
-
715
-	/**
716
-	 * @param string $filePath
717
-	 * @param ?\Sabre\DAV\INode $node
718
-	 * @return void
719
-	 * @throws \Sabre\DAV\Exception\BadRequest
720
-	 */
721
-	public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
722
-		// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
723
-		try {
724
-			$node = $this->server->tree->getNodeForPath($filePath);
725
-			if ($node instanceof Node) {
726
-				$fileId = $node->getFileId();
727
-				if (!is_null($fileId)) {
728
-					$this->server->httpResponse->setHeader('OC-FileId', $fileId);
729
-				}
730
-			}
731
-		} catch (NotFound) {
732
-		}
733
-	}
42
+    // namespace
43
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
44
+    public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
45
+    public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
46
+    public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
47
+    public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
48
+    public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
49
+    public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
50
+    public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
51
+    public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
52
+    public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
53
+    public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
54
+    public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
55
+    public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
56
+    public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
57
+    public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
58
+    public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
59
+    public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
60
+    public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
61
+    public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
62
+    public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
63
+    public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
64
+    public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
65
+    public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
66
+    public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
67
+    public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
68
+    public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
69
+    public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
70
+    public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
71
+    public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
72
+    public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
73
+    public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
74
+
75
+    /** Reference to main server object */
76
+    private ?Server $server = null;
77
+
78
+    /**
79
+     * @param Tree $tree
80
+     * @param IConfig $config
81
+     * @param IRequest $request
82
+     * @param IPreview $previewManager
83
+     * @param IUserSession $userSession
84
+     * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
85
+     * @param bool $downloadAttachment
86
+     * @return void
87
+     */
88
+    public function __construct(
89
+        private Tree $tree,
90
+        private IConfig $config,
91
+        private IRequest $request,
92
+        private IPreview $previewManager,
93
+        private IUserSession $userSession,
94
+        private IFilenameValidator $validator,
95
+        private IAccountManager $accountManager,
96
+        private bool $isPublic = false,
97
+        private bool $downloadAttachment = true,
98
+    ) {
99
+    }
100
+
101
+    /**
102
+     * This initializes the plugin.
103
+     *
104
+     * This function is called by \Sabre\DAV\Server, after
105
+     * addPlugin is called.
106
+     *
107
+     * This method should set up the required event subscriptions.
108
+     *
109
+     * @return void
110
+     */
111
+    public function initialize(Server $server) {
112
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
113
+        $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
114
+        $server->protectedProperties[] = self::FILEID_PROPERTYNAME;
115
+        $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
116
+        $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
117
+        $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
118
+        $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
119
+        $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
120
+        $server->protectedProperties[] = self::SIZE_PROPERTYNAME;
121
+        $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
122
+        $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
123
+        $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
124
+        $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
125
+        $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
126
+        $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
127
+        $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
128
+        $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
129
+        $server->protectedProperties[] = self::SHARE_NOTE;
130
+
131
+        // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
132
+        $allowedProperties = ['{DAV:}getetag'];
133
+        $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
134
+
135
+        $this->server = $server;
136
+        $this->server->on('propFind', [$this, 'handleGetProperties']);
137
+        $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
138
+        $this->server->on('afterBind', [$this, 'sendFileIdHeader']);
139
+        $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
140
+        $this->server->on('afterMethod:GET', [$this,'httpGet']);
141
+        $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
142
+        $this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
143
+            $body = $response->getBody();
144
+            if (is_resource($body)) {
145
+                fclose($body);
146
+            }
147
+        });
148
+        $this->server->on('beforeMove', [$this, 'checkMove']);
149
+        $this->server->on('beforeCopy', [$this, 'checkCopy']);
150
+    }
151
+
152
+    /**
153
+     * Plugin that checks if a copy can actually be performed.
154
+     *
155
+     * @param string $source source path
156
+     * @param string $target target path
157
+     * @throws NotFound If the source does not exist
158
+     * @throws InvalidPath If the target is invalid
159
+     */
160
+    public function checkCopy($source, $target): void {
161
+        $sourceNode = $this->tree->getNodeForPath($source);
162
+        if (!$sourceNode instanceof Node) {
163
+            return;
164
+        }
165
+
166
+        // Ensure source exists
167
+        $sourceNodeFileInfo = $sourceNode->getFileInfo();
168
+        if ($sourceNodeFileInfo === null) {
169
+            throw new NotFound($source . ' does not exist');
170
+        }
171
+        // Ensure the target name is valid
172
+        try {
173
+            [$targetPath, $targetName] = \Sabre\Uri\split($target);
174
+            $this->validator->validateFilename($targetName);
175
+        } catch (InvalidPathException $e) {
176
+            throw new InvalidPath($e->getMessage(), false);
177
+        }
178
+        // Ensure the target path is valid
179
+        $segments = array_slice(explode('/', $targetPath), 2);
180
+        foreach ($segments as $segment) {
181
+            if ($this->validator->isFilenameValid($segment) === false) {
182
+                $l = \OCP\Server::get(IFactory::class)->get('dav');
183
+                throw new InvalidPath($l->t('Invalid target path'));
184
+            }
185
+        }
186
+    }
187
+
188
+    /**
189
+     * Plugin that checks if a move can actually be performed.
190
+     *
191
+     * @param string $source source path
192
+     * @param string $target target path
193
+     * @throws Forbidden If the source is not deletable
194
+     * @throws NotFound If the source does not exist
195
+     * @throws InvalidPath If the target name is invalid
196
+     */
197
+    public function checkMove(string $source, string $target): void {
198
+        $sourceNode = $this->tree->getNodeForPath($source);
199
+        if (!$sourceNode instanceof Node) {
200
+            return;
201
+        }
202
+
203
+        // First check copyable (move only needs additional delete permission)
204
+        $this->checkCopy($source, $target);
205
+
206
+        // The source needs to be deletable for moving
207
+        $sourceNodeFileInfo = $sourceNode->getFileInfo();
208
+        if (!$sourceNodeFileInfo->isDeletable()) {
209
+            throw new Forbidden($source . ' cannot be deleted');
210
+        }
211
+
212
+        // The source is not allowed to be the parent of the target
213
+        if (str_starts_with($source, $target . '/')) {
214
+            throw new Forbidden($source . ' cannot be moved to it\'s parent');
215
+        }
216
+    }
217
+
218
+    /**
219
+     * This sets a cookie to be able to recognize the start of the download
220
+     * the content must not be longer than 32 characters and must only contain
221
+     * alphanumeric characters
222
+     *
223
+     * @param RequestInterface $request
224
+     * @param ResponseInterface $response
225
+     */
226
+    public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
227
+        $queryParams = $request->getQueryParameters();
228
+
229
+        /**
230
+         * this sets a cookie to be able to recognize the start of the download
231
+         * the content must not be longer than 32 characters and must only contain
232
+         * alphanumeric characters
233
+         */
234
+        if (isset($queryParams['downloadStartSecret'])) {
235
+            $token = $queryParams['downloadStartSecret'];
236
+            if (!isset($token[32])
237
+                && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
238
+                // FIXME: use $response->setHeader() instead
239
+                setcookie('ocDownloadStarted', $token, time() + 20, '/');
240
+            }
241
+        }
242
+    }
243
+
244
+    /**
245
+     * Add headers to file download
246
+     *
247
+     * @param RequestInterface $request
248
+     * @param ResponseInterface $response
249
+     */
250
+    public function httpGet(RequestInterface $request, ResponseInterface $response) {
251
+        // Only handle valid files
252
+        $node = $this->tree->getNodeForPath($request->getPath());
253
+        if (!($node instanceof IFile)) {
254
+            return;
255
+        }
256
+
257
+        // adds a 'Content-Disposition: attachment' header in case no disposition
258
+        // header has been set before
259
+        if ($this->downloadAttachment &&
260
+            $response->getHeader('Content-Disposition') === null) {
261
+            $filename = $node->getName();
262
+            if ($this->request->isUserAgent(
263
+                [
264
+                    Request::USER_AGENT_IE,
265
+                    Request::USER_AGENT_ANDROID_MOBILE_CHROME,
266
+                    Request::USER_AGENT_FREEBOX,
267
+                ])) {
268
+                $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
269
+            } else {
270
+                $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
271
+                                                        . '; filename="' . rawurlencode($filename) . '"');
272
+            }
273
+        }
274
+
275
+        if ($node instanceof File) {
276
+            //Add OC-Checksum header
277
+            $checksum = $node->getChecksum();
278
+            if ($checksum !== null && $checksum !== '') {
279
+                $response->addHeader('OC-Checksum', $checksum);
280
+            }
281
+        }
282
+        $response->addHeader('X-Accel-Buffering', 'no');
283
+    }
284
+
285
+    /**
286
+     * Adds all ownCloud-specific properties
287
+     *
288
+     * @param PropFind $propFind
289
+     * @param \Sabre\DAV\INode $node
290
+     * @return void
291
+     */
292
+    public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
293
+        $httpRequest = $this->server->httpRequest;
294
+
295
+        if ($node instanceof Node) {
296
+            /**
297
+             * This was disabled, because it made dir listing throw an exception,
298
+             * so users were unable to navigate into folders where one subitem
299
+             * is blocked by the files_accesscontrol app, see:
300
+             * https://github.com/nextcloud/files_accesscontrol/issues/65
301
+             * if (!$node->getFileInfo()->isReadable()) {
302
+             *     // avoid detecting files through this means
303
+             *     throw new NotFound();
304
+             * }
305
+             */
306
+
307
+            $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
308
+                return $node->getFileId();
309
+            });
310
+
311
+            $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
312
+                return $node->getInternalFileId();
313
+            });
314
+
315
+            $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
316
+                $perms = $node->getDavPermissions();
317
+                if ($this->isPublic) {
318
+                    // remove mount information
319
+                    $perms = str_replace(['S', 'M'], '', $perms);
320
+                }
321
+                return $perms;
322
+            });
323
+
324
+            $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
325
+                $user = $this->userSession->getUser();
326
+                if ($user === null) {
327
+                    return null;
328
+                }
329
+                return $node->getSharePermissions(
330
+                    $user->getUID()
331
+                );
332
+            });
333
+
334
+            $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
335
+                $user = $this->userSession->getUser();
336
+                if ($user === null) {
337
+                    return null;
338
+                }
339
+                $ncPermissions = $node->getSharePermissions(
340
+                    $user->getUID()
341
+                );
342
+                $ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
343
+                return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
344
+            });
345
+
346
+            $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
347
+                return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
348
+            });
349
+
350
+            $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
351
+                return $node->getETag();
352
+            });
353
+
354
+            $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
355
+                $owner = $node->getOwner();
356
+                if (!$owner) {
357
+                    return null;
358
+                } else {
359
+                    return $owner->getUID();
360
+                }
361
+            });
362
+            $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
363
+                $owner = $node->getOwner();
364
+                if (!$owner) {
365
+                    return null;
366
+                }
367
+
368
+                // Get current user to see if we're in a public share or not
369
+                $user = $this->userSession->getUser();
370
+
371
+                // If the user is logged in, we can return the display name
372
+                if ($user !== null) {
373
+                    return $owner->getDisplayName();
374
+                }
375
+
376
+                // Check if the user published their display name
377
+                $ownerAccount = $this->accountManager->getAccount($owner);
378
+                $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
379
+
380
+                // Since we are not logged in, we need to have at least the published scope
381
+                if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
382
+                    return $owner->getDisplayName();
383
+                }
384
+
385
+                return null;
386
+            });
387
+
388
+            $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
389
+                return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
390
+            });
391
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
392
+                return $node->getSize();
393
+            });
394
+            $propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
395
+                return $node->getFileInfo()->getMountPoint()->getMountType();
396
+            });
397
+
398
+            /**
399
+             * This is a special property which is used to determine if a node
400
+             * is a mount root or not, e.g. a shared folder.
401
+             * If so, then the node can only be unshared and not deleted.
402
+             * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
403
+             */
404
+            $propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
405
+                return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
406
+            });
407
+
408
+            $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
409
+                $user = $this->userSession->getUser();
410
+                return $node->getNoteFromShare(
411
+                    $user?->getUID()
412
+                );
413
+            });
414
+
415
+            $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
416
+                $storage = $node->getNode()->getStorage();
417
+                if ($storage->instanceOfStorage(ISharedStorage::class)) {
418
+                    /** @var ISharedStorage $storage */
419
+                    return match($storage->getShare()->getHideDownload()) {
420
+                        true => 'true',
421
+                        false => 'false',
422
+                    };
423
+                } else {
424
+                    return null;
425
+                }
426
+            });
427
+
428
+            $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
429
+                return $this->config->getSystemValue('data-fingerprint', '');
430
+            });
431
+            $propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
432
+                return (new \DateTimeImmutable())
433
+                    ->setTimestamp($node->getFileInfo()->getCreationTime())
434
+                    ->format(\DateTimeInterface::ATOM);
435
+            });
436
+            $propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
437
+                return $node->getFileInfo()->getCreationTime();
438
+            });
439
+
440
+            foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
441
+                $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
442
+            }
443
+
444
+            $propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
445
+                $isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
446
+                $isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
447
+                return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
448
+            });
449
+
450
+            /**
451
+             * Return file/folder name as displayname. The primary reason to
452
+             * implement it this way is to avoid costly fallback to
453
+             * CustomPropertiesBackend (esp. visible when querying all files
454
+             * in a folder).
455
+             */
456
+            $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
457
+                return $node->getName();
458
+            });
459
+
460
+            $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
461
+                return $node->getFileInfo()->getMountPoint()
462
+                    instanceof SharingExternalMount;
463
+            });
464
+        }
465
+
466
+        if ($node instanceof File) {
467
+            $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
468
+                try {
469
+                    $directDownloadUrl = $node->getDirectDownload();
470
+                    if (isset($directDownloadUrl['url'])) {
471
+                        return $directDownloadUrl['url'];
472
+                    }
473
+                } catch (StorageNotAvailableException $e) {
474
+                    return false;
475
+                } catch (ForbiddenException $e) {
476
+                    return false;
477
+                }
478
+                return false;
479
+            });
480
+
481
+            $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
482
+                $checksum = $node->getChecksum();
483
+                if ($checksum === null || $checksum === '') {
484
+                    return null;
485
+                }
486
+
487
+                return new ChecksumList($checksum);
488
+            });
489
+
490
+            $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
491
+                return $node->getFileInfo()->getUploadTime();
492
+            });
493
+        }
494
+
495
+        if ($node instanceof Directory) {
496
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
497
+                return $node->getSize();
498
+            });
499
+
500
+            $requestProperties = $propFind->getRequestedProperties();
501
+
502
+            if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
503
+                || in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
504
+                $nbFiles = 0;
505
+                $nbFolders = 0;
506
+                foreach ($node->getChildren() as $child) {
507
+                    if ($child instanceof File) {
508
+                        $nbFiles++;
509
+                    } elseif ($child instanceof Directory) {
510
+                        $nbFolders++;
511
+                    }
512
+                }
513
+
514
+                $propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
515
+                $propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
516
+            }
517
+        }
518
+    }
519
+
520
+    /**
521
+     * translate Nextcloud permissions to OCM Permissions
522
+     *
523
+     * @param $ncPermissions
524
+     * @return array
525
+     */
526
+    protected function ncPermissions2ocmPermissions($ncPermissions) {
527
+        $ocmPermissions = [];
528
+
529
+        if ($ncPermissions & Constants::PERMISSION_SHARE) {
530
+            $ocmPermissions[] = 'share';
531
+        }
532
+
533
+        if ($ncPermissions & Constants::PERMISSION_READ) {
534
+            $ocmPermissions[] = 'read';
535
+        }
536
+
537
+        if (($ncPermissions & Constants::PERMISSION_CREATE) ||
538
+            ($ncPermissions & Constants::PERMISSION_UPDATE)) {
539
+            $ocmPermissions[] = 'write';
540
+        }
541
+
542
+        return $ocmPermissions;
543
+    }
544
+
545
+    /**
546
+     * Update ownCloud-specific properties
547
+     *
548
+     * @param string $path
549
+     * @param PropPatch $propPatch
550
+     *
551
+     * @return void
552
+     */
553
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
554
+        $node = $this->tree->getNodeForPath($path);
555
+        if (!($node instanceof Node)) {
556
+            return;
557
+        }
558
+
559
+        $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
560
+            if (empty($time)) {
561
+                return false;
562
+            }
563
+            $node->touch($time);
564
+            return true;
565
+        });
566
+        $propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
567
+            if (empty($etag)) {
568
+                return false;
569
+            }
570
+            return $node->setEtag($etag) !== -1;
571
+        });
572
+        $propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
573
+            if (empty($time)) {
574
+                return false;
575
+            }
576
+            $dateTime = new \DateTimeImmutable($time);
577
+            $node->setCreationTime($dateTime->getTimestamp());
578
+            return true;
579
+        });
580
+        $propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
581
+            if (empty($time)) {
582
+                return false;
583
+            }
584
+            $node->setCreationTime((int)$time);
585
+            return true;
586
+        });
587
+
588
+        $this->handleUpdatePropertiesMetadata($propPatch, $node);
589
+
590
+        /**
591
+         * Disable modification of the displayname property for files and
592
+         * folders via PROPPATCH. See PROPFIND for more information.
593
+         */
594
+        $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
595
+            return 403;
596
+        });
597
+    }
598
+
599
+
600
+    /**
601
+     * handle the update of metadata from PROPPATCH requests
602
+     *
603
+     * @param PropPatch $propPatch
604
+     * @param Node $node
605
+     *
606
+     * @throws FilesMetadataException
607
+     */
608
+    private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
609
+        $userId = $this->userSession->getUser()?->getUID();
610
+        if ($userId === null) {
611
+            return;
612
+        }
613
+
614
+        $accessRight = $this->getMetadataFileAccessRight($node, $userId);
615
+        $filesMetadataManager = $this->initFilesMetadataManager();
616
+        $knownMetadata = $filesMetadataManager->getKnownMetadata();
617
+
618
+        foreach ($propPatch->getRemainingMutations() as $mutation) {
619
+            if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
620
+                continue;
621
+            }
622
+
623
+            $propPatch->handle(
624
+                $mutation,
625
+                function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
626
+                    /** @var FilesMetadata $metadata */
627
+                    $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
628
+                    $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
629
+                    $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
630
+
631
+                    // confirm metadata key is editable via PROPPATCH
632
+                    if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
633
+                        throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
634
+                    }
635
+
636
+                    if ($value === null) {
637
+                        $metadata->unset($metadataKey);
638
+                        $filesMetadataManager->saveMetadata($metadata);
639
+                        return true;
640
+                    }
641
+
642
+                    // If the metadata is unknown, it defaults to string.
643
+                    try {
644
+                        $type = $knownMetadata->getType($metadataKey);
645
+                    } catch (FilesMetadataNotFoundException) {
646
+                        $type = IMetadataValueWrapper::TYPE_STRING;
647
+                    }
648
+
649
+                    switch ($type) {
650
+                        case IMetadataValueWrapper::TYPE_STRING:
651
+                            $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
652
+                            break;
653
+                        case IMetadataValueWrapper::TYPE_INT:
654
+                            $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
655
+                            break;
656
+                        case IMetadataValueWrapper::TYPE_FLOAT:
657
+                            $metadata->setFloat($metadataKey, $value);
658
+                            break;
659
+                        case IMetadataValueWrapper::TYPE_BOOL:
660
+                            $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
661
+                            break;
662
+                        case IMetadataValueWrapper::TYPE_ARRAY:
663
+                            $metadata->setArray($metadataKey, $value);
664
+                            break;
665
+                        case IMetadataValueWrapper::TYPE_STRING_LIST:
666
+                            $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
667
+                            break;
668
+                        case IMetadataValueWrapper::TYPE_INT_LIST:
669
+                            $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
670
+                            break;
671
+                    }
672
+
673
+                    $filesMetadataManager->saveMetadata($metadata);
674
+
675
+                    return true;
676
+                }
677
+            );
678
+        }
679
+    }
680
+
681
+    /**
682
+     * init default internal metadata
683
+     *
684
+     * @return IFilesMetadataManager
685
+     */
686
+    private function initFilesMetadataManager(): IFilesMetadataManager {
687
+        /** @var IFilesMetadataManager $manager */
688
+        $manager = \OCP\Server::get(IFilesMetadataManager::class);
689
+        $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP);
690
+
691
+        return $manager;
692
+    }
693
+
694
+    /**
695
+     * based on owner and shares, returns the bottom limit to update related metadata
696
+     *
697
+     * @param Node $node
698
+     * @param string $userId
699
+     *
700
+     * @return int
701
+     */
702
+    private function getMetadataFileAccessRight(Node $node, string $userId): int {
703
+        if ($node->getOwner()?->getUID() === $userId) {
704
+            return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
705
+        } else {
706
+            $filePermissions = $node->getSharePermissions($userId);
707
+            if ($filePermissions & Constants::PERMISSION_UPDATE) {
708
+                return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
709
+            }
710
+        }
711
+
712
+        return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
713
+    }
714
+
715
+    /**
716
+     * @param string $filePath
717
+     * @param ?\Sabre\DAV\INode $node
718
+     * @return void
719
+     * @throws \Sabre\DAV\Exception\BadRequest
720
+     */
721
+    public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
722
+        // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
723
+        try {
724
+            $node = $this->server->tree->getNodeForPath($filePath);
725
+            if ($node instanceof Node) {
726
+                $fileId = $node->getFileId();
727
+                if (!is_null($fileId)) {
728
+                    $this->server->httpResponse->setHeader('OC-FileId', $fileId);
729
+                }
730
+            }
731
+        } catch (NotFound) {
732
+        }
733
+    }
734 734
 }
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/Principal.php 2 patches
Indentation   +568 added lines, -568 removed lines patch added patch discarded remove patch
@@ -32,572 +32,572 @@
 block discarded – undo
32 32
 
33 33
 class Principal implements BackendInterface {
34 34
 
35
-	/** @var string */
36
-	private $principalPrefix;
37
-
38
-	/** @var bool */
39
-	private $hasGroups;
40
-
41
-	/** @var bool */
42
-	private $hasCircles;
43
-
44
-	/** @var ProxyMapper */
45
-	private $proxyMapper;
46
-
47
-	/** @var KnownUserService */
48
-	private $knownUserService;
49
-
50
-	public function __construct(
51
-		private IUserManager $userManager,
52
-		private IGroupManager $groupManager,
53
-		private IAccountManager $accountManager,
54
-		private IShareManager $shareManager,
55
-		private IUserSession $userSession,
56
-		private IAppManager $appManager,
57
-		ProxyMapper $proxyMapper,
58
-		KnownUserService $knownUserService,
59
-		private IConfig $config,
60
-		private IFactory $languageFactory,
61
-		string $principalPrefix = 'principals/users/',
62
-	) {
63
-		$this->principalPrefix = trim($principalPrefix, '/');
64
-		$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
65
-		$this->proxyMapper = $proxyMapper;
66
-		$this->knownUserService = $knownUserService;
67
-	}
68
-
69
-	use PrincipalProxyTrait {
70
-		getGroupMembership as protected traitGetGroupMembership;
71
-	}
72
-
73
-	/**
74
-	 * Returns a list of principals based on a prefix.
75
-	 *
76
-	 * This prefix will often contain something like 'principals'. You are only
77
-	 * expected to return principals that are in this base path.
78
-	 *
79
-	 * You are expected to return at least a 'uri' for every user, you can
80
-	 * return any additional properties if you wish so. Common properties are:
81
-	 *   {DAV:}displayname
82
-	 *
83
-	 * @param string $prefixPath
84
-	 * @return string[]
85
-	 */
86
-	public function getPrincipalsByPrefix($prefixPath) {
87
-		$principals = [];
88
-
89
-		if ($prefixPath === $this->principalPrefix) {
90
-			foreach ($this->userManager->search('') as $user) {
91
-				$principals[] = $this->userToPrincipal($user);
92
-			}
93
-		}
94
-
95
-		return $principals;
96
-	}
97
-
98
-	/**
99
-	 * Returns a specific principal, specified by it's path.
100
-	 * The returned structure should be the exact same as from
101
-	 * getPrincipalsByPrefix.
102
-	 *
103
-	 * @param string $path
104
-	 * @return array
105
-	 */
106
-	public function getPrincipalByPath($path) {
107
-		[$prefix, $name] = \Sabre\Uri\split($path);
108
-		$decodedName = urldecode($name);
109
-
110
-		if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
111
-			[$prefix2, $name2] = \Sabre\Uri\split($prefix);
112
-
113
-			if ($prefix2 === $this->principalPrefix) {
114
-				$user = $this->userManager->get($name2);
115
-
116
-				if ($user !== null) {
117
-					return [
118
-						'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
119
-					];
120
-				}
121
-				return null;
122
-			}
123
-		}
124
-
125
-		if ($prefix === $this->principalPrefix) {
126
-			// Depending on where it is called, it may happen that this function
127
-			// is called either with a urlencoded version of the name or with a non-urlencoded one.
128
-			// The urldecode function replaces %## and +, both of which are forbidden in usernames.
129
-			// Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
130
-			$user = $this->userManager->get($decodedName);
131
-
132
-			if ($user !== null) {
133
-				return $this->userToPrincipal($user);
134
-			}
135
-		} elseif ($prefix === 'principals/circles') {
136
-			if ($this->userSession->getUser() !== null) {
137
-				// At the time of writing - 2021-01-19 — a mixed state is possible.
138
-				// The second condition can be removed when this is fixed.
139
-				return $this->circleToPrincipal($decodedName)
140
-					?: $this->circleToPrincipal($name);
141
-			}
142
-		} elseif ($prefix === 'principals/groups') {
143
-			// At the time of writing - 2021-01-19 — a mixed state is possible.
144
-			// The second condition can be removed when this is fixed.
145
-			$group = $this->groupManager->get($decodedName)
146
-				?: $this->groupManager->get($name);
147
-			if ($group instanceof IGroup) {
148
-				return [
149
-					'uri' => 'principals/groups/' . $name,
150
-					'{DAV:}displayname' => $group->getDisplayName(),
151
-				];
152
-			}
153
-		} elseif ($prefix === 'principals/system') {
154
-			return [
155
-				'uri' => 'principals/system/' . $name,
156
-				'{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
157
-			];
158
-		} elseif ($prefix === 'principals/shares') {
159
-			return [
160
-				'uri' => 'principals/shares/' . $name,
161
-				'{DAV:}displayname' => $name,
162
-			];
163
-		}
164
-		return null;
165
-	}
166
-
167
-	/**
168
-	 * Returns the list of groups a principal is a member of
169
-	 *
170
-	 * @param string $principal
171
-	 * @param bool $needGroups
172
-	 * @return array
173
-	 * @throws Exception
174
-	 */
175
-	public function getGroupMembership($principal, $needGroups = false) {
176
-		[$prefix, $name] = \Sabre\Uri\split($principal);
177
-
178
-		if ($prefix !== $this->principalPrefix) {
179
-			return [];
180
-		}
181
-
182
-		$user = $this->userManager->get($name);
183
-		if (!$user) {
184
-			throw new Exception('Principal not found');
185
-		}
186
-
187
-		$groups = [];
188
-
189
-		if ($this->hasGroups || $needGroups) {
190
-			$userGroups = $this->groupManager->getUserGroups($user);
191
-			foreach ($userGroups as $userGroup) {
192
-				$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
193
-			}
194
-		}
195
-
196
-		$groups = array_unique(array_merge(
197
-			$groups,
198
-			$this->traitGetGroupMembership($principal, $needGroups)
199
-		));
200
-
201
-		return $groups;
202
-	}
203
-
204
-	/**
205
-	 * @param string $path
206
-	 * @param PropPatch $propPatch
207
-	 * @return int
208
-	 */
209
-	public function updatePrincipal($path, PropPatch $propPatch) {
210
-		// Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
211
-		return 0;
212
-	}
213
-
214
-	/**
215
-	 * Search user principals
216
-	 *
217
-	 * @param array $searchProperties
218
-	 * @param string $test
219
-	 * @return array
220
-	 */
221
-	protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
222
-		$results = [];
223
-
224
-		// If sharing is disabled, return the empty array
225
-		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
226
-		if (!$shareAPIEnabled) {
227
-			return [];
228
-		}
229
-
230
-		$allowEnumeration = $this->shareManager->allowEnumeration();
231
-		$limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
232
-		$limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
233
-		$allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
234
-		$ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
235
-		$matchEmail = $this->shareManager->matchEmail();
236
-
237
-		// If sharing is restricted to group members only,
238
-		// return only members that have groups in common
239
-		$restrictGroups = false;
240
-		$currentUser = $this->userSession->getUser();
241
-		if ($this->shareManager->shareWithGroupMembersOnly()) {
242
-			if (!$currentUser instanceof IUser) {
243
-				return [];
244
-			}
245
-
246
-			$restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
247
-		}
248
-
249
-		$currentUserGroups = [];
250
-		if ($limitEnumerationGroup) {
251
-			if ($currentUser instanceof IUser) {
252
-				$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
253
-			}
254
-		}
255
-
256
-		$searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
257
-		if ($searchLimit <= 0) {
258
-			$searchLimit = null;
259
-		}
260
-		foreach ($searchProperties as $prop => $value) {
261
-			switch ($prop) {
262
-				case '{http://sabredav.org/ns}email-address':
263
-					if (!$allowEnumeration) {
264
-						if ($allowEnumerationFullMatch && $matchEmail) {
265
-							$users = $this->userManager->getByEmail($value);
266
-						} else {
267
-							$users = [];
268
-						}
269
-					} else {
270
-						$users = $this->userManager->getByEmail($value);
271
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
272
-							if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
273
-								return true;
274
-							}
275
-
276
-							if ($limitEnumerationPhone
277
-								&& $currentUser instanceof IUser
278
-								&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
279
-								// Synced phonebook match
280
-								return true;
281
-							}
282
-
283
-							if (!$limitEnumerationGroup) {
284
-								// No limitation on enumeration, all allowed
285
-								return true;
286
-							}
287
-
288
-							return !empty($currentUserGroups) && !empty(array_intersect(
289
-								$this->groupManager->getUserGroupIds($user),
290
-								$currentUserGroups
291
-							));
292
-						});
293
-					}
294
-
295
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
296
-						// is sharing restricted to groups only?
297
-						if ($restrictGroups !== false) {
298
-							$userGroups = $this->groupManager->getUserGroupIds($user);
299
-							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
300
-								return $carry;
301
-							}
302
-						}
303
-
304
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
305
-						return $carry;
306
-					}, []);
307
-					break;
308
-
309
-				case '{DAV:}displayname':
310
-
311
-					if (!$allowEnumeration) {
312
-						if ($allowEnumerationFullMatch) {
313
-							$lowerSearch = strtolower($value);
314
-							$users = $this->userManager->searchDisplayName($value, $searchLimit);
315
-							$users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
316
-								$lowerDisplayName = strtolower($user->getDisplayName());
317
-								return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
318
-							});
319
-						} else {
320
-							$users = [];
321
-						}
322
-					} else {
323
-						$users = $this->userManager->searchDisplayName($value, $searchLimit);
324
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
325
-							if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
326
-								return true;
327
-							}
328
-
329
-							if ($limitEnumerationPhone
330
-								&& $currentUser instanceof IUser
331
-								&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
332
-								// Synced phonebook match
333
-								return true;
334
-							}
335
-
336
-							if (!$limitEnumerationGroup) {
337
-								// No limitation on enumeration, all allowed
338
-								return true;
339
-							}
340
-
341
-							return !empty($currentUserGroups) && !empty(array_intersect(
342
-								$this->groupManager->getUserGroupIds($user),
343
-								$currentUserGroups
344
-							));
345
-						});
346
-					}
347
-
348
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
349
-						// is sharing restricted to groups only?
350
-						if ($restrictGroups !== false) {
351
-							$userGroups = $this->groupManager->getUserGroupIds($user);
352
-							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
353
-								return $carry;
354
-							}
355
-						}
356
-
357
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
358
-						return $carry;
359
-					}, []);
360
-					break;
361
-
362
-				case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
363
-					// If you add support for more search properties that qualify as a user-address,
364
-					// please also add them to the array below
365
-					$results[] = $this->searchUserPrincipals([
366
-						// In theory this should also search for principal:principals/users/...
367
-						// but that's used internally only anyway and i don't know of any client querying that
368
-						'{http://sabredav.org/ns}email-address' => $value,
369
-					], 'anyof');
370
-					break;
371
-
372
-				default:
373
-					$results[] = [];
374
-					break;
375
-			}
376
-		}
377
-
378
-		// results is an array of arrays, so this is not the first search result
379
-		// but the results of the first searchProperty
380
-		if (count($results) === 1) {
381
-			return $results[0];
382
-		}
383
-
384
-		switch ($test) {
385
-			case 'anyof':
386
-				return array_values(array_unique(array_merge(...$results)));
387
-
388
-			case 'allof':
389
-			default:
390
-				return array_values(array_intersect(...$results));
391
-		}
392
-	}
393
-
394
-	/**
395
-	 * @param string $prefixPath
396
-	 * @param array $searchProperties
397
-	 * @param string $test
398
-	 * @return array
399
-	 */
400
-	public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
401
-		if (count($searchProperties) === 0) {
402
-			return [];
403
-		}
404
-
405
-		switch ($prefixPath) {
406
-			case 'principals/users':
407
-				return $this->searchUserPrincipals($searchProperties, $test);
408
-
409
-			default:
410
-				return [];
411
-		}
412
-	}
413
-
414
-	/**
415
-	 * @param string $uri
416
-	 * @param string $principalPrefix
417
-	 * @return string
418
-	 */
419
-	public function findByUri($uri, $principalPrefix) {
420
-		// If sharing is disabled, return the empty array
421
-		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
422
-		if (!$shareAPIEnabled) {
423
-			return null;
424
-		}
425
-
426
-		// If sharing is restricted to group members only,
427
-		// return only members that have groups in common
428
-		$restrictGroups = false;
429
-		if ($this->shareManager->shareWithGroupMembersOnly()) {
430
-			$user = $this->userSession->getUser();
431
-			if (!$user) {
432
-				return null;
433
-			}
434
-
435
-			$restrictGroups = $this->groupManager->getUserGroupIds($user);
436
-		}
437
-
438
-		if (str_starts_with($uri, 'mailto:')) {
439
-			if ($principalPrefix === 'principals/users') {
440
-				$users = $this->userManager->getByEmail(substr($uri, 7));
441
-				if (count($users) !== 1) {
442
-					return null;
443
-				}
444
-				$user = $users[0];
445
-
446
-				if ($restrictGroups !== false) {
447
-					$userGroups = $this->groupManager->getUserGroupIds($user);
448
-					if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
449
-						return null;
450
-					}
451
-				}
452
-
453
-				return $this->principalPrefix . '/' . $user->getUID();
454
-			}
455
-		}
456
-		if (str_starts_with($uri, 'principal:')) {
457
-			$principal = substr($uri, 10);
458
-			$principal = $this->getPrincipalByPath($principal);
459
-			if ($principal !== null) {
460
-				return $principal['uri'];
461
-			}
462
-		}
463
-
464
-		return null;
465
-	}
466
-
467
-	/**
468
-	 * @param IUser $user
469
-	 * @return array
470
-	 * @throws PropertyDoesNotExistException
471
-	 */
472
-	protected function userToPrincipal($user) {
473
-		$userId = $user->getUID();
474
-		$displayName = $user->getDisplayName();
475
-		$principal = [
476
-			'uri' => $this->principalPrefix . '/' . $userId,
477
-			'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
478
-			'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
479
-			'{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
480
-		];
481
-
482
-		$account = $this->accountManager->getAccount($user);
483
-		$alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
484
-
485
-		$email = $user->getSystemEMailAddress();
486
-		if (!empty($email)) {
487
-			$principal['{http://sabredav.org/ns}email-address'] = $email;
488
-		}
489
-
490
-		if (!empty($alternativeEmails)) {
491
-			$principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
492
-		}
493
-
494
-		return $principal;
495
-	}
496
-
497
-	public function getPrincipalPrefix() {
498
-		return $this->principalPrefix;
499
-	}
500
-
501
-	/**
502
-	 * @param string $circleUniqueId
503
-	 * @return array|null
504
-	 */
505
-	protected function circleToPrincipal($circleUniqueId) {
506
-		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
507
-			return null;
508
-		}
509
-
510
-		try {
511
-			$circle = Circles::detailsCircle($circleUniqueId, true);
512
-		} catch (QueryException $ex) {
513
-			return null;
514
-		} catch (CircleNotFoundException $ex) {
515
-			return null;
516
-		}
517
-
518
-		if (!$circle) {
519
-			return null;
520
-		}
521
-
522
-		$principal = [
523
-			'uri' => 'principals/circles/' . $circleUniqueId,
524
-			'{DAV:}displayname' => $circle->getDisplayName(),
525
-		];
526
-
527
-		return $principal;
528
-	}
529
-
530
-	/**
531
-	 * Returns the list of circles a principal is a member of
532
-	 *
533
-	 * @param string $principal
534
-	 * @return array
535
-	 * @throws Exception
536
-	 * @throws QueryException
537
-	 * @suppress PhanUndeclaredClassMethod
538
-	 */
539
-	public function getCircleMembership($principal):array {
540
-		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
541
-			return [];
542
-		}
543
-
544
-		[$prefix, $name] = \Sabre\Uri\split($principal);
545
-		if ($this->hasCircles && $prefix === $this->principalPrefix) {
546
-			$user = $this->userManager->get($name);
547
-			if (!$user) {
548
-				throw new Exception('Principal not found');
549
-			}
550
-
551
-			$circles = Circles::joinedCircles($name, true);
552
-
553
-			$circles = array_map(function ($circle) {
554
-				/** @var Circle $circle */
555
-				return 'principals/circles/' . urlencode($circle->getSingleId());
556
-			}, $circles);
557
-
558
-			return $circles;
559
-		}
560
-
561
-		return [];
562
-	}
563
-
564
-	/**
565
-	 * Get all email addresses associated to a principal.
566
-	 *
567
-	 * @param array $principal Data from getPrincipal*()
568
-	 * @return string[] All email addresses without the mailto: prefix
569
-	 */
570
-	public function getEmailAddressesOfPrincipal(array $principal): array {
571
-		$emailAddresses = [];
572
-
573
-		if (isset($principal['{http://sabredav.org/ns}email-address'])) {
574
-			$emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
575
-		}
576
-
577
-		if (isset($principal['{DAV:}alternate-URI-set'])) {
578
-			foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
579
-				if (str_starts_with($address, 'mailto:')) {
580
-					$emailAddresses[] = substr($address, 7);
581
-				}
582
-			}
583
-		}
584
-
585
-		if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
586
-			foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
587
-				if (str_starts_with($address, 'mailto:')) {
588
-					$emailAddresses[] = substr($address, 7);
589
-				}
590
-			}
591
-		}
592
-
593
-		if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
594
-			foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
595
-				if (str_starts_with($address, 'mailto:')) {
596
-					$emailAddresses[] = substr($address, 7);
597
-				}
598
-			}
599
-		}
600
-
601
-		return array_values(array_unique($emailAddresses));
602
-	}
35
+    /** @var string */
36
+    private $principalPrefix;
37
+
38
+    /** @var bool */
39
+    private $hasGroups;
40
+
41
+    /** @var bool */
42
+    private $hasCircles;
43
+
44
+    /** @var ProxyMapper */
45
+    private $proxyMapper;
46
+
47
+    /** @var KnownUserService */
48
+    private $knownUserService;
49
+
50
+    public function __construct(
51
+        private IUserManager $userManager,
52
+        private IGroupManager $groupManager,
53
+        private IAccountManager $accountManager,
54
+        private IShareManager $shareManager,
55
+        private IUserSession $userSession,
56
+        private IAppManager $appManager,
57
+        ProxyMapper $proxyMapper,
58
+        KnownUserService $knownUserService,
59
+        private IConfig $config,
60
+        private IFactory $languageFactory,
61
+        string $principalPrefix = 'principals/users/',
62
+    ) {
63
+        $this->principalPrefix = trim($principalPrefix, '/');
64
+        $this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
65
+        $this->proxyMapper = $proxyMapper;
66
+        $this->knownUserService = $knownUserService;
67
+    }
68
+
69
+    use PrincipalProxyTrait {
70
+        getGroupMembership as protected traitGetGroupMembership;
71
+    }
72
+
73
+    /**
74
+     * Returns a list of principals based on a prefix.
75
+     *
76
+     * This prefix will often contain something like 'principals'. You are only
77
+     * expected to return principals that are in this base path.
78
+     *
79
+     * You are expected to return at least a 'uri' for every user, you can
80
+     * return any additional properties if you wish so. Common properties are:
81
+     *   {DAV:}displayname
82
+     *
83
+     * @param string $prefixPath
84
+     * @return string[]
85
+     */
86
+    public function getPrincipalsByPrefix($prefixPath) {
87
+        $principals = [];
88
+
89
+        if ($prefixPath === $this->principalPrefix) {
90
+            foreach ($this->userManager->search('') as $user) {
91
+                $principals[] = $this->userToPrincipal($user);
92
+            }
93
+        }
94
+
95
+        return $principals;
96
+    }
97
+
98
+    /**
99
+     * Returns a specific principal, specified by it's path.
100
+     * The returned structure should be the exact same as from
101
+     * getPrincipalsByPrefix.
102
+     *
103
+     * @param string $path
104
+     * @return array
105
+     */
106
+    public function getPrincipalByPath($path) {
107
+        [$prefix, $name] = \Sabre\Uri\split($path);
108
+        $decodedName = urldecode($name);
109
+
110
+        if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
111
+            [$prefix2, $name2] = \Sabre\Uri\split($prefix);
112
+
113
+            if ($prefix2 === $this->principalPrefix) {
114
+                $user = $this->userManager->get($name2);
115
+
116
+                if ($user !== null) {
117
+                    return [
118
+                        'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
119
+                    ];
120
+                }
121
+                return null;
122
+            }
123
+        }
124
+
125
+        if ($prefix === $this->principalPrefix) {
126
+            // Depending on where it is called, it may happen that this function
127
+            // is called either with a urlencoded version of the name or with a non-urlencoded one.
128
+            // The urldecode function replaces %## and +, both of which are forbidden in usernames.
129
+            // Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
130
+            $user = $this->userManager->get($decodedName);
131
+
132
+            if ($user !== null) {
133
+                return $this->userToPrincipal($user);
134
+            }
135
+        } elseif ($prefix === 'principals/circles') {
136
+            if ($this->userSession->getUser() !== null) {
137
+                // At the time of writing - 2021-01-19 — a mixed state is possible.
138
+                // The second condition can be removed when this is fixed.
139
+                return $this->circleToPrincipal($decodedName)
140
+                    ?: $this->circleToPrincipal($name);
141
+            }
142
+        } elseif ($prefix === 'principals/groups') {
143
+            // At the time of writing - 2021-01-19 — a mixed state is possible.
144
+            // The second condition can be removed when this is fixed.
145
+            $group = $this->groupManager->get($decodedName)
146
+                ?: $this->groupManager->get($name);
147
+            if ($group instanceof IGroup) {
148
+                return [
149
+                    'uri' => 'principals/groups/' . $name,
150
+                    '{DAV:}displayname' => $group->getDisplayName(),
151
+                ];
152
+            }
153
+        } elseif ($prefix === 'principals/system') {
154
+            return [
155
+                'uri' => 'principals/system/' . $name,
156
+                '{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
157
+            ];
158
+        } elseif ($prefix === 'principals/shares') {
159
+            return [
160
+                'uri' => 'principals/shares/' . $name,
161
+                '{DAV:}displayname' => $name,
162
+            ];
163
+        }
164
+        return null;
165
+    }
166
+
167
+    /**
168
+     * Returns the list of groups a principal is a member of
169
+     *
170
+     * @param string $principal
171
+     * @param bool $needGroups
172
+     * @return array
173
+     * @throws Exception
174
+     */
175
+    public function getGroupMembership($principal, $needGroups = false) {
176
+        [$prefix, $name] = \Sabre\Uri\split($principal);
177
+
178
+        if ($prefix !== $this->principalPrefix) {
179
+            return [];
180
+        }
181
+
182
+        $user = $this->userManager->get($name);
183
+        if (!$user) {
184
+            throw new Exception('Principal not found');
185
+        }
186
+
187
+        $groups = [];
188
+
189
+        if ($this->hasGroups || $needGroups) {
190
+            $userGroups = $this->groupManager->getUserGroups($user);
191
+            foreach ($userGroups as $userGroup) {
192
+                $groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
193
+            }
194
+        }
195
+
196
+        $groups = array_unique(array_merge(
197
+            $groups,
198
+            $this->traitGetGroupMembership($principal, $needGroups)
199
+        ));
200
+
201
+        return $groups;
202
+    }
203
+
204
+    /**
205
+     * @param string $path
206
+     * @param PropPatch $propPatch
207
+     * @return int
208
+     */
209
+    public function updatePrincipal($path, PropPatch $propPatch) {
210
+        // Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
211
+        return 0;
212
+    }
213
+
214
+    /**
215
+     * Search user principals
216
+     *
217
+     * @param array $searchProperties
218
+     * @param string $test
219
+     * @return array
220
+     */
221
+    protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
222
+        $results = [];
223
+
224
+        // If sharing is disabled, return the empty array
225
+        $shareAPIEnabled = $this->shareManager->shareApiEnabled();
226
+        if (!$shareAPIEnabled) {
227
+            return [];
228
+        }
229
+
230
+        $allowEnumeration = $this->shareManager->allowEnumeration();
231
+        $limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
232
+        $limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
233
+        $allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
234
+        $ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
235
+        $matchEmail = $this->shareManager->matchEmail();
236
+
237
+        // If sharing is restricted to group members only,
238
+        // return only members that have groups in common
239
+        $restrictGroups = false;
240
+        $currentUser = $this->userSession->getUser();
241
+        if ($this->shareManager->shareWithGroupMembersOnly()) {
242
+            if (!$currentUser instanceof IUser) {
243
+                return [];
244
+            }
245
+
246
+            $restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
247
+        }
248
+
249
+        $currentUserGroups = [];
250
+        if ($limitEnumerationGroup) {
251
+            if ($currentUser instanceof IUser) {
252
+                $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
253
+            }
254
+        }
255
+
256
+        $searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
257
+        if ($searchLimit <= 0) {
258
+            $searchLimit = null;
259
+        }
260
+        foreach ($searchProperties as $prop => $value) {
261
+            switch ($prop) {
262
+                case '{http://sabredav.org/ns}email-address':
263
+                    if (!$allowEnumeration) {
264
+                        if ($allowEnumerationFullMatch && $matchEmail) {
265
+                            $users = $this->userManager->getByEmail($value);
266
+                        } else {
267
+                            $users = [];
268
+                        }
269
+                    } else {
270
+                        $users = $this->userManager->getByEmail($value);
271
+                        $users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
272
+                            if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
273
+                                return true;
274
+                            }
275
+
276
+                            if ($limitEnumerationPhone
277
+                                && $currentUser instanceof IUser
278
+                                && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
279
+                                // Synced phonebook match
280
+                                return true;
281
+                            }
282
+
283
+                            if (!$limitEnumerationGroup) {
284
+                                // No limitation on enumeration, all allowed
285
+                                return true;
286
+                            }
287
+
288
+                            return !empty($currentUserGroups) && !empty(array_intersect(
289
+                                $this->groupManager->getUserGroupIds($user),
290
+                                $currentUserGroups
291
+                            ));
292
+                        });
293
+                    }
294
+
295
+                    $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
296
+                        // is sharing restricted to groups only?
297
+                        if ($restrictGroups !== false) {
298
+                            $userGroups = $this->groupManager->getUserGroupIds($user);
299
+                            if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
300
+                                return $carry;
301
+                            }
302
+                        }
303
+
304
+                        $carry[] = $this->principalPrefix . '/' . $user->getUID();
305
+                        return $carry;
306
+                    }, []);
307
+                    break;
308
+
309
+                case '{DAV:}displayname':
310
+
311
+                    if (!$allowEnumeration) {
312
+                        if ($allowEnumerationFullMatch) {
313
+                            $lowerSearch = strtolower($value);
314
+                            $users = $this->userManager->searchDisplayName($value, $searchLimit);
315
+                            $users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
316
+                                $lowerDisplayName = strtolower($user->getDisplayName());
317
+                                return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
318
+                            });
319
+                        } else {
320
+                            $users = [];
321
+                        }
322
+                    } else {
323
+                        $users = $this->userManager->searchDisplayName($value, $searchLimit);
324
+                        $users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
325
+                            if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
326
+                                return true;
327
+                            }
328
+
329
+                            if ($limitEnumerationPhone
330
+                                && $currentUser instanceof IUser
331
+                                && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
332
+                                // Synced phonebook match
333
+                                return true;
334
+                            }
335
+
336
+                            if (!$limitEnumerationGroup) {
337
+                                // No limitation on enumeration, all allowed
338
+                                return true;
339
+                            }
340
+
341
+                            return !empty($currentUserGroups) && !empty(array_intersect(
342
+                                $this->groupManager->getUserGroupIds($user),
343
+                                $currentUserGroups
344
+                            ));
345
+                        });
346
+                    }
347
+
348
+                    $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
349
+                        // is sharing restricted to groups only?
350
+                        if ($restrictGroups !== false) {
351
+                            $userGroups = $this->groupManager->getUserGroupIds($user);
352
+                            if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
353
+                                return $carry;
354
+                            }
355
+                        }
356
+
357
+                        $carry[] = $this->principalPrefix . '/' . $user->getUID();
358
+                        return $carry;
359
+                    }, []);
360
+                    break;
361
+
362
+                case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
363
+                    // If you add support for more search properties that qualify as a user-address,
364
+                    // please also add them to the array below
365
+                    $results[] = $this->searchUserPrincipals([
366
+                        // In theory this should also search for principal:principals/users/...
367
+                        // but that's used internally only anyway and i don't know of any client querying that
368
+                        '{http://sabredav.org/ns}email-address' => $value,
369
+                    ], 'anyof');
370
+                    break;
371
+
372
+                default:
373
+                    $results[] = [];
374
+                    break;
375
+            }
376
+        }
377
+
378
+        // results is an array of arrays, so this is not the first search result
379
+        // but the results of the first searchProperty
380
+        if (count($results) === 1) {
381
+            return $results[0];
382
+        }
383
+
384
+        switch ($test) {
385
+            case 'anyof':
386
+                return array_values(array_unique(array_merge(...$results)));
387
+
388
+            case 'allof':
389
+            default:
390
+                return array_values(array_intersect(...$results));
391
+        }
392
+    }
393
+
394
+    /**
395
+     * @param string $prefixPath
396
+     * @param array $searchProperties
397
+     * @param string $test
398
+     * @return array
399
+     */
400
+    public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
401
+        if (count($searchProperties) === 0) {
402
+            return [];
403
+        }
404
+
405
+        switch ($prefixPath) {
406
+            case 'principals/users':
407
+                return $this->searchUserPrincipals($searchProperties, $test);
408
+
409
+            default:
410
+                return [];
411
+        }
412
+    }
413
+
414
+    /**
415
+     * @param string $uri
416
+     * @param string $principalPrefix
417
+     * @return string
418
+     */
419
+    public function findByUri($uri, $principalPrefix) {
420
+        // If sharing is disabled, return the empty array
421
+        $shareAPIEnabled = $this->shareManager->shareApiEnabled();
422
+        if (!$shareAPIEnabled) {
423
+            return null;
424
+        }
425
+
426
+        // If sharing is restricted to group members only,
427
+        // return only members that have groups in common
428
+        $restrictGroups = false;
429
+        if ($this->shareManager->shareWithGroupMembersOnly()) {
430
+            $user = $this->userSession->getUser();
431
+            if (!$user) {
432
+                return null;
433
+            }
434
+
435
+            $restrictGroups = $this->groupManager->getUserGroupIds($user);
436
+        }
437
+
438
+        if (str_starts_with($uri, 'mailto:')) {
439
+            if ($principalPrefix === 'principals/users') {
440
+                $users = $this->userManager->getByEmail(substr($uri, 7));
441
+                if (count($users) !== 1) {
442
+                    return null;
443
+                }
444
+                $user = $users[0];
445
+
446
+                if ($restrictGroups !== false) {
447
+                    $userGroups = $this->groupManager->getUserGroupIds($user);
448
+                    if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
449
+                        return null;
450
+                    }
451
+                }
452
+
453
+                return $this->principalPrefix . '/' . $user->getUID();
454
+            }
455
+        }
456
+        if (str_starts_with($uri, 'principal:')) {
457
+            $principal = substr($uri, 10);
458
+            $principal = $this->getPrincipalByPath($principal);
459
+            if ($principal !== null) {
460
+                return $principal['uri'];
461
+            }
462
+        }
463
+
464
+        return null;
465
+    }
466
+
467
+    /**
468
+     * @param IUser $user
469
+     * @return array
470
+     * @throws PropertyDoesNotExistException
471
+     */
472
+    protected function userToPrincipal($user) {
473
+        $userId = $user->getUID();
474
+        $displayName = $user->getDisplayName();
475
+        $principal = [
476
+            'uri' => $this->principalPrefix . '/' . $userId,
477
+            '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
478
+            '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
479
+            '{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
480
+        ];
481
+
482
+        $account = $this->accountManager->getAccount($user);
483
+        $alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
484
+
485
+        $email = $user->getSystemEMailAddress();
486
+        if (!empty($email)) {
487
+            $principal['{http://sabredav.org/ns}email-address'] = $email;
488
+        }
489
+
490
+        if (!empty($alternativeEmails)) {
491
+            $principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
492
+        }
493
+
494
+        return $principal;
495
+    }
496
+
497
+    public function getPrincipalPrefix() {
498
+        return $this->principalPrefix;
499
+    }
500
+
501
+    /**
502
+     * @param string $circleUniqueId
503
+     * @return array|null
504
+     */
505
+    protected function circleToPrincipal($circleUniqueId) {
506
+        if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
507
+            return null;
508
+        }
509
+
510
+        try {
511
+            $circle = Circles::detailsCircle($circleUniqueId, true);
512
+        } catch (QueryException $ex) {
513
+            return null;
514
+        } catch (CircleNotFoundException $ex) {
515
+            return null;
516
+        }
517
+
518
+        if (!$circle) {
519
+            return null;
520
+        }
521
+
522
+        $principal = [
523
+            'uri' => 'principals/circles/' . $circleUniqueId,
524
+            '{DAV:}displayname' => $circle->getDisplayName(),
525
+        ];
526
+
527
+        return $principal;
528
+    }
529
+
530
+    /**
531
+     * Returns the list of circles a principal is a member of
532
+     *
533
+     * @param string $principal
534
+     * @return array
535
+     * @throws Exception
536
+     * @throws QueryException
537
+     * @suppress PhanUndeclaredClassMethod
538
+     */
539
+    public function getCircleMembership($principal):array {
540
+        if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
541
+            return [];
542
+        }
543
+
544
+        [$prefix, $name] = \Sabre\Uri\split($principal);
545
+        if ($this->hasCircles && $prefix === $this->principalPrefix) {
546
+            $user = $this->userManager->get($name);
547
+            if (!$user) {
548
+                throw new Exception('Principal not found');
549
+            }
550
+
551
+            $circles = Circles::joinedCircles($name, true);
552
+
553
+            $circles = array_map(function ($circle) {
554
+                /** @var Circle $circle */
555
+                return 'principals/circles/' . urlencode($circle->getSingleId());
556
+            }, $circles);
557
+
558
+            return $circles;
559
+        }
560
+
561
+        return [];
562
+    }
563
+
564
+    /**
565
+     * Get all email addresses associated to a principal.
566
+     *
567
+     * @param array $principal Data from getPrincipal*()
568
+     * @return string[] All email addresses without the mailto: prefix
569
+     */
570
+    public function getEmailAddressesOfPrincipal(array $principal): array {
571
+        $emailAddresses = [];
572
+
573
+        if (isset($principal['{http://sabredav.org/ns}email-address'])) {
574
+            $emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
575
+        }
576
+
577
+        if (isset($principal['{DAV:}alternate-URI-set'])) {
578
+            foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
579
+                if (str_starts_with($address, 'mailto:')) {
580
+                    $emailAddresses[] = substr($address, 7);
581
+                }
582
+            }
583
+        }
584
+
585
+        if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
586
+            foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
587
+                if (str_starts_with($address, 'mailto:')) {
588
+                    $emailAddresses[] = substr($address, 7);
589
+                }
590
+            }
591
+        }
592
+
593
+        if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
594
+            foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
595
+                if (str_starts_with($address, 'mailto:')) {
596
+                    $emailAddresses[] = substr($address, 7);
597
+                }
598
+            }
599
+        }
600
+
601
+        return array_values(array_unique($emailAddresses));
602
+    }
603 603
 }
Please login to merge, or discard this patch.
Spacing   +18 added lines, -18 removed lines patch added patch discarded remove patch
@@ -115,7 +115,7 @@  discard block
 block discarded – undo
115 115
 
116 116
 				if ($user !== null) {
117 117
 					return [
118
-						'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
118
+						'uri' => 'principals/users/'.$user->getUID().'/'.$name,
119 119
 					];
120 120
 				}
121 121
 				return null;
@@ -146,18 +146,18 @@  discard block
 block discarded – undo
146 146
 				?: $this->groupManager->get($name);
147 147
 			if ($group instanceof IGroup) {
148 148
 				return [
149
-					'uri' => 'principals/groups/' . $name,
149
+					'uri' => 'principals/groups/'.$name,
150 150
 					'{DAV:}displayname' => $group->getDisplayName(),
151 151
 				];
152 152
 			}
153 153
 		} elseif ($prefix === 'principals/system') {
154 154
 			return [
155
-				'uri' => 'principals/system/' . $name,
155
+				'uri' => 'principals/system/'.$name,
156 156
 				'{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
157 157
 			];
158 158
 		} elseif ($prefix === 'principals/shares') {
159 159
 			return [
160
-				'uri' => 'principals/shares/' . $name,
160
+				'uri' => 'principals/shares/'.$name,
161 161
 				'{DAV:}displayname' => $name,
162 162
 			];
163 163
 		}
@@ -189,7 +189,7 @@  discard block
 block discarded – undo
189 189
 		if ($this->hasGroups || $needGroups) {
190 190
 			$userGroups = $this->groupManager->getUserGroups($user);
191 191
 			foreach ($userGroups as $userGroup) {
192
-				$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
192
+				$groups[] = 'principals/groups/'.urlencode($userGroup->getGID());
193 193
 			}
194 194
 		}
195 195
 
@@ -268,7 +268,7 @@  discard block
 block discarded – undo
268 268
 						}
269 269
 					} else {
270 270
 						$users = $this->userManager->getByEmail($value);
271
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
271
+						$users = \array_filter($users, function(IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
272 272
 							if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
273 273
 								return true;
274 274
 							}
@@ -292,7 +292,7 @@  discard block
 block discarded – undo
292 292
 						});
293 293
 					}
294 294
 
295
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
295
+					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
296 296
 						// is sharing restricted to groups only?
297 297
 						if ($restrictGroups !== false) {
298 298
 							$userGroups = $this->groupManager->getUserGroupIds($user);
@@ -301,7 +301,7 @@  discard block
 block discarded – undo
301 301
 							}
302 302
 						}
303 303
 
304
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
304
+						$carry[] = $this->principalPrefix.'/'.$user->getUID();
305 305
 						return $carry;
306 306
 					}, []);
307 307
 					break;
@@ -312,7 +312,7 @@  discard block
 block discarded – undo
312 312
 						if ($allowEnumerationFullMatch) {
313 313
 							$lowerSearch = strtolower($value);
314 314
 							$users = $this->userManager->searchDisplayName($value, $searchLimit);
315
-							$users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
315
+							$users = \array_filter($users, static function(IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
316 316
 								$lowerDisplayName = strtolower($user->getDisplayName());
317 317
 								return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
318 318
 							});
@@ -321,7 +321,7 @@  discard block
 block discarded – undo
321 321
 						}
322 322
 					} else {
323 323
 						$users = $this->userManager->searchDisplayName($value, $searchLimit);
324
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
324
+						$users = \array_filter($users, function(IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
325 325
 							if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
326 326
 								return true;
327 327
 							}
@@ -345,7 +345,7 @@  discard block
 block discarded – undo
345 345
 						});
346 346
 					}
347 347
 
348
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
348
+					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
349 349
 						// is sharing restricted to groups only?
350 350
 						if ($restrictGroups !== false) {
351 351
 							$userGroups = $this->groupManager->getUserGroupIds($user);
@@ -354,7 +354,7 @@  discard block
 block discarded – undo
354 354
 							}
355 355
 						}
356 356
 
357
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
357
+						$carry[] = $this->principalPrefix.'/'.$user->getUID();
358 358
 						return $carry;
359 359
 					}, []);
360 360
 					break;
@@ -450,7 +450,7 @@  discard block
 block discarded – undo
450 450
 					}
451 451
 				}
452 452
 
453
-				return $this->principalPrefix . '/' . $user->getUID();
453
+				return $this->principalPrefix.'/'.$user->getUID();
454 454
 			}
455 455
 		}
456 456
 		if (str_starts_with($uri, 'principal:')) {
@@ -473,14 +473,14 @@  discard block
 block discarded – undo
473 473
 		$userId = $user->getUID();
474 474
 		$displayName = $user->getDisplayName();
475 475
 		$principal = [
476
-			'uri' => $this->principalPrefix . '/' . $userId,
476
+			'uri' => $this->principalPrefix.'/'.$userId,
477 477
 			'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
478 478
 			'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
479 479
 			'{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
480 480
 		];
481 481
 
482 482
 		$account = $this->accountManager->getAccount($user);
483
-		$alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
483
+		$alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:'.$property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
484 484
 
485 485
 		$email = $user->getSystemEMailAddress();
486 486
 		if (!empty($email)) {
@@ -520,7 +520,7 @@  discard block
 block discarded – undo
520 520
 		}
521 521
 
522 522
 		$principal = [
523
-			'uri' => 'principals/circles/' . $circleUniqueId,
523
+			'uri' => 'principals/circles/'.$circleUniqueId,
524 524
 			'{DAV:}displayname' => $circle->getDisplayName(),
525 525
 		];
526 526
 
@@ -550,9 +550,9 @@  discard block
 block discarded – undo
550 550
 
551 551
 			$circles = Circles::joinedCircles($name, true);
552 552
 
553
-			$circles = array_map(function ($circle) {
553
+			$circles = array_map(function($circle) {
554 554
 				/** @var Circle $circle */
555
-				return 'principals/circles/' . urlencode($circle->getSingleId());
555
+				return 'principals/circles/'.urlencode($circle->getSingleId());
556 556
 			}, $circles);
557 557
 
558 558
 			return $circles;
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/Directory.php 1 patch
Indentation   +449 added lines, -449 removed lines patch added patch discarded remove patch
@@ -40,453 +40,453 @@
 block discarded – undo
40 40
 use Sabre\DAV\INode;
41 41
 
42 42
 class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
43
-	/**
44
-	 * Cached directory content
45
-	 * @var FileInfo[]
46
-	 */
47
-	private ?array $dirContent = null;
48
-
49
-	/** Cached quota info */
50
-	private ?array $quotaInfo = null;
51
-
52
-	/**
53
-	 * Sets up the node, expects a full path name
54
-	 */
55
-	public function __construct(
56
-		View $view,
57
-		FileInfo $info,
58
-		private ?CachingTree $tree = null,
59
-		?IShareManager $shareManager = null,
60
-	) {
61
-		parent::__construct($view, $info, $shareManager);
62
-	}
63
-
64
-	/**
65
-	 * Creates a new file in the directory
66
-	 *
67
-	 * Data will either be supplied as a stream resource, or in certain cases
68
-	 * as a string. Keep in mind that you may have to support either.
69
-	 *
70
-	 * After successful creation of the file, you may choose to return the ETag
71
-	 * of the new file here.
72
-	 *
73
-	 * The returned ETag must be surrounded by double-quotes (The quotes should
74
-	 * be part of the actual string).
75
-	 *
76
-	 * If you cannot accurately determine the ETag, you should not return it.
77
-	 * If you don't store the file exactly as-is (you're transforming it
78
-	 * somehow) you should also not return an ETag.
79
-	 *
80
-	 * This means that if a subsequent GET to this new file does not exactly
81
-	 * return the same contents of what was submitted here, you are strongly
82
-	 * recommended to omit the ETag.
83
-	 *
84
-	 * @param string $name Name of the file
85
-	 * @param resource|string $data Initial payload
86
-	 * @return null|string
87
-	 * @throws Exception\EntityTooLarge
88
-	 * @throws Exception\UnsupportedMediaType
89
-	 * @throws FileLocked
90
-	 * @throws InvalidPath
91
-	 * @throws \Sabre\DAV\Exception
92
-	 * @throws \Sabre\DAV\Exception\BadRequest
93
-	 * @throws \Sabre\DAV\Exception\Forbidden
94
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
95
-	 */
96
-	public function createFile($name, $data = null) {
97
-		try {
98
-			if (!$this->fileView->isCreatable($this->path)) {
99
-				throw new \Sabre\DAV\Exception\Forbidden();
100
-			}
101
-
102
-			$this->fileView->verifyPath($this->path, $name);
103
-
104
-			$path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
105
-			// in case the file already exists/overwriting
106
-			$info = $this->fileView->getFileInfo($this->path . '/' . $name);
107
-			if (!$info) {
108
-				// use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
109
-				$info = new \OC\Files\FileInfo($path, null, null, [
110
-					'type' => FileInfo::TYPE_FILE
111
-				], null);
112
-			}
113
-			$node = new File($this->fileView, $info);
114
-
115
-			// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
116
-			$node->acquireLock(ILockingProvider::LOCK_SHARED);
117
-			$this->fileView->lockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
118
-
119
-			$result = $node->put($data);
120
-
121
-			$this->fileView->unlockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
122
-			$node->releaseLock(ILockingProvider::LOCK_SHARED);
123
-			return $result;
124
-		} catch (StorageNotAvailableException $e) {
125
-			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
126
-		} catch (InvalidPathException $ex) {
127
-			throw new InvalidPath($ex->getMessage(), false, $ex);
128
-		} catch (ForbiddenException $ex) {
129
-			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
130
-		} catch (LockedException $e) {
131
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
132
-		}
133
-	}
134
-
135
-	/**
136
-	 * Creates a new subdirectory
137
-	 *
138
-	 * @param string $name
139
-	 * @throws FileLocked
140
-	 * @throws InvalidPath
141
-	 * @throws \Sabre\DAV\Exception\Forbidden
142
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
143
-	 */
144
-	public function createDirectory($name) {
145
-		try {
146
-			if (!$this->info->isCreatable()) {
147
-				throw new \Sabre\DAV\Exception\Forbidden();
148
-			}
149
-
150
-			$this->fileView->verifyPath($this->path, $name);
151
-			$newPath = $this->path . '/' . $name;
152
-			if (!$this->fileView->mkdir($newPath)) {
153
-				throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
154
-			}
155
-		} catch (StorageNotAvailableException $e) {
156
-			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
157
-		} catch (InvalidPathException $ex) {
158
-			throw new InvalidPath($ex->getMessage(), false, $ex);
159
-		} catch (ForbiddenException $ex) {
160
-			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
161
-		} catch (LockedException $e) {
162
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
163
-		}
164
-	}
165
-
166
-	/**
167
-	 * Returns a specific child node, referenced by its name
168
-	 *
169
-	 * @param string $name
170
-	 * @param FileInfo $info
171
-	 * @return \Sabre\DAV\INode
172
-	 * @throws InvalidPath
173
-	 * @throws \Sabre\DAV\Exception\NotFound
174
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
175
-	 */
176
-	public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
177
-		$storage = $this->info->getStorage();
178
-		$allowDirectory = false;
179
-		if ($storage instanceof PublicShareWrapper) {
180
-			$share = $storage->getShare();
181
-			$allowDirectory =
182
-				// Only allow directories for file drops
183
-				($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ &&
184
-				// And only allow it for directories which are a direct child of the share root
185
-				$this->info->getId() === $share->getNodeId();
186
-		}
187
-
188
-		// For file drop we need to be allowed to read the directory with the nickname
189
-		if (!$allowDirectory && !$this->info->isReadable()) {
190
-			// avoid detecting files through this way
191
-			throw new NotFound();
192
-		}
193
-
194
-		$path = $this->path . '/' . $name;
195
-		if (is_null($info)) {
196
-			try {
197
-				$this->fileView->verifyPath($this->path, $name, true);
198
-				$info = $this->fileView->getFileInfo($path);
199
-			} catch (StorageNotAvailableException $e) {
200
-				throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
201
-			} catch (InvalidPathException $ex) {
202
-				throw new InvalidPath($ex->getMessage(), false, $ex);
203
-			} catch (ForbiddenException $e) {
204
-				throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e);
205
-			}
206
-		}
207
-
208
-		if (!$info) {
209
-			throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
210
-		}
211
-
212
-		if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
213
-			$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
214
-		} else {
215
-			// In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
216
-			if (!$this->info->isReadable()) {
217
-				throw new NotFound();
218
-			}
219
-
220
-			$node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
221
-		}
222
-		if ($this->tree) {
223
-			$this->tree->cacheNode($node);
224
-		}
225
-		return $node;
226
-	}
227
-
228
-	/**
229
-	 * Returns an array with all the child nodes
230
-	 *
231
-	 * @return \Sabre\DAV\INode[]
232
-	 * @throws \Sabre\DAV\Exception\Locked
233
-	 * @throws Forbidden
234
-	 */
235
-	public function getChildren() {
236
-		if (!is_null($this->dirContent)) {
237
-			return $this->dirContent;
238
-		}
239
-		try {
240
-			if (!$this->info->isReadable()) {
241
-				// return 403 instead of 404 because a 404 would make
242
-				// the caller believe that the collection itself does not exist
243
-				if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) {
244
-					throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
245
-				} else {
246
-					throw new Forbidden('No read permissions');
247
-				}
248
-			}
249
-			$folderContent = $this->getNode()->getDirectoryListing();
250
-		} catch (LockedException $e) {
251
-			throw new Locked();
252
-		}
253
-
254
-		$nodes = [];
255
-		$request = Server::get(IRequest::class);
256
-		$l10nFactory = Server::get(IFactory::class);
257
-		$l10n = $l10nFactory->get(Application::APP_ID);
258
-		foreach ($folderContent as $info) {
259
-			$node = $this->getChild($info->getName(), $info, $request, $l10n);
260
-			$nodes[] = $node;
261
-		}
262
-		$this->dirContent = $nodes;
263
-		return $this->dirContent;
264
-	}
265
-
266
-	/**
267
-	 * Checks if a child exists.
268
-	 *
269
-	 * @param string $name
270
-	 * @return bool
271
-	 */
272
-	public function childExists($name) {
273
-		// note: here we do NOT resolve the chunk file name to the real file name
274
-		// to make sure we return false when checking for file existence with a chunk
275
-		// file name.
276
-		// This is to make sure that "createFile" is still triggered
277
-		// (required old code) instead of "updateFile".
278
-		//
279
-		// TODO: resolve chunk file name here and implement "updateFile"
280
-		$path = $this->path . '/' . $name;
281
-		return $this->fileView->file_exists($path);
282
-	}
283
-
284
-	/**
285
-	 * Deletes all files in this directory, and then itself
286
-	 *
287
-	 * @return void
288
-	 * @throws FileLocked
289
-	 * @throws \Sabre\DAV\Exception\Forbidden
290
-	 */
291
-	public function delete() {
292
-		if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
293
-			throw new \Sabre\DAV\Exception\Forbidden();
294
-		}
295
-
296
-		try {
297
-			if (!$this->fileView->rmdir($this->path)) {
298
-				// assume it wasn't possible to remove due to permission issue
299
-				throw new \Sabre\DAV\Exception\Forbidden();
300
-			}
301
-		} catch (ForbiddenException $ex) {
302
-			throw new Forbidden($ex->getMessage(), $ex->getRetry());
303
-		} catch (LockedException $e) {
304
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
305
-		}
306
-	}
307
-
308
-	private function getLogger(): LoggerInterface {
309
-		return Server::get(LoggerInterface::class);
310
-	}
311
-
312
-	/**
313
-	 * Returns available diskspace information
314
-	 *
315
-	 * @return array
316
-	 */
317
-	public function getQuotaInfo() {
318
-		if ($this->quotaInfo) {
319
-			return $this->quotaInfo;
320
-		}
321
-		$relativePath = $this->fileView->getRelativePath($this->info->getPath());
322
-		if ($relativePath === null) {
323
-			$this->getLogger()->warning('error while getting quota as the relative path cannot be found');
324
-			return [0, 0];
325
-		}
326
-
327
-		try {
328
-			$storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
329
-			if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
330
-				$free = FileInfo::SPACE_UNLIMITED;
331
-			} else {
332
-				$free = $storageInfo['free'];
333
-			}
334
-			$this->quotaInfo = [
335
-				$storageInfo['used'],
336
-				$free
337
-			];
338
-			return $this->quotaInfo;
339
-		} catch (NotFoundException $e) {
340
-			$this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
341
-			return [0, 0];
342
-		} catch (StorageNotAvailableException $e) {
343
-			$this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
344
-			return [0, 0];
345
-		} catch (NotPermittedException $e) {
346
-			$this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
347
-			return [0, 0];
348
-		}
349
-	}
350
-
351
-	/**
352
-	 * Moves a node into this collection.
353
-	 *
354
-	 * It is up to the implementors to:
355
-	 *   1. Create the new resource.
356
-	 *   2. Remove the old resource.
357
-	 *   3. Transfer any properties or other data.
358
-	 *
359
-	 * Generally you should make very sure that your collection can easily move
360
-	 * the move.
361
-	 *
362
-	 * If you don't, just return false, which will trigger sabre/dav to handle
363
-	 * the move itself. If you return true from this function, the assumption
364
-	 * is that the move was successful.
365
-	 *
366
-	 * @param string $targetName New local file/collection name.
367
-	 * @param string $fullSourcePath Full path to source node
368
-	 * @param INode $sourceNode Source node itself
369
-	 * @return bool
370
-	 * @throws BadRequest
371
-	 * @throws ServiceUnavailable
372
-	 * @throws Forbidden
373
-	 * @throws FileLocked
374
-	 * @throws \Sabre\DAV\Exception\Forbidden
375
-	 */
376
-	public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
377
-		if (!$sourceNode instanceof Node) {
378
-			// it's a file of another kind, like FutureFile
379
-			if ($sourceNode instanceof IFile) {
380
-				// fallback to default copy+delete handling
381
-				return false;
382
-			}
383
-			throw new BadRequest('Incompatible node types');
384
-		}
385
-
386
-		$destinationPath = $this->getPath() . '/' . $targetName;
387
-
388
-
389
-		$targetNodeExists = $this->childExists($targetName);
390
-
391
-		// at getNodeForPath we also check the path for isForbiddenFileOrDir
392
-		// with that we have covered both source and destination
393
-		if ($sourceNode instanceof Directory && $targetNodeExists) {
394
-			throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
395
-		}
396
-
397
-		[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
398
-		$destinationDir = $this->getPath();
399
-
400
-		$sourcePath = $sourceNode->getPath();
401
-
402
-		$isMovableMount = false;
403
-		$sourceMount = Server::get(IMountManager::class)->find($this->fileView->getAbsolutePath($sourcePath));
404
-		$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
405
-		if ($sourceMount instanceof MoveableMount && $internalPath === '') {
406
-			$isMovableMount = true;
407
-		}
408
-
409
-		try {
410
-			$sameFolder = ($sourceDir === $destinationDir);
411
-			// if we're overwriting or same folder
412
-			if ($targetNodeExists || $sameFolder) {
413
-				// note that renaming a share mount point is always allowed
414
-				if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
415
-					throw new \Sabre\DAV\Exception\Forbidden();
416
-				}
417
-			} else {
418
-				if (!$this->fileView->isCreatable($destinationDir)) {
419
-					throw new \Sabre\DAV\Exception\Forbidden();
420
-				}
421
-			}
422
-
423
-			if (!$sameFolder) {
424
-				// moving to a different folder, source will be gone, like a deletion
425
-				// note that moving a share mount point is always allowed
426
-				if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
427
-					throw new \Sabre\DAV\Exception\Forbidden();
428
-				}
429
-			}
430
-
431
-			$fileName = basename($destinationPath);
432
-			try {
433
-				$this->fileView->verifyPath($destinationDir, $fileName);
434
-			} catch (InvalidPathException $ex) {
435
-				throw new InvalidPath($ex->getMessage());
436
-			}
437
-
438
-			$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
439
-			if (!$renameOkay) {
440
-				throw new \Sabre\DAV\Exception\Forbidden('');
441
-			}
442
-		} catch (StorageNotAvailableException $e) {
443
-			throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
444
-		} catch (ForbiddenException $ex) {
445
-			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
446
-		} catch (LockedException $e) {
447
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
448
-		}
449
-
450
-		return true;
451
-	}
452
-
453
-
454
-	public function copyInto($targetName, $sourcePath, INode $sourceNode) {
455
-		if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
456
-			try {
457
-				$destinationPath = $this->getPath() . '/' . $targetName;
458
-				$sourcePath = $sourceNode->getPath();
459
-
460
-				if (!$this->fileView->isCreatable($this->getPath())) {
461
-					throw new \Sabre\DAV\Exception\Forbidden();
462
-				}
463
-
464
-				try {
465
-					$this->fileView->verifyPath($this->getPath(), $targetName);
466
-				} catch (InvalidPathException $ex) {
467
-					throw new InvalidPath($ex->getMessage());
468
-				}
469
-
470
-				$copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
471
-
472
-				if (!$copyOkay) {
473
-					throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed');
474
-				}
475
-
476
-				return true;
477
-			} catch (StorageNotAvailableException $e) {
478
-				throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
479
-			} catch (ForbiddenException $ex) {
480
-				throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
481
-			} catch (LockedException $e) {
482
-				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
483
-			}
484
-		}
485
-
486
-		return false;
487
-	}
488
-
489
-	public function getNode(): Folder {
490
-		return $this->node;
491
-	}
43
+    /**
44
+     * Cached directory content
45
+     * @var FileInfo[]
46
+     */
47
+    private ?array $dirContent = null;
48
+
49
+    /** Cached quota info */
50
+    private ?array $quotaInfo = null;
51
+
52
+    /**
53
+     * Sets up the node, expects a full path name
54
+     */
55
+    public function __construct(
56
+        View $view,
57
+        FileInfo $info,
58
+        private ?CachingTree $tree = null,
59
+        ?IShareManager $shareManager = null,
60
+    ) {
61
+        parent::__construct($view, $info, $shareManager);
62
+    }
63
+
64
+    /**
65
+     * Creates a new file in the directory
66
+     *
67
+     * Data will either be supplied as a stream resource, or in certain cases
68
+     * as a string. Keep in mind that you may have to support either.
69
+     *
70
+     * After successful creation of the file, you may choose to return the ETag
71
+     * of the new file here.
72
+     *
73
+     * The returned ETag must be surrounded by double-quotes (The quotes should
74
+     * be part of the actual string).
75
+     *
76
+     * If you cannot accurately determine the ETag, you should not return it.
77
+     * If you don't store the file exactly as-is (you're transforming it
78
+     * somehow) you should also not return an ETag.
79
+     *
80
+     * This means that if a subsequent GET to this new file does not exactly
81
+     * return the same contents of what was submitted here, you are strongly
82
+     * recommended to omit the ETag.
83
+     *
84
+     * @param string $name Name of the file
85
+     * @param resource|string $data Initial payload
86
+     * @return null|string
87
+     * @throws Exception\EntityTooLarge
88
+     * @throws Exception\UnsupportedMediaType
89
+     * @throws FileLocked
90
+     * @throws InvalidPath
91
+     * @throws \Sabre\DAV\Exception
92
+     * @throws \Sabre\DAV\Exception\BadRequest
93
+     * @throws \Sabre\DAV\Exception\Forbidden
94
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
95
+     */
96
+    public function createFile($name, $data = null) {
97
+        try {
98
+            if (!$this->fileView->isCreatable($this->path)) {
99
+                throw new \Sabre\DAV\Exception\Forbidden();
100
+            }
101
+
102
+            $this->fileView->verifyPath($this->path, $name);
103
+
104
+            $path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
105
+            // in case the file already exists/overwriting
106
+            $info = $this->fileView->getFileInfo($this->path . '/' . $name);
107
+            if (!$info) {
108
+                // use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
109
+                $info = new \OC\Files\FileInfo($path, null, null, [
110
+                    'type' => FileInfo::TYPE_FILE
111
+                ], null);
112
+            }
113
+            $node = new File($this->fileView, $info);
114
+
115
+            // only allow 1 process to upload a file at once but still allow reading the file while writing the part file
116
+            $node->acquireLock(ILockingProvider::LOCK_SHARED);
117
+            $this->fileView->lockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
118
+
119
+            $result = $node->put($data);
120
+
121
+            $this->fileView->unlockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
122
+            $node->releaseLock(ILockingProvider::LOCK_SHARED);
123
+            return $result;
124
+        } catch (StorageNotAvailableException $e) {
125
+            throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
126
+        } catch (InvalidPathException $ex) {
127
+            throw new InvalidPath($ex->getMessage(), false, $ex);
128
+        } catch (ForbiddenException $ex) {
129
+            throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
130
+        } catch (LockedException $e) {
131
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
132
+        }
133
+    }
134
+
135
+    /**
136
+     * Creates a new subdirectory
137
+     *
138
+     * @param string $name
139
+     * @throws FileLocked
140
+     * @throws InvalidPath
141
+     * @throws \Sabre\DAV\Exception\Forbidden
142
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
143
+     */
144
+    public function createDirectory($name) {
145
+        try {
146
+            if (!$this->info->isCreatable()) {
147
+                throw new \Sabre\DAV\Exception\Forbidden();
148
+            }
149
+
150
+            $this->fileView->verifyPath($this->path, $name);
151
+            $newPath = $this->path . '/' . $name;
152
+            if (!$this->fileView->mkdir($newPath)) {
153
+                throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
154
+            }
155
+        } catch (StorageNotAvailableException $e) {
156
+            throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
157
+        } catch (InvalidPathException $ex) {
158
+            throw new InvalidPath($ex->getMessage(), false, $ex);
159
+        } catch (ForbiddenException $ex) {
160
+            throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
161
+        } catch (LockedException $e) {
162
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
163
+        }
164
+    }
165
+
166
+    /**
167
+     * Returns a specific child node, referenced by its name
168
+     *
169
+     * @param string $name
170
+     * @param FileInfo $info
171
+     * @return \Sabre\DAV\INode
172
+     * @throws InvalidPath
173
+     * @throws \Sabre\DAV\Exception\NotFound
174
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
175
+     */
176
+    public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
177
+        $storage = $this->info->getStorage();
178
+        $allowDirectory = false;
179
+        if ($storage instanceof PublicShareWrapper) {
180
+            $share = $storage->getShare();
181
+            $allowDirectory =
182
+                // Only allow directories for file drops
183
+                ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ &&
184
+                // And only allow it for directories which are a direct child of the share root
185
+                $this->info->getId() === $share->getNodeId();
186
+        }
187
+
188
+        // For file drop we need to be allowed to read the directory with the nickname
189
+        if (!$allowDirectory && !$this->info->isReadable()) {
190
+            // avoid detecting files through this way
191
+            throw new NotFound();
192
+        }
193
+
194
+        $path = $this->path . '/' . $name;
195
+        if (is_null($info)) {
196
+            try {
197
+                $this->fileView->verifyPath($this->path, $name, true);
198
+                $info = $this->fileView->getFileInfo($path);
199
+            } catch (StorageNotAvailableException $e) {
200
+                throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
201
+            } catch (InvalidPathException $ex) {
202
+                throw new InvalidPath($ex->getMessage(), false, $ex);
203
+            } catch (ForbiddenException $e) {
204
+                throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e);
205
+            }
206
+        }
207
+
208
+        if (!$info) {
209
+            throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
210
+        }
211
+
212
+        if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
213
+            $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
214
+        } else {
215
+            // In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
216
+            if (!$this->info->isReadable()) {
217
+                throw new NotFound();
218
+            }
219
+
220
+            $node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
221
+        }
222
+        if ($this->tree) {
223
+            $this->tree->cacheNode($node);
224
+        }
225
+        return $node;
226
+    }
227
+
228
+    /**
229
+     * Returns an array with all the child nodes
230
+     *
231
+     * @return \Sabre\DAV\INode[]
232
+     * @throws \Sabre\DAV\Exception\Locked
233
+     * @throws Forbidden
234
+     */
235
+    public function getChildren() {
236
+        if (!is_null($this->dirContent)) {
237
+            return $this->dirContent;
238
+        }
239
+        try {
240
+            if (!$this->info->isReadable()) {
241
+                // return 403 instead of 404 because a 404 would make
242
+                // the caller believe that the collection itself does not exist
243
+                if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) {
244
+                    throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
245
+                } else {
246
+                    throw new Forbidden('No read permissions');
247
+                }
248
+            }
249
+            $folderContent = $this->getNode()->getDirectoryListing();
250
+        } catch (LockedException $e) {
251
+            throw new Locked();
252
+        }
253
+
254
+        $nodes = [];
255
+        $request = Server::get(IRequest::class);
256
+        $l10nFactory = Server::get(IFactory::class);
257
+        $l10n = $l10nFactory->get(Application::APP_ID);
258
+        foreach ($folderContent as $info) {
259
+            $node = $this->getChild($info->getName(), $info, $request, $l10n);
260
+            $nodes[] = $node;
261
+        }
262
+        $this->dirContent = $nodes;
263
+        return $this->dirContent;
264
+    }
265
+
266
+    /**
267
+     * Checks if a child exists.
268
+     *
269
+     * @param string $name
270
+     * @return bool
271
+     */
272
+    public function childExists($name) {
273
+        // note: here we do NOT resolve the chunk file name to the real file name
274
+        // to make sure we return false when checking for file existence with a chunk
275
+        // file name.
276
+        // This is to make sure that "createFile" is still triggered
277
+        // (required old code) instead of "updateFile".
278
+        //
279
+        // TODO: resolve chunk file name here and implement "updateFile"
280
+        $path = $this->path . '/' . $name;
281
+        return $this->fileView->file_exists($path);
282
+    }
283
+
284
+    /**
285
+     * Deletes all files in this directory, and then itself
286
+     *
287
+     * @return void
288
+     * @throws FileLocked
289
+     * @throws \Sabre\DAV\Exception\Forbidden
290
+     */
291
+    public function delete() {
292
+        if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
293
+            throw new \Sabre\DAV\Exception\Forbidden();
294
+        }
295
+
296
+        try {
297
+            if (!$this->fileView->rmdir($this->path)) {
298
+                // assume it wasn't possible to remove due to permission issue
299
+                throw new \Sabre\DAV\Exception\Forbidden();
300
+            }
301
+        } catch (ForbiddenException $ex) {
302
+            throw new Forbidden($ex->getMessage(), $ex->getRetry());
303
+        } catch (LockedException $e) {
304
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
305
+        }
306
+    }
307
+
308
+    private function getLogger(): LoggerInterface {
309
+        return Server::get(LoggerInterface::class);
310
+    }
311
+
312
+    /**
313
+     * Returns available diskspace information
314
+     *
315
+     * @return array
316
+     */
317
+    public function getQuotaInfo() {
318
+        if ($this->quotaInfo) {
319
+            return $this->quotaInfo;
320
+        }
321
+        $relativePath = $this->fileView->getRelativePath($this->info->getPath());
322
+        if ($relativePath === null) {
323
+            $this->getLogger()->warning('error while getting quota as the relative path cannot be found');
324
+            return [0, 0];
325
+        }
326
+
327
+        try {
328
+            $storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
329
+            if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
330
+                $free = FileInfo::SPACE_UNLIMITED;
331
+            } else {
332
+                $free = $storageInfo['free'];
333
+            }
334
+            $this->quotaInfo = [
335
+                $storageInfo['used'],
336
+                $free
337
+            ];
338
+            return $this->quotaInfo;
339
+        } catch (NotFoundException $e) {
340
+            $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
341
+            return [0, 0];
342
+        } catch (StorageNotAvailableException $e) {
343
+            $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
344
+            return [0, 0];
345
+        } catch (NotPermittedException $e) {
346
+            $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
347
+            return [0, 0];
348
+        }
349
+    }
350
+
351
+    /**
352
+     * Moves a node into this collection.
353
+     *
354
+     * It is up to the implementors to:
355
+     *   1. Create the new resource.
356
+     *   2. Remove the old resource.
357
+     *   3. Transfer any properties or other data.
358
+     *
359
+     * Generally you should make very sure that your collection can easily move
360
+     * the move.
361
+     *
362
+     * If you don't, just return false, which will trigger sabre/dav to handle
363
+     * the move itself. If you return true from this function, the assumption
364
+     * is that the move was successful.
365
+     *
366
+     * @param string $targetName New local file/collection name.
367
+     * @param string $fullSourcePath Full path to source node
368
+     * @param INode $sourceNode Source node itself
369
+     * @return bool
370
+     * @throws BadRequest
371
+     * @throws ServiceUnavailable
372
+     * @throws Forbidden
373
+     * @throws FileLocked
374
+     * @throws \Sabre\DAV\Exception\Forbidden
375
+     */
376
+    public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
377
+        if (!$sourceNode instanceof Node) {
378
+            // it's a file of another kind, like FutureFile
379
+            if ($sourceNode instanceof IFile) {
380
+                // fallback to default copy+delete handling
381
+                return false;
382
+            }
383
+            throw new BadRequest('Incompatible node types');
384
+        }
385
+
386
+        $destinationPath = $this->getPath() . '/' . $targetName;
387
+
388
+
389
+        $targetNodeExists = $this->childExists($targetName);
390
+
391
+        // at getNodeForPath we also check the path for isForbiddenFileOrDir
392
+        // with that we have covered both source and destination
393
+        if ($sourceNode instanceof Directory && $targetNodeExists) {
394
+            throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
395
+        }
396
+
397
+        [$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
398
+        $destinationDir = $this->getPath();
399
+
400
+        $sourcePath = $sourceNode->getPath();
401
+
402
+        $isMovableMount = false;
403
+        $sourceMount = Server::get(IMountManager::class)->find($this->fileView->getAbsolutePath($sourcePath));
404
+        $internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
405
+        if ($sourceMount instanceof MoveableMount && $internalPath === '') {
406
+            $isMovableMount = true;
407
+        }
408
+
409
+        try {
410
+            $sameFolder = ($sourceDir === $destinationDir);
411
+            // if we're overwriting or same folder
412
+            if ($targetNodeExists || $sameFolder) {
413
+                // note that renaming a share mount point is always allowed
414
+                if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
415
+                    throw new \Sabre\DAV\Exception\Forbidden();
416
+                }
417
+            } else {
418
+                if (!$this->fileView->isCreatable($destinationDir)) {
419
+                    throw new \Sabre\DAV\Exception\Forbidden();
420
+                }
421
+            }
422
+
423
+            if (!$sameFolder) {
424
+                // moving to a different folder, source will be gone, like a deletion
425
+                // note that moving a share mount point is always allowed
426
+                if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
427
+                    throw new \Sabre\DAV\Exception\Forbidden();
428
+                }
429
+            }
430
+
431
+            $fileName = basename($destinationPath);
432
+            try {
433
+                $this->fileView->verifyPath($destinationDir, $fileName);
434
+            } catch (InvalidPathException $ex) {
435
+                throw new InvalidPath($ex->getMessage());
436
+            }
437
+
438
+            $renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
439
+            if (!$renameOkay) {
440
+                throw new \Sabre\DAV\Exception\Forbidden('');
441
+            }
442
+        } catch (StorageNotAvailableException $e) {
443
+            throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
444
+        } catch (ForbiddenException $ex) {
445
+            throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
446
+        } catch (LockedException $e) {
447
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
448
+        }
449
+
450
+        return true;
451
+    }
452
+
453
+
454
+    public function copyInto($targetName, $sourcePath, INode $sourceNode) {
455
+        if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
456
+            try {
457
+                $destinationPath = $this->getPath() . '/' . $targetName;
458
+                $sourcePath = $sourceNode->getPath();
459
+
460
+                if (!$this->fileView->isCreatable($this->getPath())) {
461
+                    throw new \Sabre\DAV\Exception\Forbidden();
462
+                }
463
+
464
+                try {
465
+                    $this->fileView->verifyPath($this->getPath(), $targetName);
466
+                } catch (InvalidPathException $ex) {
467
+                    throw new InvalidPath($ex->getMessage());
468
+                }
469
+
470
+                $copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
471
+
472
+                if (!$copyOkay) {
473
+                    throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed');
474
+                }
475
+
476
+                return true;
477
+            } catch (StorageNotAvailableException $e) {
478
+                throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
479
+            } catch (ForbiddenException $ex) {
480
+                throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
481
+            } catch (LockedException $e) {
482
+                throw new FileLocked($e->getMessage(), $e->getCode(), $e);
483
+            }
484
+        }
485
+
486
+        return false;
487
+    }
488
+
489
+    public function getNode(): Folder {
490
+        return $this->node;
491
+    }
492 492
 }
Please login to merge, or discard this patch.
apps/dav/lib/Files/Sharing/FilesDropPlugin.php 2 patches
Indentation   +71 added lines, -71 removed lines patch added patch discarded remove patch
@@ -17,76 +17,76 @@
 block discarded – undo
17 17
  */
18 18
 class FilesDropPlugin extends ServerPlugin {
19 19
 
20
-	private ?View $view = null;
21
-	private ?IShare $share = null;
22
-	private bool $enabled = false;
23
-
24
-	public function setView(View $view): void {
25
-		$this->view = $view;
26
-	}
27
-
28
-	public function setShare(IShare $share): void {
29
-		$this->share = $share;
30
-	}
31
-
32
-	public function enable(): void {
33
-		$this->enabled = true;
34
-	}
35
-
36
-
37
-	/**
38
-	 * This initializes the plugin.
39
-	 *
40
-	 * @param \Sabre\DAV\Server $server Sabre server
41
-	 *
42
-	 * @return void
43
-	 * @throws MethodNotAllowed
44
-	 */
45
-	public function initialize(\Sabre\DAV\Server $server): void {
46
-		$server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
47
-		$this->enabled = false;
48
-	}
49
-
50
-	public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
51
-		if (!$this->enabled || $this->share === null || $this->view === null) {
52
-			return;
53
-		}
54
-
55
-		// Only allow file drop
56
-		if ($request->getMethod() !== 'PUT') {
57
-			throw new MethodNotAllowed('Only PUT is allowed on files drop');
58
-		}
59
-
60
-		// Always upload at the root level
61
-		$path = explode('/', $request->getPath());
62
-		$path = array_pop($path);
63
-
64
-		// Extract the attributes for the file request
65
-		$isFileRequest = false;
66
-		$attributes = $this->share->getAttributes();
67
-		$nickName = $request->hasHeader('X-NC-Nickname') ? urldecode($request->getHeader('X-NC-Nickname')) : null;
68
-		if ($attributes !== null) {
69
-			$isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
70
-		}
71
-
72
-		// We need a valid nickname for file requests
73
-		if ($isFileRequest && ($nickName == null || trim($nickName) === '')) {
74
-			throw new MethodNotAllowed('Nickname is required for file requests');
75
-		}
76
-
77
-		// If this is a file request we need to create a folder for the user
78
-		if ($isFileRequest) {
79
-			// Check if the folder already exists
80
-			if (!($this->view->file_exists($nickName) === true)) {
81
-				$this->view->mkdir($nickName);
82
-			}
83
-			// Put all files in the subfolder
84
-			$path = $nickName . '/' . $path;
85
-		}
86
-
87
-		$newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
88
-		$url = $request->getBaseUrl() . '/files/' . $this->share->getToken() . $newName;
89
-		$request->setUrl($url);
90
-	}
20
+    private ?View $view = null;
21
+    private ?IShare $share = null;
22
+    private bool $enabled = false;
23
+
24
+    public function setView(View $view): void {
25
+        $this->view = $view;
26
+    }
27
+
28
+    public function setShare(IShare $share): void {
29
+        $this->share = $share;
30
+    }
31
+
32
+    public function enable(): void {
33
+        $this->enabled = true;
34
+    }
35
+
36
+
37
+    /**
38
+     * This initializes the plugin.
39
+     *
40
+     * @param \Sabre\DAV\Server $server Sabre server
41
+     *
42
+     * @return void
43
+     * @throws MethodNotAllowed
44
+     */
45
+    public function initialize(\Sabre\DAV\Server $server): void {
46
+        $server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
47
+        $this->enabled = false;
48
+    }
49
+
50
+    public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
51
+        if (!$this->enabled || $this->share === null || $this->view === null) {
52
+            return;
53
+        }
54
+
55
+        // Only allow file drop
56
+        if ($request->getMethod() !== 'PUT') {
57
+            throw new MethodNotAllowed('Only PUT is allowed on files drop');
58
+        }
59
+
60
+        // Always upload at the root level
61
+        $path = explode('/', $request->getPath());
62
+        $path = array_pop($path);
63
+
64
+        // Extract the attributes for the file request
65
+        $isFileRequest = false;
66
+        $attributes = $this->share->getAttributes();
67
+        $nickName = $request->hasHeader('X-NC-Nickname') ? urldecode($request->getHeader('X-NC-Nickname')) : null;
68
+        if ($attributes !== null) {
69
+            $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
70
+        }
71
+
72
+        // We need a valid nickname for file requests
73
+        if ($isFileRequest && ($nickName == null || trim($nickName) === '')) {
74
+            throw new MethodNotAllowed('Nickname is required for file requests');
75
+        }
76
+
77
+        // If this is a file request we need to create a folder for the user
78
+        if ($isFileRequest) {
79
+            // Check if the folder already exists
80
+            if (!($this->view->file_exists($nickName) === true)) {
81
+                $this->view->mkdir($nickName);
82
+            }
83
+            // Put all files in the subfolder
84
+            $path = $nickName . '/' . $path;
85
+        }
86
+
87
+        $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
88
+        $url = $request->getBaseUrl() . '/files/' . $this->share->getToken() . $newName;
89
+        $request->setUrl($url);
90
+    }
91 91
 
92 92
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -81,11 +81,11 @@
 block discarded – undo
81 81
 				$this->view->mkdir($nickName);
82 82
 			}
83 83
 			// Put all files in the subfolder
84
-			$path = $nickName . '/' . $path;
84
+			$path = $nickName.'/'.$path;
85 85
 		}
86 86
 
87 87
 		$newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
88
-		$url = $request->getBaseUrl() . '/files/' . $this->share->getToken() . $newName;
88
+		$url = $request->getBaseUrl().'/files/'.$this->share->getToken().$newName;
89 89
 		$request->setUrl($url);
90 90
 	}
91 91
 
Please login to merge, or discard this patch.
apps/dav/lib/Files/Sharing/RootCollection.php 1 patch
Indentation   +13 added lines, -13 removed lines patch added patch discarded remove patch
@@ -14,19 +14,19 @@
 block discarded – undo
14 14
 use Sabre\DAVACL\PrincipalBackend\BackendInterface;
15 15
 
16 16
 class RootCollection extends AbstractPrincipalCollection {
17
-	public function __construct(
18
-		private INode $root,
19
-		BackendInterface $principalBackend,
20
-		string $principalPrefix = 'principals',
21
-	) {
22
-		parent::__construct($principalBackend, $principalPrefix);
23
-	}
17
+    public function __construct(
18
+        private INode $root,
19
+        BackendInterface $principalBackend,
20
+        string $principalPrefix = 'principals',
21
+    ) {
22
+        parent::__construct($principalBackend, $principalPrefix);
23
+    }
24 24
 
25
-	public function getChildForPrincipal(array $principalInfo): INode {
26
-		return $this->root;
27
-	}
25
+    public function getChildForPrincipal(array $principalInfo): INode {
26
+        return $this->root;
27
+    }
28 28
 
29
-	public function getName() {
30
-		return 'files';
31
-	}
29
+    public function getName() {
30
+        return 'files';
31
+    }
32 32
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/UploadHome.php 2 patches
Indentation   +100 added lines, -100 removed lines patch added patch discarded remove patch
@@ -17,104 +17,104 @@
 block discarded – undo
17 17
 use Sabre\DAV\ICollection;
18 18
 
19 19
 class UploadHome implements ICollection {
20
-	private string $uid;
21
-	private ?Folder $uploadFolder = null;
22
-
23
-	public function __construct(
24
-		private readonly array $principalInfo,
25
-		private readonly CleanupService $cleanupService,
26
-		private readonly IRootFolder $rootFolder,
27
-		private readonly IUserSession $userSession,
28
-		private readonly \OCP\Share\IManager $shareManager,
29
-	) {
30
-		[$prefix, $name] = \Sabre\Uri\split($principalInfo['uri']);
31
-		if ($prefix === 'principals/shares') {
32
-			$this->uid = $this->shareManager->getShareByToken($name)->getShareOwner();
33
-		} else {
34
-			$user = $this->userSession->getUser();
35
-			if (!$user) {
36
-				throw new Forbidden('Not logged in');
37
-			}
38
-
39
-			$this->uid = $user->getUID();
40
-		}
41
-	}
42
-
43
-	public function createFile($name, $data = null) {
44
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
45
-	}
46
-
47
-	public function createDirectory($name) {
48
-		$this->impl()->createDirectory($name);
49
-
50
-		// Add a cleanup job
51
-		$this->cleanupService->addJob($this->uid, $name);
52
-	}
53
-
54
-	public function getChild($name): UploadFolder {
55
-		return new UploadFolder(
56
-			$this->impl()->getChild($name),
57
-			$this->cleanupService,
58
-			$this->getStorage(),
59
-			$this->uid,
60
-		);
61
-	}
62
-
63
-	public function getChildren(): array {
64
-		return array_map(function ($node) {
65
-			return new UploadFolder(
66
-				$node,
67
-				$this->cleanupService,
68
-				$this->getStorage(),
69
-				$this->uid,
70
-			);
71
-		}, $this->impl()->getChildren());
72
-	}
73
-
74
-	public function childExists($name): bool {
75
-		return !is_null($this->getChild($name));
76
-	}
77
-
78
-	public function delete() {
79
-		$this->impl()->delete();
80
-	}
81
-
82
-	public function getName() {
83
-		[,$name] = \Sabre\Uri\split($this->principalInfo['uri']);
84
-		return $name;
85
-	}
86
-
87
-	public function setName($name) {
88
-		throw new Forbidden('Permission denied to rename this folder');
89
-	}
90
-
91
-	public function getLastModified() {
92
-		return $this->impl()->getLastModified();
93
-	}
94
-
95
-	private function getUploadFolder(): Folder {
96
-		if ($this->uploadFolder === null) {
97
-			$path = '/' . $this->uid . '/uploads';
98
-			try {
99
-				$folder = $this->rootFolder->get($path);
100
-				if (!$folder instanceof Folder) {
101
-					throw new \Exception('Upload folder is a file');
102
-				}
103
-				$this->uploadFolder = $folder;
104
-			} catch (NotFoundException $e) {
105
-				$this->uploadFolder = $this->rootFolder->newFolder($path);
106
-			}
107
-		}
108
-		return $this->uploadFolder;
109
-	}
110
-
111
-	private function impl(): Directory {
112
-		$folder = $this->getUploadFolder();
113
-		$view = new View($folder->getPath());
114
-		return new Directory($view, $folder);
115
-	}
116
-
117
-	private function getStorage() {
118
-		return $this->getUploadFolder()->getStorage();
119
-	}
20
+    private string $uid;
21
+    private ?Folder $uploadFolder = null;
22
+
23
+    public function __construct(
24
+        private readonly array $principalInfo,
25
+        private readonly CleanupService $cleanupService,
26
+        private readonly IRootFolder $rootFolder,
27
+        private readonly IUserSession $userSession,
28
+        private readonly \OCP\Share\IManager $shareManager,
29
+    ) {
30
+        [$prefix, $name] = \Sabre\Uri\split($principalInfo['uri']);
31
+        if ($prefix === 'principals/shares') {
32
+            $this->uid = $this->shareManager->getShareByToken($name)->getShareOwner();
33
+        } else {
34
+            $user = $this->userSession->getUser();
35
+            if (!$user) {
36
+                throw new Forbidden('Not logged in');
37
+            }
38
+
39
+            $this->uid = $user->getUID();
40
+        }
41
+    }
42
+
43
+    public function createFile($name, $data = null) {
44
+        throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
45
+    }
46
+
47
+    public function createDirectory($name) {
48
+        $this->impl()->createDirectory($name);
49
+
50
+        // Add a cleanup job
51
+        $this->cleanupService->addJob($this->uid, $name);
52
+    }
53
+
54
+    public function getChild($name): UploadFolder {
55
+        return new UploadFolder(
56
+            $this->impl()->getChild($name),
57
+            $this->cleanupService,
58
+            $this->getStorage(),
59
+            $this->uid,
60
+        );
61
+    }
62
+
63
+    public function getChildren(): array {
64
+        return array_map(function ($node) {
65
+            return new UploadFolder(
66
+                $node,
67
+                $this->cleanupService,
68
+                $this->getStorage(),
69
+                $this->uid,
70
+            );
71
+        }, $this->impl()->getChildren());
72
+    }
73
+
74
+    public function childExists($name): bool {
75
+        return !is_null($this->getChild($name));
76
+    }
77
+
78
+    public function delete() {
79
+        $this->impl()->delete();
80
+    }
81
+
82
+    public function getName() {
83
+        [,$name] = \Sabre\Uri\split($this->principalInfo['uri']);
84
+        return $name;
85
+    }
86
+
87
+    public function setName($name) {
88
+        throw new Forbidden('Permission denied to rename this folder');
89
+    }
90
+
91
+    public function getLastModified() {
92
+        return $this->impl()->getLastModified();
93
+    }
94
+
95
+    private function getUploadFolder(): Folder {
96
+        if ($this->uploadFolder === null) {
97
+            $path = '/' . $this->uid . '/uploads';
98
+            try {
99
+                $folder = $this->rootFolder->get($path);
100
+                if (!$folder instanceof Folder) {
101
+                    throw new \Exception('Upload folder is a file');
102
+                }
103
+                $this->uploadFolder = $folder;
104
+            } catch (NotFoundException $e) {
105
+                $this->uploadFolder = $this->rootFolder->newFolder($path);
106
+            }
107
+        }
108
+        return $this->uploadFolder;
109
+    }
110
+
111
+    private function impl(): Directory {
112
+        $folder = $this->getUploadFolder();
113
+        $view = new View($folder->getPath());
114
+        return new Directory($view, $folder);
115
+    }
116
+
117
+    private function getStorage() {
118
+        return $this->getUploadFolder()->getStorage();
119
+    }
120 120
 }
Please login to merge, or discard this patch.
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -41,7 +41,7 @@  discard block
 block discarded – undo
41 41
 	}
42 42
 
43 43
 	public function createFile($name, $data = null) {
44
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
44
+		throw new Forbidden('Permission denied to create file (filename '.$name.')');
45 45
 	}
46 46
 
47 47
 	public function createDirectory($name) {
@@ -61,7 +61,7 @@  discard block
 block discarded – undo
61 61
 	}
62 62
 
63 63
 	public function getChildren(): array {
64
-		return array_map(function ($node) {
64
+		return array_map(function($node) {
65 65
 			return new UploadFolder(
66 66
 				$node,
67 67
 				$this->cleanupService,
@@ -94,7 +94,7 @@  discard block
 block discarded – undo
94 94
 
95 95
 	private function getUploadFolder(): Folder {
96 96
 		if ($this->uploadFolder === null) {
97
-			$path = '/' . $this->uid . '/uploads';
97
+			$path = '/'.$this->uid.'/uploads';
98 98
 			try {
99 99
 				$folder = $this->rootFolder->get($path);
100 100
 				if (!$folder instanceof Folder) {
Please login to merge, or discard this patch.
apps/dav/lib/Upload/UploadFolder.php 1 patch
Indentation   +91 added lines, -91 removed lines patch added patch discarded remove patch
@@ -17,95 +17,95 @@
 block discarded – undo
17 17
 use Sabre\DAV\ICollection;
18 18
 
19 19
 class UploadFolder implements ICollection {
20
-	public function __construct(
21
-		private Directory $node,
22
-		private CleanupService $cleanupService,
23
-		private IStorage $storage,
24
-		private string $uid,
25
-	) {
26
-	}
27
-
28
-	public function createFile($name, $data = null) {
29
-		// TODO: verify name - should be a simple number
30
-		try {
31
-			$this->node->createFile($name, $data);
32
-		} catch (\Exception $e) {
33
-			if ($this->node->childExists($name)) {
34
-				$child = $this->node->getChild($name);
35
-				$child->delete();
36
-			}
37
-			throw $e;
38
-		}
39
-	}
40
-
41
-	public function createDirectory($name) {
42
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
43
-	}
44
-
45
-	public function getChild($name) {
46
-		if ($name === '.file') {
47
-			return new FutureFile($this->node, '.file');
48
-		}
49
-		return new UploadFile($this->node->getChild($name));
50
-	}
51
-
52
-	public function getChildren() {
53
-		$tmpChildren = $this->node->getChildren();
54
-
55
-		$children = [];
56
-		$children[] = new FutureFile($this->node, '.file');
57
-
58
-		foreach ($tmpChildren as $child) {
59
-			$children[] = new UploadFile($child);
60
-		}
61
-
62
-		if ($this->storage->instanceOfStorage(ObjectStoreStorage::class)) {
63
-			/** @var ObjectStoreStorage $storage */
64
-			$objectStore = $this->storage->getObjectStore();
65
-			if ($objectStore instanceof IObjectStoreMultiPartUpload) {
66
-				$cache = Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY);
67
-				$uploadSession = $cache->get($this->getName());
68
-				if ($uploadSession) {
69
-					$uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID];
70
-					$id = $uploadSession[ChunkingV2Plugin::UPLOAD_TARGET_ID];
71
-					$parts = $objectStore->getMultipartUploads($this->storage->getURN($id), $uploadId);
72
-					foreach ($parts as $part) {
73
-						$children[] = new PartFile($this->node, $part);
74
-					}
75
-				}
76
-			}
77
-		}
78
-
79
-		return $children;
80
-	}
81
-
82
-	public function childExists($name) {
83
-		if ($name === '.file') {
84
-			return true;
85
-		}
86
-		return $this->node->childExists($name);
87
-	}
88
-
89
-	public function delete() {
90
-		$this->node->delete();
91
-
92
-		// Background cleanup job is not needed anymore
93
-		$this->cleanupService->removeJob($this->uid, $this->getName());
94
-	}
95
-
96
-	public function getName() {
97
-		return $this->node->getName();
98
-	}
99
-
100
-	public function setName($name) {
101
-		throw new Forbidden('Permission denied to rename this folder');
102
-	}
103
-
104
-	public function getLastModified() {
105
-		return $this->node->getLastModified();
106
-	}
107
-
108
-	public function getStorage() {
109
-		return $this->storage;
110
-	}
20
+    public function __construct(
21
+        private Directory $node,
22
+        private CleanupService $cleanupService,
23
+        private IStorage $storage,
24
+        private string $uid,
25
+    ) {
26
+    }
27
+
28
+    public function createFile($name, $data = null) {
29
+        // TODO: verify name - should be a simple number
30
+        try {
31
+            $this->node->createFile($name, $data);
32
+        } catch (\Exception $e) {
33
+            if ($this->node->childExists($name)) {
34
+                $child = $this->node->getChild($name);
35
+                $child->delete();
36
+            }
37
+            throw $e;
38
+        }
39
+    }
40
+
41
+    public function createDirectory($name) {
42
+        throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
43
+    }
44
+
45
+    public function getChild($name) {
46
+        if ($name === '.file') {
47
+            return new FutureFile($this->node, '.file');
48
+        }
49
+        return new UploadFile($this->node->getChild($name));
50
+    }
51
+
52
+    public function getChildren() {
53
+        $tmpChildren = $this->node->getChildren();
54
+
55
+        $children = [];
56
+        $children[] = new FutureFile($this->node, '.file');
57
+
58
+        foreach ($tmpChildren as $child) {
59
+            $children[] = new UploadFile($child);
60
+        }
61
+
62
+        if ($this->storage->instanceOfStorage(ObjectStoreStorage::class)) {
63
+            /** @var ObjectStoreStorage $storage */
64
+            $objectStore = $this->storage->getObjectStore();
65
+            if ($objectStore instanceof IObjectStoreMultiPartUpload) {
66
+                $cache = Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY);
67
+                $uploadSession = $cache->get($this->getName());
68
+                if ($uploadSession) {
69
+                    $uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID];
70
+                    $id = $uploadSession[ChunkingV2Plugin::UPLOAD_TARGET_ID];
71
+                    $parts = $objectStore->getMultipartUploads($this->storage->getURN($id), $uploadId);
72
+                    foreach ($parts as $part) {
73
+                        $children[] = new PartFile($this->node, $part);
74
+                    }
75
+                }
76
+            }
77
+        }
78
+
79
+        return $children;
80
+    }
81
+
82
+    public function childExists($name) {
83
+        if ($name === '.file') {
84
+            return true;
85
+        }
86
+        return $this->node->childExists($name);
87
+    }
88
+
89
+    public function delete() {
90
+        $this->node->delete();
91
+
92
+        // Background cleanup job is not needed anymore
93
+        $this->cleanupService->removeJob($this->uid, $this->getName());
94
+    }
95
+
96
+    public function getName() {
97
+        return $this->node->getName();
98
+    }
99
+
100
+    public function setName($name) {
101
+        throw new Forbidden('Permission denied to rename this folder');
102
+    }
103
+
104
+    public function getLastModified() {
105
+        return $this->node->getLastModified();
106
+    }
107
+
108
+    public function getStorage() {
109
+        return $this->storage;
110
+    }
111 111
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/RootCollection.php 1 patch
Indentation   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -17,34 +17,34 @@
 block discarded – undo
17 17
 
18 18
 class RootCollection extends AbstractPrincipalCollection {
19 19
 
20
-	public function __construct(
21
-		PrincipalBackend\BackendInterface $principalBackend,
22
-		string $principalPrefix,
23
-		private CleanupService $cleanupService,
24
-		private IRootFolder $rootFolder,
25
-		private IUserSession $userSession,
26
-		private IManager $shareManager,
27
-	) {
28
-		parent::__construct($principalBackend, $principalPrefix);
29
-	}
20
+    public function __construct(
21
+        PrincipalBackend\BackendInterface $principalBackend,
22
+        string $principalPrefix,
23
+        private CleanupService $cleanupService,
24
+        private IRootFolder $rootFolder,
25
+        private IUserSession $userSession,
26
+        private IManager $shareManager,
27
+    ) {
28
+        parent::__construct($principalBackend, $principalPrefix);
29
+    }
30 30
 
31
-	/**
32
-	 * @inheritdoc
33
-	 */
34
-	public function getChildForPrincipal(array $principalInfo): UploadHome {
35
-		return new UploadHome(
36
-			$principalInfo,
37
-			$this->cleanupService,
38
-			$this->rootFolder,
39
-			$this->userSession,
40
-			$this->shareManager,
41
-		);
42
-	}
31
+    /**
32
+     * @inheritdoc
33
+     */
34
+    public function getChildForPrincipal(array $principalInfo): UploadHome {
35
+        return new UploadHome(
36
+            $principalInfo,
37
+            $this->cleanupService,
38
+            $this->rootFolder,
39
+            $this->userSession,
40
+            $this->shareManager,
41
+        );
42
+    }
43 43
 
44
-	/**
45
-	 * @inheritdoc
46
-	 */
47
-	public function getName(): string {
48
-		return 'uploads';
49
-	}
44
+    /**
45
+     * @inheritdoc
46
+     */
47
+    public function getName(): string {
48
+        return 'uploads';
49
+    }
50 50
 }
Please login to merge, or discard this patch.