@@ -71,605 +71,605 @@ |
||
71 | 71 | |
72 | 72 | class File extends Node implements IFile { |
73 | 73 | |
74 | - protected $request; |
|
75 | - |
|
76 | - /** |
|
77 | - * Sets up the node, expects a full path name |
|
78 | - * |
|
79 | - * @param \OC\Files\View $view |
|
80 | - * @param \OCP\Files\FileInfo $info |
|
81 | - * @param \OCP\Share\IManager $shareManager |
|
82 | - * @param \OC\AppFramework\Http\Request $request |
|
83 | - */ |
|
84 | - public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) { |
|
85 | - parent::__construct($view, $info, $shareManager); |
|
86 | - |
|
87 | - if (isset($request)) { |
|
88 | - $this->request = $request; |
|
89 | - } else { |
|
90 | - $this->request = \OC::$server->getRequest(); |
|
91 | - } |
|
92 | - } |
|
93 | - |
|
94 | - /** |
|
95 | - * Updates the data |
|
96 | - * |
|
97 | - * The data argument is a readable stream resource. |
|
98 | - * |
|
99 | - * After a successful put operation, you may choose to return an ETag. The |
|
100 | - * etag must always be surrounded by double-quotes. These quotes must |
|
101 | - * appear in the actual string you're returning. |
|
102 | - * |
|
103 | - * Clients may use the ETag from a PUT request to later on make sure that |
|
104 | - * when they update the file, the contents haven't changed in the mean |
|
105 | - * time. |
|
106 | - * |
|
107 | - * If you don't plan to store the file byte-by-byte, and you return a |
|
108 | - * different object on a subsequent GET you are strongly recommended to not |
|
109 | - * return an ETag, and just return null. |
|
110 | - * |
|
111 | - * @param resource $data |
|
112 | - * |
|
113 | - * @throws Forbidden |
|
114 | - * @throws UnsupportedMediaType |
|
115 | - * @throws BadRequest |
|
116 | - * @throws Exception |
|
117 | - * @throws EntityTooLarge |
|
118 | - * @throws ServiceUnavailable |
|
119 | - * @throws FileLocked |
|
120 | - * @return string|null |
|
121 | - */ |
|
122 | - public function put($data) { |
|
123 | - try { |
|
124 | - $exists = $this->fileView->file_exists($this->path); |
|
125 | - if ($this->info && $exists && !$this->info->isUpdateable()) { |
|
126 | - throw new Forbidden(); |
|
127 | - } |
|
128 | - } catch (StorageNotAvailableException $e) { |
|
129 | - throw new ServiceUnavailable("File is not updatable: " . $e->getMessage()); |
|
130 | - } |
|
131 | - |
|
132 | - // verify path of the target |
|
133 | - $this->verifyPath(); |
|
134 | - |
|
135 | - // chunked handling |
|
136 | - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { |
|
137 | - try { |
|
138 | - return $this->createFileChunked($data); |
|
139 | - } catch (\Exception $e) { |
|
140 | - $this->convertToSabreException($e); |
|
141 | - } |
|
142 | - } |
|
143 | - |
|
144 | - /** @var Storage $partStorage */ |
|
145 | - list($partStorage) = $this->fileView->resolvePath($this->path); |
|
146 | - $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1); |
|
147 | - |
|
148 | - $view = \OC\Files\Filesystem::getView(); |
|
149 | - |
|
150 | - if ($needsPartFile) { |
|
151 | - // mark file as partial while uploading (ignored by the scanner) |
|
152 | - $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; |
|
153 | - |
|
154 | - if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) { |
|
155 | - $needsPartFile = false; |
|
156 | - } |
|
157 | - } |
|
158 | - if (!$needsPartFile) { |
|
159 | - // upload file directly as the final path |
|
160 | - $partFilePath = $this->path; |
|
161 | - |
|
162 | - if ($view && !$this->emitPreHooks($exists)) { |
|
163 | - throw new Exception('Could not write to final file, canceled by hook'); |
|
164 | - } |
|
165 | - } |
|
166 | - |
|
167 | - // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) |
|
168 | - /** @var \OC\Files\Storage\Storage $partStorage */ |
|
169 | - list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath); |
|
170 | - /** @var \OC\Files\Storage\Storage $storage */ |
|
171 | - list($storage, $internalPath) = $this->fileView->resolvePath($this->path); |
|
172 | - try { |
|
173 | - if (!$needsPartFile) { |
|
174 | - $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
175 | - } |
|
176 | - |
|
177 | - if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) { |
|
178 | - |
|
179 | - if (!is_resource($data)) { |
|
180 | - $tmpData = fopen('php://temp', 'r+'); |
|
181 | - if ($data !== null) { |
|
182 | - fwrite($tmpData, $data); |
|
183 | - rewind($tmpData); |
|
184 | - } |
|
185 | - $data = $tmpData; |
|
186 | - } |
|
187 | - |
|
188 | - $isEOF = false; |
|
189 | - $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) { |
|
190 | - $isEOF = feof($stream); |
|
191 | - }); |
|
192 | - |
|
193 | - $count = $partStorage->writeStream($internalPartPath, $wrappedData); |
|
194 | - $result = $count > 0; |
|
195 | - |
|
196 | - if ($result === false) { |
|
197 | - $result = $isEOF; |
|
198 | - if (is_resource($wrappedData)) { |
|
199 | - $result = feof($wrappedData); |
|
200 | - } |
|
201 | - } |
|
202 | - |
|
203 | - } else { |
|
204 | - $target = $partStorage->fopen($internalPartPath, 'wb'); |
|
205 | - if ($target === false) { |
|
206 | - \OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); |
|
207 | - // because we have no clue about the cause we can only throw back a 500/Internal Server Error |
|
208 | - throw new Exception('Could not write file contents'); |
|
209 | - } |
|
210 | - list($count, $result) = \OC_Helper::streamCopy($data, $target); |
|
211 | - fclose($target); |
|
212 | - } |
|
213 | - |
|
214 | - if ($result === false) { |
|
215 | - $expected = -1; |
|
216 | - if (isset($_SERVER['CONTENT_LENGTH'])) { |
|
217 | - $expected = $_SERVER['CONTENT_LENGTH']; |
|
218 | - } |
|
219 | - if ($expected !== "0") { |
|
220 | - throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )'); |
|
221 | - } |
|
222 | - } |
|
223 | - |
|
224 | - // if content length is sent by client: |
|
225 | - // double check if the file was fully received |
|
226 | - // compare expected and actual size |
|
227 | - if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
|
228 | - $expected = (int)$_SERVER['CONTENT_LENGTH']; |
|
229 | - if ($count !== $expected) { |
|
230 | - throw new BadRequest('Expected filesize of ' . $expected . ' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) ' . $count . ' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
231 | - } |
|
232 | - } |
|
233 | - |
|
234 | - } catch (\Exception $e) { |
|
235 | - $context = []; |
|
236 | - |
|
237 | - if ($e instanceof LockedException) { |
|
238 | - $context['level'] = ILogger::DEBUG; |
|
239 | - } |
|
240 | - |
|
241 | - \OC::$server->getLogger()->logException($e, $context); |
|
242 | - if ($needsPartFile) { |
|
243 | - $partStorage->unlink($internalPartPath); |
|
244 | - } |
|
245 | - $this->convertToSabreException($e); |
|
246 | - } |
|
247 | - |
|
248 | - try { |
|
249 | - if ($needsPartFile) { |
|
250 | - if ($view && !$this->emitPreHooks($exists)) { |
|
251 | - $partStorage->unlink($internalPartPath); |
|
252 | - throw new Exception('Could not rename part file to final file, canceled by hook'); |
|
253 | - } |
|
254 | - try { |
|
255 | - $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
256 | - } catch (LockedException $e) { |
|
257 | - // during very large uploads, the shared lock we got at the start might have been expired |
|
258 | - // meaning that the above lock can fail not just only because somebody else got a shared lock |
|
259 | - // or because there is no existing shared lock to make exclusive |
|
260 | - // |
|
261 | - // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
|
262 | - // lock this will still fail, if our original shared lock expired the new lock will be successful and |
|
263 | - // the entire operation will be safe |
|
264 | - |
|
265 | - try { |
|
266 | - $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
267 | - } catch (LockedException $ex) { |
|
268 | - if ($needsPartFile) { |
|
269 | - $partStorage->unlink($internalPartPath); |
|
270 | - } |
|
271 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
272 | - } |
|
273 | - } |
|
274 | - |
|
275 | - // rename to correct path |
|
276 | - try { |
|
277 | - $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); |
|
278 | - $fileExists = $storage->file_exists($internalPath); |
|
279 | - if ($renameOkay === false || $fileExists === false) { |
|
280 | - \OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); |
|
281 | - throw new Exception('Could not rename part file to final file'); |
|
282 | - } |
|
283 | - } catch (ForbiddenException $ex) { |
|
284 | - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
285 | - } catch (\Exception $e) { |
|
286 | - $partStorage->unlink($internalPartPath); |
|
287 | - $this->convertToSabreException($e); |
|
288 | - } |
|
289 | - } |
|
290 | - |
|
291 | - // since we skipped the view we need to scan and emit the hooks ourselves |
|
292 | - $storage->getUpdater()->update($internalPath); |
|
293 | - |
|
294 | - try { |
|
295 | - $this->changeLock(ILockingProvider::LOCK_SHARED); |
|
296 | - } catch (LockedException $e) { |
|
297 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
298 | - } |
|
299 | - |
|
300 | - // allow sync clients to send the mtime along in a header |
|
301 | - if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
|
302 | - $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']); |
|
303 | - if ($this->fileView->touch($this->path, $mtime)) { |
|
304 | - $this->header('X-OC-MTime: accepted'); |
|
305 | - } |
|
306 | - } |
|
307 | - |
|
308 | - $fileInfoUpdate = [ |
|
309 | - 'upload_time' => time() |
|
310 | - ]; |
|
311 | - |
|
312 | - // allow sync clients to send the creation time along in a header |
|
313 | - if (isset($this->request->server['HTTP_X_OC_CTIME'])) { |
|
314 | - $ctime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_CTIME']); |
|
315 | - $fileInfoUpdate['creation_time'] = $ctime; |
|
316 | - $this->header('X-OC-CTime: accepted'); |
|
317 | - } |
|
318 | - |
|
319 | - $this->fileView->putFileInfo($this->path, $fileInfoUpdate); |
|
320 | - |
|
321 | - if ($view) { |
|
322 | - $this->emitPostHooks($exists); |
|
323 | - } |
|
324 | - |
|
325 | - $this->refreshInfo(); |
|
326 | - |
|
327 | - if (isset($this->request->server['HTTP_OC_CHECKSUM'])) { |
|
328 | - $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']); |
|
329 | - $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); |
|
330 | - $this->refreshInfo(); |
|
331 | - } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') { |
|
332 | - $this->fileView->putFileInfo($this->path, ['checksum' => '']); |
|
333 | - $this->refreshInfo(); |
|
334 | - } |
|
335 | - |
|
336 | - } catch (StorageNotAvailableException $e) { |
|
337 | - throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage(), 0, $e); |
|
338 | - } |
|
339 | - |
|
340 | - return '"' . $this->info->getEtag() . '"'; |
|
341 | - } |
|
342 | - |
|
343 | - private function getPartFileBasePath($path) { |
|
344 | - $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); |
|
345 | - if ($partFileInStorage) { |
|
346 | - return $path; |
|
347 | - } else { |
|
348 | - return md5($path); // will place it in the root of the view with a unique name |
|
349 | - } |
|
350 | - } |
|
351 | - |
|
352 | - /** |
|
353 | - * @param string $path |
|
354 | - */ |
|
355 | - private function emitPreHooks($exists, $path = null) { |
|
356 | - if (is_null($path)) { |
|
357 | - $path = $this->path; |
|
358 | - } |
|
359 | - $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
360 | - $run = true; |
|
361 | - |
|
362 | - if (!$exists) { |
|
363 | - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [ |
|
364 | - \OC\Files\Filesystem::signal_param_path => $hookPath, |
|
365 | - \OC\Files\Filesystem::signal_param_run => &$run, |
|
366 | - ]); |
|
367 | - } else { |
|
368 | - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [ |
|
369 | - \OC\Files\Filesystem::signal_param_path => $hookPath, |
|
370 | - \OC\Files\Filesystem::signal_param_run => &$run, |
|
371 | - ]); |
|
372 | - } |
|
373 | - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [ |
|
374 | - \OC\Files\Filesystem::signal_param_path => $hookPath, |
|
375 | - \OC\Files\Filesystem::signal_param_run => &$run, |
|
376 | - ]); |
|
377 | - return $run; |
|
378 | - } |
|
379 | - |
|
380 | - /** |
|
381 | - * @param string $path |
|
382 | - */ |
|
383 | - private function emitPostHooks($exists, $path = null) { |
|
384 | - if (is_null($path)) { |
|
385 | - $path = $this->path; |
|
386 | - } |
|
387 | - $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
388 | - if (!$exists) { |
|
389 | - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [ |
|
390 | - \OC\Files\Filesystem::signal_param_path => $hookPath |
|
391 | - ]); |
|
392 | - } else { |
|
393 | - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [ |
|
394 | - \OC\Files\Filesystem::signal_param_path => $hookPath |
|
395 | - ]); |
|
396 | - } |
|
397 | - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [ |
|
398 | - \OC\Files\Filesystem::signal_param_path => $hookPath |
|
399 | - ]); |
|
400 | - } |
|
401 | - |
|
402 | - /** |
|
403 | - * Returns the data |
|
404 | - * |
|
405 | - * @return resource |
|
406 | - * @throws Forbidden |
|
407 | - * @throws ServiceUnavailable |
|
408 | - */ |
|
409 | - public function get() { |
|
410 | - //throw exception if encryption is disabled but files are still encrypted |
|
411 | - try { |
|
412 | - if (!$this->info->isReadable()) { |
|
413 | - // do a if the file did not exist |
|
414 | - throw new NotFound(); |
|
415 | - } |
|
416 | - try { |
|
417 | - $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb'); |
|
418 | - } catch (\Exception $e) { |
|
419 | - $this->convertToSabreException($e); |
|
420 | - } |
|
421 | - if ($res === false) { |
|
422 | - throw new ServiceUnavailable("Could not open file"); |
|
423 | - } |
|
424 | - return $res; |
|
425 | - } catch (GenericEncryptionException $e) { |
|
426 | - // returning 503 will allow retry of the operation at a later point in time |
|
427 | - throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); |
|
428 | - } catch (StorageNotAvailableException $e) { |
|
429 | - throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); |
|
430 | - } catch (ForbiddenException $ex) { |
|
431 | - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
432 | - } catch (LockedException $e) { |
|
433 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
434 | - } |
|
435 | - } |
|
436 | - |
|
437 | - /** |
|
438 | - * Delete the current file |
|
439 | - * |
|
440 | - * @throws Forbidden |
|
441 | - * @throws ServiceUnavailable |
|
442 | - */ |
|
443 | - public function delete() { |
|
444 | - if (!$this->info->isDeletable()) { |
|
445 | - throw new Forbidden(); |
|
446 | - } |
|
447 | - |
|
448 | - try { |
|
449 | - if (!$this->fileView->unlink($this->path)) { |
|
450 | - // assume it wasn't possible to delete due to permissions |
|
451 | - throw new Forbidden(); |
|
452 | - } |
|
453 | - } catch (StorageNotAvailableException $e) { |
|
454 | - throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); |
|
455 | - } catch (ForbiddenException $ex) { |
|
456 | - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
457 | - } catch (LockedException $e) { |
|
458 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
459 | - } |
|
460 | - } |
|
461 | - |
|
462 | - /** |
|
463 | - * Returns the mime-type for a file |
|
464 | - * |
|
465 | - * If null is returned, we'll assume application/octet-stream |
|
466 | - * |
|
467 | - * @return string |
|
468 | - */ |
|
469 | - public function getContentType() { |
|
470 | - $mimeType = $this->info->getMimetype(); |
|
471 | - |
|
472 | - // PROPFIND needs to return the correct mime type, for consistency with the web UI |
|
473 | - if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { |
|
474 | - return $mimeType; |
|
475 | - } |
|
476 | - return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType); |
|
477 | - } |
|
478 | - |
|
479 | - /** |
|
480 | - * @return array|false |
|
481 | - */ |
|
482 | - public function getDirectDownload() { |
|
483 | - if (\OCP\App::isEnabled('encryption')) { |
|
484 | - return []; |
|
485 | - } |
|
486 | - /** @var \OCP\Files\Storage $storage */ |
|
487 | - list($storage, $internalPath) = $this->fileView->resolvePath($this->path); |
|
488 | - if (is_null($storage)) { |
|
489 | - return []; |
|
490 | - } |
|
491 | - |
|
492 | - return $storage->getDirectDownload($internalPath); |
|
493 | - } |
|
494 | - |
|
495 | - /** |
|
496 | - * @param resource $data |
|
497 | - * @return null|string |
|
498 | - * @throws Exception |
|
499 | - * @throws BadRequest |
|
500 | - * @throws NotImplemented |
|
501 | - * @throws ServiceUnavailable |
|
502 | - */ |
|
503 | - private function createFileChunked($data) { |
|
504 | - list($path, $name) = \Sabre\Uri\split($this->path); |
|
505 | - |
|
506 | - $info = \OC_FileChunking::decodeName($name); |
|
507 | - if (empty($info)) { |
|
508 | - throw new NotImplemented('Invalid chunk name'); |
|
509 | - } |
|
510 | - |
|
511 | - $chunk_handler = new \OC_FileChunking($info); |
|
512 | - $bytesWritten = $chunk_handler->store($info['index'], $data); |
|
513 | - |
|
514 | - //detect aborted upload |
|
515 | - if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
|
516 | - if (isset($_SERVER['CONTENT_LENGTH'])) { |
|
517 | - $expected = (int)$_SERVER['CONTENT_LENGTH']; |
|
518 | - if ($bytesWritten !== $expected) { |
|
519 | - $chunk_handler->remove($info['index']); |
|
520 | - throw new BadRequest('Expected filesize of ' . $expected . ' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) ' . $bytesWritten . ' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
521 | - } |
|
522 | - } |
|
523 | - } |
|
524 | - |
|
525 | - if ($chunk_handler->isComplete()) { |
|
526 | - /** @var Storage $storage */ |
|
527 | - list($storage,) = $this->fileView->resolvePath($path); |
|
528 | - $needsPartFile = $storage->needsPartFile(); |
|
529 | - $partFile = null; |
|
530 | - |
|
531 | - $targetPath = $path . '/' . $info['name']; |
|
532 | - /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
533 | - list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); |
|
534 | - |
|
535 | - $exists = $this->fileView->file_exists($targetPath); |
|
536 | - |
|
537 | - try { |
|
538 | - $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); |
|
539 | - |
|
540 | - $this->emitPreHooks($exists, $targetPath); |
|
541 | - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); |
|
542 | - /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
543 | - list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); |
|
544 | - |
|
545 | - if ($needsPartFile) { |
|
546 | - // we first assembly the target file as a part file |
|
547 | - $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; |
|
548 | - /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
549 | - list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile); |
|
550 | - |
|
551 | - |
|
552 | - $chunk_handler->file_assemble($partStorage, $partInternalPath); |
|
553 | - |
|
554 | - // here is the final atomic rename |
|
555 | - $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); |
|
556 | - $fileExists = $targetStorage->file_exists($targetInternalPath); |
|
557 | - if ($renameOkay === false || $fileExists === false) { |
|
558 | - \OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']); |
|
559 | - // only delete if an error occurred and the target file was already created |
|
560 | - if ($fileExists) { |
|
561 | - // set to null to avoid double-deletion when handling exception |
|
562 | - // stray part file |
|
563 | - $partFile = null; |
|
564 | - $targetStorage->unlink($targetInternalPath); |
|
565 | - } |
|
566 | - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
|
567 | - throw new Exception('Could not rename part file assembled from chunks'); |
|
568 | - } |
|
569 | - } else { |
|
570 | - // assemble directly into the final file |
|
571 | - $chunk_handler->file_assemble($targetStorage, $targetInternalPath); |
|
572 | - } |
|
573 | - |
|
574 | - // allow sync clients to send the mtime along in a header |
|
575 | - if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
|
576 | - $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']); |
|
577 | - if ($targetStorage->touch($targetInternalPath, $mtime)) { |
|
578 | - $this->header('X-OC-MTime: accepted'); |
|
579 | - } |
|
580 | - } |
|
581 | - |
|
582 | - // since we skipped the view we need to scan and emit the hooks ourselves |
|
583 | - $targetStorage->getUpdater()->update($targetInternalPath); |
|
584 | - |
|
585 | - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
|
586 | - |
|
587 | - $this->emitPostHooks($exists, $targetPath); |
|
588 | - |
|
589 | - // FIXME: should call refreshInfo but can't because $this->path is not the of the final file |
|
590 | - $info = $this->fileView->getFileInfo($targetPath); |
|
591 | - |
|
592 | - if (isset($this->request->server['HTTP_OC_CHECKSUM'])) { |
|
593 | - $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']); |
|
594 | - $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]); |
|
595 | - } else if ($info->getChecksum() !== null && $info->getChecksum() !== '') { |
|
596 | - $this->fileView->putFileInfo($this->path, ['checksum' => '']); |
|
597 | - } |
|
598 | - |
|
599 | - $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); |
|
600 | - |
|
601 | - return $info->getEtag(); |
|
602 | - } catch (\Exception $e) { |
|
603 | - if ($partFile !== null) { |
|
604 | - $targetStorage->unlink($targetInternalPath); |
|
605 | - } |
|
606 | - $this->convertToSabreException($e); |
|
607 | - } |
|
608 | - } |
|
609 | - |
|
610 | - return null; |
|
611 | - } |
|
612 | - |
|
613 | - /** |
|
614 | - * Convert the given exception to a SabreException instance |
|
615 | - * |
|
616 | - * @param \Exception $e |
|
617 | - * |
|
618 | - * @throws \Sabre\DAV\Exception |
|
619 | - */ |
|
620 | - private function convertToSabreException(\Exception $e) { |
|
621 | - if ($e instanceof \Sabre\DAV\Exception) { |
|
622 | - throw $e; |
|
623 | - } |
|
624 | - if ($e instanceof NotPermittedException) { |
|
625 | - // a more general case - due to whatever reason the content could not be written |
|
626 | - throw new Forbidden($e->getMessage(), 0, $e); |
|
627 | - } |
|
628 | - if ($e instanceof ForbiddenException) { |
|
629 | - // the path for the file was forbidden |
|
630 | - throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); |
|
631 | - } |
|
632 | - if ($e instanceof EntityTooLargeException) { |
|
633 | - // the file is too big to be stored |
|
634 | - throw new EntityTooLarge($e->getMessage(), 0, $e); |
|
635 | - } |
|
636 | - if ($e instanceof InvalidContentException) { |
|
637 | - // the file content is not permitted |
|
638 | - throw new UnsupportedMediaType($e->getMessage(), 0, $e); |
|
639 | - } |
|
640 | - if ($e instanceof InvalidPathException) { |
|
641 | - // the path for the file was not valid |
|
642 | - // TODO: find proper http status code for this case |
|
643 | - throw new Forbidden($e->getMessage(), 0, $e); |
|
644 | - } |
|
645 | - if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { |
|
646 | - // the file is currently being written to by another process |
|
647 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
648 | - } |
|
649 | - if ($e instanceof GenericEncryptionException) { |
|
650 | - // returning 503 will allow retry of the operation at a later point in time |
|
651 | - throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e); |
|
652 | - } |
|
653 | - if ($e instanceof StorageNotAvailableException) { |
|
654 | - throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e); |
|
655 | - } |
|
656 | - if ($e instanceof NotFoundException) { |
|
657 | - throw new NotFound('File not found: ' . $e->getMessage(), 0, $e); |
|
658 | - } |
|
659 | - |
|
660 | - throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); |
|
661 | - } |
|
662 | - |
|
663 | - /** |
|
664 | - * Get the checksum for this file |
|
665 | - * |
|
666 | - * @return string |
|
667 | - */ |
|
668 | - public function getChecksum() { |
|
669 | - return $this->info->getChecksum(); |
|
670 | - } |
|
671 | - |
|
672 | - protected function header($string) { |
|
673 | - \header($string); |
|
674 | - } |
|
74 | + protected $request; |
|
75 | + |
|
76 | + /** |
|
77 | + * Sets up the node, expects a full path name |
|
78 | + * |
|
79 | + * @param \OC\Files\View $view |
|
80 | + * @param \OCP\Files\FileInfo $info |
|
81 | + * @param \OCP\Share\IManager $shareManager |
|
82 | + * @param \OC\AppFramework\Http\Request $request |
|
83 | + */ |
|
84 | + public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) { |
|
85 | + parent::__construct($view, $info, $shareManager); |
|
86 | + |
|
87 | + if (isset($request)) { |
|
88 | + $this->request = $request; |
|
89 | + } else { |
|
90 | + $this->request = \OC::$server->getRequest(); |
|
91 | + } |
|
92 | + } |
|
93 | + |
|
94 | + /** |
|
95 | + * Updates the data |
|
96 | + * |
|
97 | + * The data argument is a readable stream resource. |
|
98 | + * |
|
99 | + * After a successful put operation, you may choose to return an ETag. The |
|
100 | + * etag must always be surrounded by double-quotes. These quotes must |
|
101 | + * appear in the actual string you're returning. |
|
102 | + * |
|
103 | + * Clients may use the ETag from a PUT request to later on make sure that |
|
104 | + * when they update the file, the contents haven't changed in the mean |
|
105 | + * time. |
|
106 | + * |
|
107 | + * If you don't plan to store the file byte-by-byte, and you return a |
|
108 | + * different object on a subsequent GET you are strongly recommended to not |
|
109 | + * return an ETag, and just return null. |
|
110 | + * |
|
111 | + * @param resource $data |
|
112 | + * |
|
113 | + * @throws Forbidden |
|
114 | + * @throws UnsupportedMediaType |
|
115 | + * @throws BadRequest |
|
116 | + * @throws Exception |
|
117 | + * @throws EntityTooLarge |
|
118 | + * @throws ServiceUnavailable |
|
119 | + * @throws FileLocked |
|
120 | + * @return string|null |
|
121 | + */ |
|
122 | + public function put($data) { |
|
123 | + try { |
|
124 | + $exists = $this->fileView->file_exists($this->path); |
|
125 | + if ($this->info && $exists && !$this->info->isUpdateable()) { |
|
126 | + throw new Forbidden(); |
|
127 | + } |
|
128 | + } catch (StorageNotAvailableException $e) { |
|
129 | + throw new ServiceUnavailable("File is not updatable: " . $e->getMessage()); |
|
130 | + } |
|
131 | + |
|
132 | + // verify path of the target |
|
133 | + $this->verifyPath(); |
|
134 | + |
|
135 | + // chunked handling |
|
136 | + if (isset($_SERVER['HTTP_OC_CHUNKED'])) { |
|
137 | + try { |
|
138 | + return $this->createFileChunked($data); |
|
139 | + } catch (\Exception $e) { |
|
140 | + $this->convertToSabreException($e); |
|
141 | + } |
|
142 | + } |
|
143 | + |
|
144 | + /** @var Storage $partStorage */ |
|
145 | + list($partStorage) = $this->fileView->resolvePath($this->path); |
|
146 | + $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1); |
|
147 | + |
|
148 | + $view = \OC\Files\Filesystem::getView(); |
|
149 | + |
|
150 | + if ($needsPartFile) { |
|
151 | + // mark file as partial while uploading (ignored by the scanner) |
|
152 | + $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; |
|
153 | + |
|
154 | + if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) { |
|
155 | + $needsPartFile = false; |
|
156 | + } |
|
157 | + } |
|
158 | + if (!$needsPartFile) { |
|
159 | + // upload file directly as the final path |
|
160 | + $partFilePath = $this->path; |
|
161 | + |
|
162 | + if ($view && !$this->emitPreHooks($exists)) { |
|
163 | + throw new Exception('Could not write to final file, canceled by hook'); |
|
164 | + } |
|
165 | + } |
|
166 | + |
|
167 | + // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) |
|
168 | + /** @var \OC\Files\Storage\Storage $partStorage */ |
|
169 | + list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath); |
|
170 | + /** @var \OC\Files\Storage\Storage $storage */ |
|
171 | + list($storage, $internalPath) = $this->fileView->resolvePath($this->path); |
|
172 | + try { |
|
173 | + if (!$needsPartFile) { |
|
174 | + $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
175 | + } |
|
176 | + |
|
177 | + if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) { |
|
178 | + |
|
179 | + if (!is_resource($data)) { |
|
180 | + $tmpData = fopen('php://temp', 'r+'); |
|
181 | + if ($data !== null) { |
|
182 | + fwrite($tmpData, $data); |
|
183 | + rewind($tmpData); |
|
184 | + } |
|
185 | + $data = $tmpData; |
|
186 | + } |
|
187 | + |
|
188 | + $isEOF = false; |
|
189 | + $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) { |
|
190 | + $isEOF = feof($stream); |
|
191 | + }); |
|
192 | + |
|
193 | + $count = $partStorage->writeStream($internalPartPath, $wrappedData); |
|
194 | + $result = $count > 0; |
|
195 | + |
|
196 | + if ($result === false) { |
|
197 | + $result = $isEOF; |
|
198 | + if (is_resource($wrappedData)) { |
|
199 | + $result = feof($wrappedData); |
|
200 | + } |
|
201 | + } |
|
202 | + |
|
203 | + } else { |
|
204 | + $target = $partStorage->fopen($internalPartPath, 'wb'); |
|
205 | + if ($target === false) { |
|
206 | + \OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); |
|
207 | + // because we have no clue about the cause we can only throw back a 500/Internal Server Error |
|
208 | + throw new Exception('Could not write file contents'); |
|
209 | + } |
|
210 | + list($count, $result) = \OC_Helper::streamCopy($data, $target); |
|
211 | + fclose($target); |
|
212 | + } |
|
213 | + |
|
214 | + if ($result === false) { |
|
215 | + $expected = -1; |
|
216 | + if (isset($_SERVER['CONTENT_LENGTH'])) { |
|
217 | + $expected = $_SERVER['CONTENT_LENGTH']; |
|
218 | + } |
|
219 | + if ($expected !== "0") { |
|
220 | + throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )'); |
|
221 | + } |
|
222 | + } |
|
223 | + |
|
224 | + // if content length is sent by client: |
|
225 | + // double check if the file was fully received |
|
226 | + // compare expected and actual size |
|
227 | + if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
|
228 | + $expected = (int)$_SERVER['CONTENT_LENGTH']; |
|
229 | + if ($count !== $expected) { |
|
230 | + throw new BadRequest('Expected filesize of ' . $expected . ' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) ' . $count . ' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
231 | + } |
|
232 | + } |
|
233 | + |
|
234 | + } catch (\Exception $e) { |
|
235 | + $context = []; |
|
236 | + |
|
237 | + if ($e instanceof LockedException) { |
|
238 | + $context['level'] = ILogger::DEBUG; |
|
239 | + } |
|
240 | + |
|
241 | + \OC::$server->getLogger()->logException($e, $context); |
|
242 | + if ($needsPartFile) { |
|
243 | + $partStorage->unlink($internalPartPath); |
|
244 | + } |
|
245 | + $this->convertToSabreException($e); |
|
246 | + } |
|
247 | + |
|
248 | + try { |
|
249 | + if ($needsPartFile) { |
|
250 | + if ($view && !$this->emitPreHooks($exists)) { |
|
251 | + $partStorage->unlink($internalPartPath); |
|
252 | + throw new Exception('Could not rename part file to final file, canceled by hook'); |
|
253 | + } |
|
254 | + try { |
|
255 | + $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
256 | + } catch (LockedException $e) { |
|
257 | + // during very large uploads, the shared lock we got at the start might have been expired |
|
258 | + // meaning that the above lock can fail not just only because somebody else got a shared lock |
|
259 | + // or because there is no existing shared lock to make exclusive |
|
260 | + // |
|
261 | + // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
|
262 | + // lock this will still fail, if our original shared lock expired the new lock will be successful and |
|
263 | + // the entire operation will be safe |
|
264 | + |
|
265 | + try { |
|
266 | + $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
267 | + } catch (LockedException $ex) { |
|
268 | + if ($needsPartFile) { |
|
269 | + $partStorage->unlink($internalPartPath); |
|
270 | + } |
|
271 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
272 | + } |
|
273 | + } |
|
274 | + |
|
275 | + // rename to correct path |
|
276 | + try { |
|
277 | + $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); |
|
278 | + $fileExists = $storage->file_exists($internalPath); |
|
279 | + if ($renameOkay === false || $fileExists === false) { |
|
280 | + \OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); |
|
281 | + throw new Exception('Could not rename part file to final file'); |
|
282 | + } |
|
283 | + } catch (ForbiddenException $ex) { |
|
284 | + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
285 | + } catch (\Exception $e) { |
|
286 | + $partStorage->unlink($internalPartPath); |
|
287 | + $this->convertToSabreException($e); |
|
288 | + } |
|
289 | + } |
|
290 | + |
|
291 | + // since we skipped the view we need to scan and emit the hooks ourselves |
|
292 | + $storage->getUpdater()->update($internalPath); |
|
293 | + |
|
294 | + try { |
|
295 | + $this->changeLock(ILockingProvider::LOCK_SHARED); |
|
296 | + } catch (LockedException $e) { |
|
297 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
298 | + } |
|
299 | + |
|
300 | + // allow sync clients to send the mtime along in a header |
|
301 | + if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
|
302 | + $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']); |
|
303 | + if ($this->fileView->touch($this->path, $mtime)) { |
|
304 | + $this->header('X-OC-MTime: accepted'); |
|
305 | + } |
|
306 | + } |
|
307 | + |
|
308 | + $fileInfoUpdate = [ |
|
309 | + 'upload_time' => time() |
|
310 | + ]; |
|
311 | + |
|
312 | + // allow sync clients to send the creation time along in a header |
|
313 | + if (isset($this->request->server['HTTP_X_OC_CTIME'])) { |
|
314 | + $ctime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_CTIME']); |
|
315 | + $fileInfoUpdate['creation_time'] = $ctime; |
|
316 | + $this->header('X-OC-CTime: accepted'); |
|
317 | + } |
|
318 | + |
|
319 | + $this->fileView->putFileInfo($this->path, $fileInfoUpdate); |
|
320 | + |
|
321 | + if ($view) { |
|
322 | + $this->emitPostHooks($exists); |
|
323 | + } |
|
324 | + |
|
325 | + $this->refreshInfo(); |
|
326 | + |
|
327 | + if (isset($this->request->server['HTTP_OC_CHECKSUM'])) { |
|
328 | + $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']); |
|
329 | + $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); |
|
330 | + $this->refreshInfo(); |
|
331 | + } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') { |
|
332 | + $this->fileView->putFileInfo($this->path, ['checksum' => '']); |
|
333 | + $this->refreshInfo(); |
|
334 | + } |
|
335 | + |
|
336 | + } catch (StorageNotAvailableException $e) { |
|
337 | + throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage(), 0, $e); |
|
338 | + } |
|
339 | + |
|
340 | + return '"' . $this->info->getEtag() . '"'; |
|
341 | + } |
|
342 | + |
|
343 | + private function getPartFileBasePath($path) { |
|
344 | + $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); |
|
345 | + if ($partFileInStorage) { |
|
346 | + return $path; |
|
347 | + } else { |
|
348 | + return md5($path); // will place it in the root of the view with a unique name |
|
349 | + } |
|
350 | + } |
|
351 | + |
|
352 | + /** |
|
353 | + * @param string $path |
|
354 | + */ |
|
355 | + private function emitPreHooks($exists, $path = null) { |
|
356 | + if (is_null($path)) { |
|
357 | + $path = $this->path; |
|
358 | + } |
|
359 | + $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
360 | + $run = true; |
|
361 | + |
|
362 | + if (!$exists) { |
|
363 | + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [ |
|
364 | + \OC\Files\Filesystem::signal_param_path => $hookPath, |
|
365 | + \OC\Files\Filesystem::signal_param_run => &$run, |
|
366 | + ]); |
|
367 | + } else { |
|
368 | + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [ |
|
369 | + \OC\Files\Filesystem::signal_param_path => $hookPath, |
|
370 | + \OC\Files\Filesystem::signal_param_run => &$run, |
|
371 | + ]); |
|
372 | + } |
|
373 | + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [ |
|
374 | + \OC\Files\Filesystem::signal_param_path => $hookPath, |
|
375 | + \OC\Files\Filesystem::signal_param_run => &$run, |
|
376 | + ]); |
|
377 | + return $run; |
|
378 | + } |
|
379 | + |
|
380 | + /** |
|
381 | + * @param string $path |
|
382 | + */ |
|
383 | + private function emitPostHooks($exists, $path = null) { |
|
384 | + if (is_null($path)) { |
|
385 | + $path = $this->path; |
|
386 | + } |
|
387 | + $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
388 | + if (!$exists) { |
|
389 | + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [ |
|
390 | + \OC\Files\Filesystem::signal_param_path => $hookPath |
|
391 | + ]); |
|
392 | + } else { |
|
393 | + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [ |
|
394 | + \OC\Files\Filesystem::signal_param_path => $hookPath |
|
395 | + ]); |
|
396 | + } |
|
397 | + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [ |
|
398 | + \OC\Files\Filesystem::signal_param_path => $hookPath |
|
399 | + ]); |
|
400 | + } |
|
401 | + |
|
402 | + /** |
|
403 | + * Returns the data |
|
404 | + * |
|
405 | + * @return resource |
|
406 | + * @throws Forbidden |
|
407 | + * @throws ServiceUnavailable |
|
408 | + */ |
|
409 | + public function get() { |
|
410 | + //throw exception if encryption is disabled but files are still encrypted |
|
411 | + try { |
|
412 | + if (!$this->info->isReadable()) { |
|
413 | + // do a if the file did not exist |
|
414 | + throw new NotFound(); |
|
415 | + } |
|
416 | + try { |
|
417 | + $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb'); |
|
418 | + } catch (\Exception $e) { |
|
419 | + $this->convertToSabreException($e); |
|
420 | + } |
|
421 | + if ($res === false) { |
|
422 | + throw new ServiceUnavailable("Could not open file"); |
|
423 | + } |
|
424 | + return $res; |
|
425 | + } catch (GenericEncryptionException $e) { |
|
426 | + // returning 503 will allow retry of the operation at a later point in time |
|
427 | + throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); |
|
428 | + } catch (StorageNotAvailableException $e) { |
|
429 | + throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); |
|
430 | + } catch (ForbiddenException $ex) { |
|
431 | + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
432 | + } catch (LockedException $e) { |
|
433 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
434 | + } |
|
435 | + } |
|
436 | + |
|
437 | + /** |
|
438 | + * Delete the current file |
|
439 | + * |
|
440 | + * @throws Forbidden |
|
441 | + * @throws ServiceUnavailable |
|
442 | + */ |
|
443 | + public function delete() { |
|
444 | + if (!$this->info->isDeletable()) { |
|
445 | + throw new Forbidden(); |
|
446 | + } |
|
447 | + |
|
448 | + try { |
|
449 | + if (!$this->fileView->unlink($this->path)) { |
|
450 | + // assume it wasn't possible to delete due to permissions |
|
451 | + throw new Forbidden(); |
|
452 | + } |
|
453 | + } catch (StorageNotAvailableException $e) { |
|
454 | + throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); |
|
455 | + } catch (ForbiddenException $ex) { |
|
456 | + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
457 | + } catch (LockedException $e) { |
|
458 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
459 | + } |
|
460 | + } |
|
461 | + |
|
462 | + /** |
|
463 | + * Returns the mime-type for a file |
|
464 | + * |
|
465 | + * If null is returned, we'll assume application/octet-stream |
|
466 | + * |
|
467 | + * @return string |
|
468 | + */ |
|
469 | + public function getContentType() { |
|
470 | + $mimeType = $this->info->getMimetype(); |
|
471 | + |
|
472 | + // PROPFIND needs to return the correct mime type, for consistency with the web UI |
|
473 | + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { |
|
474 | + return $mimeType; |
|
475 | + } |
|
476 | + return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType); |
|
477 | + } |
|
478 | + |
|
479 | + /** |
|
480 | + * @return array|false |
|
481 | + */ |
|
482 | + public function getDirectDownload() { |
|
483 | + if (\OCP\App::isEnabled('encryption')) { |
|
484 | + return []; |
|
485 | + } |
|
486 | + /** @var \OCP\Files\Storage $storage */ |
|
487 | + list($storage, $internalPath) = $this->fileView->resolvePath($this->path); |
|
488 | + if (is_null($storage)) { |
|
489 | + return []; |
|
490 | + } |
|
491 | + |
|
492 | + return $storage->getDirectDownload($internalPath); |
|
493 | + } |
|
494 | + |
|
495 | + /** |
|
496 | + * @param resource $data |
|
497 | + * @return null|string |
|
498 | + * @throws Exception |
|
499 | + * @throws BadRequest |
|
500 | + * @throws NotImplemented |
|
501 | + * @throws ServiceUnavailable |
|
502 | + */ |
|
503 | + private function createFileChunked($data) { |
|
504 | + list($path, $name) = \Sabre\Uri\split($this->path); |
|
505 | + |
|
506 | + $info = \OC_FileChunking::decodeName($name); |
|
507 | + if (empty($info)) { |
|
508 | + throw new NotImplemented('Invalid chunk name'); |
|
509 | + } |
|
510 | + |
|
511 | + $chunk_handler = new \OC_FileChunking($info); |
|
512 | + $bytesWritten = $chunk_handler->store($info['index'], $data); |
|
513 | + |
|
514 | + //detect aborted upload |
|
515 | + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
|
516 | + if (isset($_SERVER['CONTENT_LENGTH'])) { |
|
517 | + $expected = (int)$_SERVER['CONTENT_LENGTH']; |
|
518 | + if ($bytesWritten !== $expected) { |
|
519 | + $chunk_handler->remove($info['index']); |
|
520 | + throw new BadRequest('Expected filesize of ' . $expected . ' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) ' . $bytesWritten . ' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
521 | + } |
|
522 | + } |
|
523 | + } |
|
524 | + |
|
525 | + if ($chunk_handler->isComplete()) { |
|
526 | + /** @var Storage $storage */ |
|
527 | + list($storage,) = $this->fileView->resolvePath($path); |
|
528 | + $needsPartFile = $storage->needsPartFile(); |
|
529 | + $partFile = null; |
|
530 | + |
|
531 | + $targetPath = $path . '/' . $info['name']; |
|
532 | + /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
533 | + list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); |
|
534 | + |
|
535 | + $exists = $this->fileView->file_exists($targetPath); |
|
536 | + |
|
537 | + try { |
|
538 | + $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); |
|
539 | + |
|
540 | + $this->emitPreHooks($exists, $targetPath); |
|
541 | + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); |
|
542 | + /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
543 | + list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); |
|
544 | + |
|
545 | + if ($needsPartFile) { |
|
546 | + // we first assembly the target file as a part file |
|
547 | + $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; |
|
548 | + /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
549 | + list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile); |
|
550 | + |
|
551 | + |
|
552 | + $chunk_handler->file_assemble($partStorage, $partInternalPath); |
|
553 | + |
|
554 | + // here is the final atomic rename |
|
555 | + $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); |
|
556 | + $fileExists = $targetStorage->file_exists($targetInternalPath); |
|
557 | + if ($renameOkay === false || $fileExists === false) { |
|
558 | + \OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']); |
|
559 | + // only delete if an error occurred and the target file was already created |
|
560 | + if ($fileExists) { |
|
561 | + // set to null to avoid double-deletion when handling exception |
|
562 | + // stray part file |
|
563 | + $partFile = null; |
|
564 | + $targetStorage->unlink($targetInternalPath); |
|
565 | + } |
|
566 | + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
|
567 | + throw new Exception('Could not rename part file assembled from chunks'); |
|
568 | + } |
|
569 | + } else { |
|
570 | + // assemble directly into the final file |
|
571 | + $chunk_handler->file_assemble($targetStorage, $targetInternalPath); |
|
572 | + } |
|
573 | + |
|
574 | + // allow sync clients to send the mtime along in a header |
|
575 | + if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
|
576 | + $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']); |
|
577 | + if ($targetStorage->touch($targetInternalPath, $mtime)) { |
|
578 | + $this->header('X-OC-MTime: accepted'); |
|
579 | + } |
|
580 | + } |
|
581 | + |
|
582 | + // since we skipped the view we need to scan and emit the hooks ourselves |
|
583 | + $targetStorage->getUpdater()->update($targetInternalPath); |
|
584 | + |
|
585 | + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
|
586 | + |
|
587 | + $this->emitPostHooks($exists, $targetPath); |
|
588 | + |
|
589 | + // FIXME: should call refreshInfo but can't because $this->path is not the of the final file |
|
590 | + $info = $this->fileView->getFileInfo($targetPath); |
|
591 | + |
|
592 | + if (isset($this->request->server['HTTP_OC_CHECKSUM'])) { |
|
593 | + $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']); |
|
594 | + $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]); |
|
595 | + } else if ($info->getChecksum() !== null && $info->getChecksum() !== '') { |
|
596 | + $this->fileView->putFileInfo($this->path, ['checksum' => '']); |
|
597 | + } |
|
598 | + |
|
599 | + $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); |
|
600 | + |
|
601 | + return $info->getEtag(); |
|
602 | + } catch (\Exception $e) { |
|
603 | + if ($partFile !== null) { |
|
604 | + $targetStorage->unlink($targetInternalPath); |
|
605 | + } |
|
606 | + $this->convertToSabreException($e); |
|
607 | + } |
|
608 | + } |
|
609 | + |
|
610 | + return null; |
|
611 | + } |
|
612 | + |
|
613 | + /** |
|
614 | + * Convert the given exception to a SabreException instance |
|
615 | + * |
|
616 | + * @param \Exception $e |
|
617 | + * |
|
618 | + * @throws \Sabre\DAV\Exception |
|
619 | + */ |
|
620 | + private function convertToSabreException(\Exception $e) { |
|
621 | + if ($e instanceof \Sabre\DAV\Exception) { |
|
622 | + throw $e; |
|
623 | + } |
|
624 | + if ($e instanceof NotPermittedException) { |
|
625 | + // a more general case - due to whatever reason the content could not be written |
|
626 | + throw new Forbidden($e->getMessage(), 0, $e); |
|
627 | + } |
|
628 | + if ($e instanceof ForbiddenException) { |
|
629 | + // the path for the file was forbidden |
|
630 | + throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); |
|
631 | + } |
|
632 | + if ($e instanceof EntityTooLargeException) { |
|
633 | + // the file is too big to be stored |
|
634 | + throw new EntityTooLarge($e->getMessage(), 0, $e); |
|
635 | + } |
|
636 | + if ($e instanceof InvalidContentException) { |
|
637 | + // the file content is not permitted |
|
638 | + throw new UnsupportedMediaType($e->getMessage(), 0, $e); |
|
639 | + } |
|
640 | + if ($e instanceof InvalidPathException) { |
|
641 | + // the path for the file was not valid |
|
642 | + // TODO: find proper http status code for this case |
|
643 | + throw new Forbidden($e->getMessage(), 0, $e); |
|
644 | + } |
|
645 | + if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { |
|
646 | + // the file is currently being written to by another process |
|
647 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
648 | + } |
|
649 | + if ($e instanceof GenericEncryptionException) { |
|
650 | + // returning 503 will allow retry of the operation at a later point in time |
|
651 | + throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e); |
|
652 | + } |
|
653 | + if ($e instanceof StorageNotAvailableException) { |
|
654 | + throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e); |
|
655 | + } |
|
656 | + if ($e instanceof NotFoundException) { |
|
657 | + throw new NotFound('File not found: ' . $e->getMessage(), 0, $e); |
|
658 | + } |
|
659 | + |
|
660 | + throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); |
|
661 | + } |
|
662 | + |
|
663 | + /** |
|
664 | + * Get the checksum for this file |
|
665 | + * |
|
666 | + * @return string |
|
667 | + */ |
|
668 | + public function getChecksum() { |
|
669 | + return $this->info->getChecksum(); |
|
670 | + } |
|
671 | + |
|
672 | + protected function header($string) { |
|
673 | + \header($string); |
|
674 | + } |
|
675 | 675 | } |
@@ -126,7 +126,7 @@ discard block |
||
126 | 126 | throw new Forbidden(); |
127 | 127 | } |
128 | 128 | } catch (StorageNotAvailableException $e) { |
129 | - throw new ServiceUnavailable("File is not updatable: " . $e->getMessage()); |
|
129 | + throw new ServiceUnavailable("File is not updatable: ".$e->getMessage()); |
|
130 | 130 | } |
131 | 131 | |
132 | 132 | // verify path of the target |
@@ -149,7 +149,7 @@ discard block |
||
149 | 149 | |
150 | 150 | if ($needsPartFile) { |
151 | 151 | // mark file as partial while uploading (ignored by the scanner) |
152 | - $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; |
|
152 | + $partFilePath = $this->getPartFileBasePath($this->path).'.ocTransferId'.rand().'.part'; |
|
153 | 153 | |
154 | 154 | if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) { |
155 | 155 | $needsPartFile = false; |
@@ -186,7 +186,7 @@ discard block |
||
186 | 186 | } |
187 | 187 | |
188 | 188 | $isEOF = false; |
189 | - $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) { |
|
189 | + $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function($stream) use (&$isEOF) { |
|
190 | 190 | $isEOF = feof($stream); |
191 | 191 | }); |
192 | 192 | |
@@ -217,7 +217,7 @@ discard block |
||
217 | 217 | $expected = $_SERVER['CONTENT_LENGTH']; |
218 | 218 | } |
219 | 219 | if ($expected !== "0") { |
220 | - throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )'); |
|
220 | + throw new Exception('Error while copying file to target location (copied bytes: '.$count.', expected filesize: '.$expected.' )'); |
|
221 | 221 | } |
222 | 222 | } |
223 | 223 | |
@@ -225,9 +225,9 @@ discard block |
||
225 | 225 | // double check if the file was fully received |
226 | 226 | // compare expected and actual size |
227 | 227 | if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
228 | - $expected = (int)$_SERVER['CONTENT_LENGTH']; |
|
228 | + $expected = (int) $_SERVER['CONTENT_LENGTH']; |
|
229 | 229 | if ($count !== $expected) { |
230 | - throw new BadRequest('Expected filesize of ' . $expected . ' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) ' . $count . ' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
230 | + throw new BadRequest('Expected filesize of '.$expected.' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) '.$count.' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
231 | 231 | } |
232 | 232 | } |
233 | 233 | |
@@ -277,7 +277,7 @@ discard block |
||
277 | 277 | $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); |
278 | 278 | $fileExists = $storage->file_exists($internalPath); |
279 | 279 | if ($renameOkay === false || $fileExists === false) { |
280 | - \OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); |
|
280 | + \OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: '.($renameOkay ? 'true' : 'false').', $fileExists: '.($fileExists ? 'true' : 'false').')', ['app' => 'webdav']); |
|
281 | 281 | throw new Exception('Could not rename part file to final file'); |
282 | 282 | } |
283 | 283 | } catch (ForbiddenException $ex) { |
@@ -334,10 +334,10 @@ discard block |
||
334 | 334 | } |
335 | 335 | |
336 | 336 | } catch (StorageNotAvailableException $e) { |
337 | - throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage(), 0, $e); |
|
337 | + throw new ServiceUnavailable("Failed to check file size: ".$e->getMessage(), 0, $e); |
|
338 | 338 | } |
339 | 339 | |
340 | - return '"' . $this->info->getEtag() . '"'; |
|
340 | + return '"'.$this->info->getEtag().'"'; |
|
341 | 341 | } |
342 | 342 | |
343 | 343 | private function getPartFileBasePath($path) { |
@@ -424,9 +424,9 @@ discard block |
||
424 | 424 | return $res; |
425 | 425 | } catch (GenericEncryptionException $e) { |
426 | 426 | // returning 503 will allow retry of the operation at a later point in time |
427 | - throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); |
|
427 | + throw new ServiceUnavailable("Encryption not ready: ".$e->getMessage()); |
|
428 | 428 | } catch (StorageNotAvailableException $e) { |
429 | - throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); |
|
429 | + throw new ServiceUnavailable("Failed to open file: ".$e->getMessage()); |
|
430 | 430 | } catch (ForbiddenException $ex) { |
431 | 431 | throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
432 | 432 | } catch (LockedException $e) { |
@@ -451,7 +451,7 @@ discard block |
||
451 | 451 | throw new Forbidden(); |
452 | 452 | } |
453 | 453 | } catch (StorageNotAvailableException $e) { |
454 | - throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); |
|
454 | + throw new ServiceUnavailable("Failed to unlink: ".$e->getMessage()); |
|
455 | 455 | } catch (ForbiddenException $ex) { |
456 | 456 | throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
457 | 457 | } catch (LockedException $e) { |
@@ -514,10 +514,10 @@ discard block |
||
514 | 514 | //detect aborted upload |
515 | 515 | if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
516 | 516 | if (isset($_SERVER['CONTENT_LENGTH'])) { |
517 | - $expected = (int)$_SERVER['CONTENT_LENGTH']; |
|
517 | + $expected = (int) $_SERVER['CONTENT_LENGTH']; |
|
518 | 518 | if ($bytesWritten !== $expected) { |
519 | 519 | $chunk_handler->remove($info['index']); |
520 | - throw new BadRequest('Expected filesize of ' . $expected . ' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) ' . $bytesWritten . ' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
520 | + throw new BadRequest('Expected filesize of '.$expected.' bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) '.$bytesWritten.' bytes. Could either be a network problem on the sending side or a problem writing to the storage on the server side.'); |
|
521 | 521 | } |
522 | 522 | } |
523 | 523 | } |
@@ -528,7 +528,7 @@ discard block |
||
528 | 528 | $needsPartFile = $storage->needsPartFile(); |
529 | 529 | $partFile = null; |
530 | 530 | |
531 | - $targetPath = $path . '/' . $info['name']; |
|
531 | + $targetPath = $path.'/'.$info['name']; |
|
532 | 532 | /** @var \OC\Files\Storage\Storage $targetStorage */ |
533 | 533 | list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); |
534 | 534 | |
@@ -544,7 +544,7 @@ discard block |
||
544 | 544 | |
545 | 545 | if ($needsPartFile) { |
546 | 546 | // we first assembly the target file as a part file |
547 | - $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; |
|
547 | + $partFile = $this->getPartFileBasePath($path.'/'.$info['name']).'.ocTransferId'.$info['transferid'].'.part'; |
|
548 | 548 | /** @var \OC\Files\Storage\Storage $targetStorage */ |
549 | 549 | list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile); |
550 | 550 | |
@@ -648,13 +648,13 @@ discard block |
||
648 | 648 | } |
649 | 649 | if ($e instanceof GenericEncryptionException) { |
650 | 650 | // returning 503 will allow retry of the operation at a later point in time |
651 | - throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e); |
|
651 | + throw new ServiceUnavailable('Encryption not ready: '.$e->getMessage(), 0, $e); |
|
652 | 652 | } |
653 | 653 | if ($e instanceof StorageNotAvailableException) { |
654 | - throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e); |
|
654 | + throw new ServiceUnavailable('Failed to write file contents: '.$e->getMessage(), 0, $e); |
|
655 | 655 | } |
656 | 656 | if ($e instanceof NotFoundException) { |
657 | - throw new NotFound('File not found: ' . $e->getMessage(), 0, $e); |
|
657 | + throw new NotFound('File not found: '.$e->getMessage(), 0, $e); |
|
658 | 658 | } |
659 | 659 | |
660 | 660 | throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); |
@@ -56,1122 +56,1122 @@ |
||
56 | 56 | |
57 | 57 | class CardDavBackend implements BackendInterface, SyncSupport { |
58 | 58 | |
59 | - const PERSONAL_ADDRESSBOOK_URI = 'contacts'; |
|
60 | - const PERSONAL_ADDRESSBOOK_NAME = 'Contacts'; |
|
61 | - |
|
62 | - /** @var Principal */ |
|
63 | - private $principalBackend; |
|
64 | - |
|
65 | - /** @var string */ |
|
66 | - private $dbCardsTable = 'cards'; |
|
67 | - |
|
68 | - /** @var string */ |
|
69 | - private $dbCardsPropertiesTable = 'cards_properties'; |
|
70 | - |
|
71 | - /** @var IDBConnection */ |
|
72 | - private $db; |
|
73 | - |
|
74 | - /** @var Backend */ |
|
75 | - private $sharingBackend; |
|
76 | - |
|
77 | - /** @var array properties to index */ |
|
78 | - public static $indexProperties = [ |
|
79 | - 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME', |
|
80 | - 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD']; |
|
81 | - |
|
82 | - /** |
|
83 | - * @var string[] Map of uid => display name |
|
84 | - */ |
|
85 | - protected $userDisplayNames; |
|
86 | - |
|
87 | - /** @var IUserManager */ |
|
88 | - private $userManager; |
|
89 | - |
|
90 | - /** @var EventDispatcherInterface */ |
|
91 | - private $dispatcher; |
|
92 | - |
|
93 | - /** |
|
94 | - * CardDavBackend constructor. |
|
95 | - * |
|
96 | - * @param IDBConnection $db |
|
97 | - * @param Principal $principalBackend |
|
98 | - * @param IUserManager $userManager |
|
99 | - * @param IGroupManager $groupManager |
|
100 | - * @param EventDispatcherInterface $dispatcher |
|
101 | - */ |
|
102 | - public function __construct(IDBConnection $db, |
|
103 | - Principal $principalBackend, |
|
104 | - IUserManager $userManager, |
|
105 | - IGroupManager $groupManager, |
|
106 | - EventDispatcherInterface $dispatcher) { |
|
107 | - $this->db = $db; |
|
108 | - $this->principalBackend = $principalBackend; |
|
109 | - $this->userManager = $userManager; |
|
110 | - $this->dispatcher = $dispatcher; |
|
111 | - $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook'); |
|
112 | - } |
|
113 | - |
|
114 | - /** |
|
115 | - * Return the number of address books for a principal |
|
116 | - * |
|
117 | - * @param $principalUri |
|
118 | - * @return int |
|
119 | - */ |
|
120 | - public function getAddressBooksForUserCount($principalUri) { |
|
121 | - $principalUri = $this->convertPrincipal($principalUri, true); |
|
122 | - $query = $this->db->getQueryBuilder(); |
|
123 | - $query->select($query->func()->count('*')) |
|
124 | - ->from('addressbooks') |
|
125 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
126 | - |
|
127 | - return (int)$query->execute()->fetchColumn(); |
|
128 | - } |
|
129 | - |
|
130 | - /** |
|
131 | - * Returns the list of address books for a specific user. |
|
132 | - * |
|
133 | - * Every addressbook should have the following properties: |
|
134 | - * id - an arbitrary unique id |
|
135 | - * uri - the 'basename' part of the url |
|
136 | - * principaluri - Same as the passed parameter |
|
137 | - * |
|
138 | - * Any additional clark-notation property may be passed besides this. Some |
|
139 | - * common ones are : |
|
140 | - * {DAV:}displayname |
|
141 | - * {urn:ietf:params:xml:ns:carddav}addressbook-description |
|
142 | - * {http://calendarserver.org/ns/}getctag |
|
143 | - * |
|
144 | - * @param string $principalUri |
|
145 | - * @return array |
|
146 | - */ |
|
147 | - function getAddressBooksForUser($principalUri) { |
|
148 | - $principalUriOriginal = $principalUri; |
|
149 | - $principalUri = $this->convertPrincipal($principalUri, true); |
|
150 | - $query = $this->db->getQueryBuilder(); |
|
151 | - $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
152 | - ->from('addressbooks') |
|
153 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
154 | - |
|
155 | - $addressBooks = []; |
|
156 | - |
|
157 | - $result = $query->execute(); |
|
158 | - while($row = $result->fetch()) { |
|
159 | - $addressBooks[$row['id']] = [ |
|
160 | - 'id' => $row['id'], |
|
161 | - 'uri' => $row['uri'], |
|
162 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], false), |
|
163 | - '{DAV:}displayname' => $row['displayname'], |
|
164 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
165 | - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
166 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
167 | - ]; |
|
168 | - |
|
169 | - $this->addOwnerPrincipal($addressBooks[$row['id']]); |
|
170 | - } |
|
171 | - $result->closeCursor(); |
|
172 | - |
|
173 | - // query for shared addressbooks |
|
174 | - $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); |
|
175 | - $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); |
|
176 | - |
|
177 | - $principals = array_map(function ($principal) { |
|
178 | - return urldecode($principal); |
|
179 | - }, $principals); |
|
180 | - $principals[]= $principalUri; |
|
181 | - |
|
182 | - $query = $this->db->getQueryBuilder(); |
|
183 | - $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) |
|
184 | - ->from('dav_shares', 's') |
|
185 | - ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
186 | - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) |
|
187 | - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) |
|
188 | - ->setParameter('type', 'addressbook') |
|
189 | - ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) |
|
190 | - ->execute(); |
|
191 | - |
|
192 | - $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; |
|
193 | - while($row = $result->fetch()) { |
|
194 | - if ($row['principaluri'] === $principalUri) { |
|
195 | - continue; |
|
196 | - } |
|
197 | - |
|
198 | - $readOnly = (int) $row['access'] === Backend::ACCESS_READ; |
|
199 | - if (isset($addressBooks[$row['id']])) { |
|
200 | - if ($readOnly) { |
|
201 | - // New share can not have more permissions then the old one. |
|
202 | - continue; |
|
203 | - } |
|
204 | - if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) && |
|
205 | - $addressBooks[$row['id']][$readOnlyPropertyName] === 0) { |
|
206 | - // Old share is already read-write, no more permissions can be gained |
|
207 | - continue; |
|
208 | - } |
|
209 | - } |
|
210 | - |
|
211 | - list(, $name) = \Sabre\Uri\split($row['principaluri']); |
|
212 | - $uri = $row['uri'] . '_shared_by_' . $name; |
|
213 | - $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')'; |
|
214 | - |
|
215 | - $addressBooks[$row['id']] = [ |
|
216 | - 'id' => $row['id'], |
|
217 | - 'uri' => $uri, |
|
218 | - 'principaluri' => $principalUriOriginal, |
|
219 | - '{DAV:}displayname' => $displayName, |
|
220 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
221 | - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
222 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
223 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], |
|
224 | - $readOnlyPropertyName => $readOnly, |
|
225 | - ]; |
|
226 | - |
|
227 | - $this->addOwnerPrincipal($addressBooks[$row['id']]); |
|
228 | - } |
|
229 | - $result->closeCursor(); |
|
230 | - |
|
231 | - return array_values($addressBooks); |
|
232 | - } |
|
233 | - |
|
234 | - public function getUsersOwnAddressBooks($principalUri) { |
|
235 | - $principalUri = $this->convertPrincipal($principalUri, true); |
|
236 | - $query = $this->db->getQueryBuilder(); |
|
237 | - $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
238 | - ->from('addressbooks') |
|
239 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
240 | - |
|
241 | - $addressBooks = []; |
|
242 | - |
|
243 | - $result = $query->execute(); |
|
244 | - while($row = $result->fetch()) { |
|
245 | - $addressBooks[$row['id']] = [ |
|
246 | - 'id' => $row['id'], |
|
247 | - 'uri' => $row['uri'], |
|
248 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], false), |
|
249 | - '{DAV:}displayname' => $row['displayname'], |
|
250 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
251 | - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
252 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
253 | - ]; |
|
254 | - |
|
255 | - $this->addOwnerPrincipal($addressBooks[$row['id']]); |
|
256 | - } |
|
257 | - $result->closeCursor(); |
|
258 | - |
|
259 | - return array_values($addressBooks); |
|
260 | - } |
|
261 | - |
|
262 | - private function getUserDisplayName($uid) { |
|
263 | - if (!isset($this->userDisplayNames[$uid])) { |
|
264 | - $user = $this->userManager->get($uid); |
|
265 | - |
|
266 | - if ($user instanceof IUser) { |
|
267 | - $this->userDisplayNames[$uid] = $user->getDisplayName(); |
|
268 | - } else { |
|
269 | - $this->userDisplayNames[$uid] = $uid; |
|
270 | - } |
|
271 | - } |
|
272 | - |
|
273 | - return $this->userDisplayNames[$uid]; |
|
274 | - } |
|
275 | - |
|
276 | - /** |
|
277 | - * @param int $addressBookId |
|
278 | - */ |
|
279 | - public function getAddressBookById($addressBookId) { |
|
280 | - $query = $this->db->getQueryBuilder(); |
|
281 | - $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
282 | - ->from('addressbooks') |
|
283 | - ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) |
|
284 | - ->execute(); |
|
285 | - |
|
286 | - $row = $result->fetch(); |
|
287 | - $result->closeCursor(); |
|
288 | - if ($row === false) { |
|
289 | - return null; |
|
290 | - } |
|
291 | - |
|
292 | - $addressBook = [ |
|
293 | - 'id' => $row['id'], |
|
294 | - 'uri' => $row['uri'], |
|
295 | - 'principaluri' => $row['principaluri'], |
|
296 | - '{DAV:}displayname' => $row['displayname'], |
|
297 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
298 | - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
299 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
300 | - ]; |
|
301 | - |
|
302 | - $this->addOwnerPrincipal($addressBook); |
|
303 | - |
|
304 | - return $addressBook; |
|
305 | - } |
|
306 | - |
|
307 | - /** |
|
308 | - * @param $addressBookUri |
|
309 | - * @return array|null |
|
310 | - */ |
|
311 | - public function getAddressBooksByUri($principal, $addressBookUri) { |
|
312 | - $query = $this->db->getQueryBuilder(); |
|
313 | - $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
314 | - ->from('addressbooks') |
|
315 | - ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri))) |
|
316 | - ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) |
|
317 | - ->setMaxResults(1) |
|
318 | - ->execute(); |
|
319 | - |
|
320 | - $row = $result->fetch(); |
|
321 | - $result->closeCursor(); |
|
322 | - if ($row === false) { |
|
323 | - return null; |
|
324 | - } |
|
325 | - |
|
326 | - $addressBook = [ |
|
327 | - 'id' => $row['id'], |
|
328 | - 'uri' => $row['uri'], |
|
329 | - 'principaluri' => $row['principaluri'], |
|
330 | - '{DAV:}displayname' => $row['displayname'], |
|
331 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
332 | - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
333 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
334 | - ]; |
|
335 | - |
|
336 | - $this->addOwnerPrincipal($addressBook); |
|
337 | - |
|
338 | - return $addressBook; |
|
339 | - } |
|
340 | - |
|
341 | - /** |
|
342 | - * Updates properties for an address book. |
|
343 | - * |
|
344 | - * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
345 | - * To do the actual updates, you must tell this object which properties |
|
346 | - * you're going to process with the handle() method. |
|
347 | - * |
|
348 | - * Calling the handle method is like telling the PropPatch object "I |
|
349 | - * promise I can handle updating this property". |
|
350 | - * |
|
351 | - * Read the PropPatch documentation for more info and examples. |
|
352 | - * |
|
353 | - * @param string $addressBookId |
|
354 | - * @param \Sabre\DAV\PropPatch $propPatch |
|
355 | - * @return void |
|
356 | - */ |
|
357 | - function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { |
|
358 | - $supportedProperties = [ |
|
359 | - '{DAV:}displayname', |
|
360 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description', |
|
361 | - ]; |
|
362 | - |
|
363 | - /** |
|
364 | - * @suppress SqlInjectionChecker |
|
365 | - */ |
|
366 | - $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) { |
|
367 | - |
|
368 | - $updates = []; |
|
369 | - foreach($mutations as $property=>$newValue) { |
|
370 | - |
|
371 | - switch($property) { |
|
372 | - case '{DAV:}displayname': |
|
373 | - $updates['displayname'] = $newValue; |
|
374 | - break; |
|
375 | - case '{' . Plugin::NS_CARDDAV . '}addressbook-description': |
|
376 | - $updates['description'] = $newValue; |
|
377 | - break; |
|
378 | - } |
|
379 | - } |
|
380 | - $query = $this->db->getQueryBuilder(); |
|
381 | - $query->update('addressbooks'); |
|
382 | - |
|
383 | - foreach($updates as $key=>$value) { |
|
384 | - $query->set($key, $query->createNamedParameter($value)); |
|
385 | - } |
|
386 | - $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) |
|
387 | - ->execute(); |
|
388 | - |
|
389 | - $this->addChange($addressBookId, "", 2); |
|
390 | - |
|
391 | - return true; |
|
392 | - |
|
393 | - }); |
|
394 | - } |
|
395 | - |
|
396 | - /** |
|
397 | - * Creates a new address book |
|
398 | - * |
|
399 | - * @param string $principalUri |
|
400 | - * @param string $url Just the 'basename' of the url. |
|
401 | - * @param array $properties |
|
402 | - * @return int |
|
403 | - * @throws BadRequest |
|
404 | - */ |
|
405 | - function createAddressBook($principalUri, $url, array $properties) { |
|
406 | - $values = [ |
|
407 | - 'displayname' => null, |
|
408 | - 'description' => null, |
|
409 | - 'principaluri' => $principalUri, |
|
410 | - 'uri' => $url, |
|
411 | - 'synctoken' => 1 |
|
412 | - ]; |
|
413 | - |
|
414 | - foreach($properties as $property=>$newValue) { |
|
415 | - |
|
416 | - switch($property) { |
|
417 | - case '{DAV:}displayname': |
|
418 | - $values['displayname'] = $newValue; |
|
419 | - break; |
|
420 | - case '{' . Plugin::NS_CARDDAV . '}addressbook-description': |
|
421 | - $values['description'] = $newValue; |
|
422 | - break; |
|
423 | - default: |
|
424 | - throw new BadRequest('Unknown property: ' . $property); |
|
425 | - } |
|
426 | - |
|
427 | - } |
|
428 | - |
|
429 | - // Fallback to make sure the displayname is set. Some clients may refuse |
|
430 | - // to work with addressbooks not having a displayname. |
|
431 | - if(is_null($values['displayname'])) { |
|
432 | - $values['displayname'] = $url; |
|
433 | - } |
|
434 | - |
|
435 | - $query = $this->db->getQueryBuilder(); |
|
436 | - $query->insert('addressbooks') |
|
437 | - ->values([ |
|
438 | - 'uri' => $query->createParameter('uri'), |
|
439 | - 'displayname' => $query->createParameter('displayname'), |
|
440 | - 'description' => $query->createParameter('description'), |
|
441 | - 'principaluri' => $query->createParameter('principaluri'), |
|
442 | - 'synctoken' => $query->createParameter('synctoken'), |
|
443 | - ]) |
|
444 | - ->setParameters($values) |
|
445 | - ->execute(); |
|
446 | - |
|
447 | - return $query->getLastInsertId(); |
|
448 | - } |
|
449 | - |
|
450 | - /** |
|
451 | - * Deletes an entire addressbook and all its contents |
|
452 | - * |
|
453 | - * @param mixed $addressBookId |
|
454 | - * @return void |
|
455 | - */ |
|
456 | - function deleteAddressBook($addressBookId) { |
|
457 | - $query = $this->db->getQueryBuilder(); |
|
458 | - $query->delete('cards') |
|
459 | - ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) |
|
460 | - ->setParameter('addressbookid', $addressBookId) |
|
461 | - ->execute(); |
|
462 | - |
|
463 | - $query->delete('addressbookchanges') |
|
464 | - ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) |
|
465 | - ->setParameter('addressbookid', $addressBookId) |
|
466 | - ->execute(); |
|
467 | - |
|
468 | - $query->delete('addressbooks') |
|
469 | - ->where($query->expr()->eq('id', $query->createParameter('id'))) |
|
470 | - ->setParameter('id', $addressBookId) |
|
471 | - ->execute(); |
|
472 | - |
|
473 | - $this->sharingBackend->deleteAllShares($addressBookId); |
|
474 | - |
|
475 | - $query->delete($this->dbCardsPropertiesTable) |
|
476 | - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
477 | - ->execute(); |
|
478 | - |
|
479 | - } |
|
480 | - |
|
481 | - /** |
|
482 | - * Returns all cards for a specific addressbook id. |
|
483 | - * |
|
484 | - * This method should return the following properties for each card: |
|
485 | - * * carddata - raw vcard data |
|
486 | - * * uri - Some unique url |
|
487 | - * * lastmodified - A unix timestamp |
|
488 | - * |
|
489 | - * It's recommended to also return the following properties: |
|
490 | - * * etag - A unique etag. This must change every time the card changes. |
|
491 | - * * size - The size of the card in bytes. |
|
492 | - * |
|
493 | - * If these last two properties are provided, less time will be spent |
|
494 | - * calculating them. If they are specified, you can also ommit carddata. |
|
495 | - * This may speed up certain requests, especially with large cards. |
|
496 | - * |
|
497 | - * @param mixed $addressBookId |
|
498 | - * @return array |
|
499 | - */ |
|
500 | - function getCards($addressBookId) { |
|
501 | - $query = $this->db->getQueryBuilder(); |
|
502 | - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) |
|
503 | - ->from('cards') |
|
504 | - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
505 | - |
|
506 | - $cards = []; |
|
507 | - |
|
508 | - $result = $query->execute(); |
|
509 | - while($row = $result->fetch()) { |
|
510 | - $row['etag'] = '"' . $row['etag'] . '"'; |
|
511 | - $row['carddata'] = $this->readBlob($row['carddata']); |
|
512 | - $cards[] = $row; |
|
513 | - } |
|
514 | - $result->closeCursor(); |
|
515 | - |
|
516 | - return $cards; |
|
517 | - } |
|
518 | - |
|
519 | - /** |
|
520 | - * Returns a specific card. |
|
521 | - * |
|
522 | - * The same set of properties must be returned as with getCards. The only |
|
523 | - * exception is that 'carddata' is absolutely required. |
|
524 | - * |
|
525 | - * If the card does not exist, you must return false. |
|
526 | - * |
|
527 | - * @param mixed $addressBookId |
|
528 | - * @param string $cardUri |
|
529 | - * @return array |
|
530 | - */ |
|
531 | - function getCard($addressBookId, $cardUri) { |
|
532 | - $query = $this->db->getQueryBuilder(); |
|
533 | - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) |
|
534 | - ->from('cards') |
|
535 | - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
536 | - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) |
|
537 | - ->setMaxResults(1); |
|
538 | - |
|
539 | - $result = $query->execute(); |
|
540 | - $row = $result->fetch(); |
|
541 | - if (!$row) { |
|
542 | - return false; |
|
543 | - } |
|
544 | - $row['etag'] = '"' . $row['etag'] . '"'; |
|
545 | - $row['carddata'] = $this->readBlob($row['carddata']); |
|
546 | - |
|
547 | - return $row; |
|
548 | - } |
|
549 | - |
|
550 | - /** |
|
551 | - * Returns a list of cards. |
|
552 | - * |
|
553 | - * This method should work identical to getCard, but instead return all the |
|
554 | - * cards in the list as an array. |
|
555 | - * |
|
556 | - * If the backend supports this, it may allow for some speed-ups. |
|
557 | - * |
|
558 | - * @param mixed $addressBookId |
|
559 | - * @param string[] $uris |
|
560 | - * @return array |
|
561 | - */ |
|
562 | - function getMultipleCards($addressBookId, array $uris) { |
|
563 | - if (empty($uris)) { |
|
564 | - return []; |
|
565 | - } |
|
566 | - |
|
567 | - $chunks = array_chunk($uris, 100); |
|
568 | - $cards = []; |
|
569 | - |
|
570 | - $query = $this->db->getQueryBuilder(); |
|
571 | - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) |
|
572 | - ->from('cards') |
|
573 | - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
574 | - ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))); |
|
575 | - |
|
576 | - foreach ($chunks as $uris) { |
|
577 | - $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); |
|
578 | - $result = $query->execute(); |
|
579 | - |
|
580 | - while ($row = $result->fetch()) { |
|
581 | - $row['etag'] = '"' . $row['etag'] . '"'; |
|
582 | - $row['carddata'] = $this->readBlob($row['carddata']); |
|
583 | - $cards[] = $row; |
|
584 | - } |
|
585 | - $result->closeCursor(); |
|
586 | - } |
|
587 | - return $cards; |
|
588 | - } |
|
589 | - |
|
590 | - /** |
|
591 | - * Creates a new card. |
|
592 | - * |
|
593 | - * The addressbook id will be passed as the first argument. This is the |
|
594 | - * same id as it is returned from the getAddressBooksForUser method. |
|
595 | - * |
|
596 | - * The cardUri is a base uri, and doesn't include the full path. The |
|
597 | - * cardData argument is the vcard body, and is passed as a string. |
|
598 | - * |
|
599 | - * It is possible to return an ETag from this method. This ETag is for the |
|
600 | - * newly created resource, and must be enclosed with double quotes (that |
|
601 | - * is, the string itself must contain the double quotes). |
|
602 | - * |
|
603 | - * You should only return the ETag if you store the carddata as-is. If a |
|
604 | - * subsequent GET request on the same card does not have the same body, |
|
605 | - * byte-by-byte and you did return an ETag here, clients tend to get |
|
606 | - * confused. |
|
607 | - * |
|
608 | - * If you don't return an ETag, you can just return null. |
|
609 | - * |
|
610 | - * @param mixed $addressBookId |
|
611 | - * @param string $cardUri |
|
612 | - * @param string $cardData |
|
613 | - * @return string |
|
614 | - */ |
|
615 | - function createCard($addressBookId, $cardUri, $cardData) { |
|
616 | - $etag = md5($cardData); |
|
617 | - $uid = $this->getUID($cardData); |
|
618 | - |
|
619 | - $q = $this->db->getQueryBuilder(); |
|
620 | - $q->select('uid') |
|
621 | - ->from('cards') |
|
622 | - ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId))) |
|
623 | - ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid))) |
|
624 | - ->setMaxResults(1); |
|
625 | - $result = $q->execute(); |
|
626 | - $count = (bool) $result->fetchColumn(); |
|
627 | - $result->closeCursor(); |
|
628 | - if ($count) { |
|
629 | - throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.'); |
|
630 | - } |
|
631 | - |
|
632 | - $query = $this->db->getQueryBuilder(); |
|
633 | - $query->insert('cards') |
|
634 | - ->values([ |
|
635 | - 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB), |
|
636 | - 'uri' => $query->createNamedParameter($cardUri), |
|
637 | - 'lastmodified' => $query->createNamedParameter(time()), |
|
638 | - 'addressbookid' => $query->createNamedParameter($addressBookId), |
|
639 | - 'size' => $query->createNamedParameter(strlen($cardData)), |
|
640 | - 'etag' => $query->createNamedParameter($etag), |
|
641 | - 'uid' => $query->createNamedParameter($uid), |
|
642 | - ]) |
|
643 | - ->execute(); |
|
644 | - |
|
645 | - $this->addChange($addressBookId, $cardUri, 1); |
|
646 | - $this->updateProperties($addressBookId, $cardUri, $cardData); |
|
647 | - |
|
648 | - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', |
|
649 | - new GenericEvent(null, [ |
|
650 | - 'addressBookId' => $addressBookId, |
|
651 | - 'cardUri' => $cardUri, |
|
652 | - 'cardData' => $cardData])); |
|
653 | - |
|
654 | - return '"' . $etag . '"'; |
|
655 | - } |
|
656 | - |
|
657 | - /** |
|
658 | - * Updates a card. |
|
659 | - * |
|
660 | - * The addressbook id will be passed as the first argument. This is the |
|
661 | - * same id as it is returned from the getAddressBooksForUser method. |
|
662 | - * |
|
663 | - * The cardUri is a base uri, and doesn't include the full path. The |
|
664 | - * cardData argument is the vcard body, and is passed as a string. |
|
665 | - * |
|
666 | - * It is possible to return an ETag from this method. This ETag should |
|
667 | - * match that of the updated resource, and must be enclosed with double |
|
668 | - * quotes (that is: the string itself must contain the actual quotes). |
|
669 | - * |
|
670 | - * You should only return the ETag if you store the carddata as-is. If a |
|
671 | - * subsequent GET request on the same card does not have the same body, |
|
672 | - * byte-by-byte and you did return an ETag here, clients tend to get |
|
673 | - * confused. |
|
674 | - * |
|
675 | - * If you don't return an ETag, you can just return null. |
|
676 | - * |
|
677 | - * @param mixed $addressBookId |
|
678 | - * @param string $cardUri |
|
679 | - * @param string $cardData |
|
680 | - * @return string |
|
681 | - */ |
|
682 | - function updateCard($addressBookId, $cardUri, $cardData) { |
|
683 | - |
|
684 | - $uid = $this->getUID($cardData); |
|
685 | - $etag = md5($cardData); |
|
686 | - $query = $this->db->getQueryBuilder(); |
|
687 | - $query->update('cards') |
|
688 | - ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB)) |
|
689 | - ->set('lastmodified', $query->createNamedParameter(time())) |
|
690 | - ->set('size', $query->createNamedParameter(strlen($cardData))) |
|
691 | - ->set('etag', $query->createNamedParameter($etag)) |
|
692 | - ->set('uid', $query->createNamedParameter($uid)) |
|
693 | - ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) |
|
694 | - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
695 | - ->execute(); |
|
696 | - |
|
697 | - $this->addChange($addressBookId, $cardUri, 2); |
|
698 | - $this->updateProperties($addressBookId, $cardUri, $cardData); |
|
699 | - |
|
700 | - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', |
|
701 | - new GenericEvent(null, [ |
|
702 | - 'addressBookId' => $addressBookId, |
|
703 | - 'cardUri' => $cardUri, |
|
704 | - 'cardData' => $cardData])); |
|
705 | - |
|
706 | - return '"' . $etag . '"'; |
|
707 | - } |
|
708 | - |
|
709 | - /** |
|
710 | - * Deletes a card |
|
711 | - * |
|
712 | - * @param mixed $addressBookId |
|
713 | - * @param string $cardUri |
|
714 | - * @return bool |
|
715 | - */ |
|
716 | - function deleteCard($addressBookId, $cardUri) { |
|
717 | - try { |
|
718 | - $cardId = $this->getCardId($addressBookId, $cardUri); |
|
719 | - } catch (\InvalidArgumentException $e) { |
|
720 | - $cardId = null; |
|
721 | - } |
|
722 | - $query = $this->db->getQueryBuilder(); |
|
723 | - $ret = $query->delete('cards') |
|
724 | - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
725 | - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) |
|
726 | - ->execute(); |
|
727 | - |
|
728 | - $this->addChange($addressBookId, $cardUri, 3); |
|
729 | - |
|
730 | - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', |
|
731 | - new GenericEvent(null, [ |
|
732 | - 'addressBookId' => $addressBookId, |
|
733 | - 'cardUri' => $cardUri])); |
|
734 | - |
|
735 | - if ($ret === 1) { |
|
736 | - if ($cardId !== null) { |
|
737 | - $this->purgeProperties($addressBookId, $cardId); |
|
738 | - } |
|
739 | - return true; |
|
740 | - } |
|
741 | - |
|
742 | - return false; |
|
743 | - } |
|
744 | - |
|
745 | - /** |
|
746 | - * The getChanges method returns all the changes that have happened, since |
|
747 | - * the specified syncToken in the specified address book. |
|
748 | - * |
|
749 | - * This function should return an array, such as the following: |
|
750 | - * |
|
751 | - * [ |
|
752 | - * 'syncToken' => 'The current synctoken', |
|
753 | - * 'added' => [ |
|
754 | - * 'new.txt', |
|
755 | - * ], |
|
756 | - * 'modified' => [ |
|
757 | - * 'modified.txt', |
|
758 | - * ], |
|
759 | - * 'deleted' => [ |
|
760 | - * 'foo.php.bak', |
|
761 | - * 'old.txt' |
|
762 | - * ] |
|
763 | - * ]; |
|
764 | - * |
|
765 | - * The returned syncToken property should reflect the *current* syncToken |
|
766 | - * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
767 | - * property. This is needed here too, to ensure the operation is atomic. |
|
768 | - * |
|
769 | - * If the $syncToken argument is specified as null, this is an initial |
|
770 | - * sync, and all members should be reported. |
|
771 | - * |
|
772 | - * The modified property is an array of nodenames that have changed since |
|
773 | - * the last token. |
|
774 | - * |
|
775 | - * The deleted property is an array with nodenames, that have been deleted |
|
776 | - * from collection. |
|
777 | - * |
|
778 | - * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
779 | - * 1, you only have to report changes that happened only directly in |
|
780 | - * immediate descendants. If it's 2, it should also include changes from |
|
781 | - * the nodes below the child collections. (grandchildren) |
|
782 | - * |
|
783 | - * The $limit argument allows a client to specify how many results should |
|
784 | - * be returned at most. If the limit is not specified, it should be treated |
|
785 | - * as infinite. |
|
786 | - * |
|
787 | - * If the limit (infinite or not) is higher than you're willing to return, |
|
788 | - * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
789 | - * |
|
790 | - * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
791 | - * return null. |
|
792 | - * |
|
793 | - * The limit is 'suggestive'. You are free to ignore it. |
|
794 | - * |
|
795 | - * @param string $addressBookId |
|
796 | - * @param string $syncToken |
|
797 | - * @param int $syncLevel |
|
798 | - * @param int $limit |
|
799 | - * @return array |
|
800 | - */ |
|
801 | - function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { |
|
802 | - // Current synctoken |
|
803 | - $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?'); |
|
804 | - $stmt->execute([ $addressBookId ]); |
|
805 | - $currentToken = $stmt->fetchColumn(0); |
|
806 | - |
|
807 | - if (is_null($currentToken)) return null; |
|
808 | - |
|
809 | - $result = [ |
|
810 | - 'syncToken' => $currentToken, |
|
811 | - 'added' => [], |
|
812 | - 'modified' => [], |
|
813 | - 'deleted' => [], |
|
814 | - ]; |
|
815 | - |
|
816 | - if ($syncToken) { |
|
817 | - |
|
818 | - $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`"; |
|
819 | - if ($limit>0) { |
|
820 | - $query .= " LIMIT " . (int)$limit; |
|
821 | - } |
|
822 | - |
|
823 | - // Fetching all changes |
|
824 | - $stmt = $this->db->prepare($query); |
|
825 | - $stmt->execute([$syncToken, $currentToken, $addressBookId]); |
|
826 | - |
|
827 | - $changes = []; |
|
828 | - |
|
829 | - // This loop ensures that any duplicates are overwritten, only the |
|
830 | - // last change on a node is relevant. |
|
831 | - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
832 | - |
|
833 | - $changes[$row['uri']] = $row['operation']; |
|
834 | - |
|
835 | - } |
|
836 | - |
|
837 | - foreach($changes as $uri => $operation) { |
|
838 | - |
|
839 | - switch($operation) { |
|
840 | - case 1: |
|
841 | - $result['added'][] = $uri; |
|
842 | - break; |
|
843 | - case 2: |
|
844 | - $result['modified'][] = $uri; |
|
845 | - break; |
|
846 | - case 3: |
|
847 | - $result['deleted'][] = $uri; |
|
848 | - break; |
|
849 | - } |
|
850 | - |
|
851 | - } |
|
852 | - } else { |
|
853 | - // No synctoken supplied, this is the initial sync. |
|
854 | - $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?"; |
|
855 | - $stmt = $this->db->prepare($query); |
|
856 | - $stmt->execute([$addressBookId]); |
|
857 | - |
|
858 | - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); |
|
859 | - } |
|
860 | - return $result; |
|
861 | - } |
|
862 | - |
|
863 | - /** |
|
864 | - * Adds a change record to the addressbookchanges table. |
|
865 | - * |
|
866 | - * @param mixed $addressBookId |
|
867 | - * @param string $objectUri |
|
868 | - * @param int $operation 1 = add, 2 = modify, 3 = delete |
|
869 | - * @return void |
|
870 | - */ |
|
871 | - protected function addChange($addressBookId, $objectUri, $operation) { |
|
872 | - $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?'; |
|
873 | - $stmt = $this->db->prepare($sql); |
|
874 | - $stmt->execute([ |
|
875 | - $objectUri, |
|
876 | - $addressBookId, |
|
877 | - $operation, |
|
878 | - $addressBookId |
|
879 | - ]); |
|
880 | - $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); |
|
881 | - $stmt->execute([ |
|
882 | - $addressBookId |
|
883 | - ]); |
|
884 | - } |
|
885 | - |
|
886 | - private function readBlob($cardData) { |
|
887 | - if (is_resource($cardData)) { |
|
888 | - return stream_get_contents($cardData); |
|
889 | - } |
|
890 | - |
|
891 | - return $cardData; |
|
892 | - } |
|
893 | - |
|
894 | - /** |
|
895 | - * @param IShareable $shareable |
|
896 | - * @param string[] $add |
|
897 | - * @param string[] $remove |
|
898 | - */ |
|
899 | - public function updateShares(IShareable $shareable, $add, $remove) { |
|
900 | - $this->sharingBackend->updateShares($shareable, $add, $remove); |
|
901 | - } |
|
902 | - |
|
903 | - /** |
|
904 | - * search contact |
|
905 | - * |
|
906 | - * @param int $addressBookId |
|
907 | - * @param string $pattern which should match within the $searchProperties |
|
908 | - * @param array $searchProperties defines the properties within the query pattern should match |
|
909 | - * @param array $options = array() to define the search behavior |
|
910 | - * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are |
|
911 | - * @return array an array of contacts which are arrays of key-value-pairs |
|
912 | - */ |
|
913 | - public function search($addressBookId, $pattern, $searchProperties, $options = []) { |
|
914 | - $query = $this->db->getQueryBuilder(); |
|
915 | - $query2 = $this->db->getQueryBuilder(); |
|
916 | - |
|
917 | - $query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp'); |
|
918 | - $query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId))); |
|
919 | - $or = $query2->expr()->orX(); |
|
920 | - foreach ($searchProperties as $property) { |
|
921 | - $or->add($query2->expr()->eq('cp.name', $query->createNamedParameter($property))); |
|
922 | - } |
|
923 | - $query2->andWhere($or); |
|
924 | - |
|
925 | - // No need for like when the pattern is empty |
|
926 | - if ('' !== $pattern) { |
|
927 | - if(\array_key_exists('escape_like_param', $options) && $options['escape_like_param'] === false) { |
|
928 | - $query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter($pattern))); |
|
929 | - } else { |
|
930 | - $query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); |
|
931 | - } |
|
932 | - } |
|
933 | - |
|
934 | - $query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c') |
|
935 | - ->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL()))); |
|
936 | - |
|
937 | - $result = $query->execute(); |
|
938 | - $cards = $result->fetchAll(); |
|
939 | - |
|
940 | - $result->closeCursor(); |
|
941 | - |
|
942 | - return array_map(function ($array) { |
|
943 | - $array['carddata'] = $this->readBlob($array['carddata']); |
|
944 | - return $array; |
|
945 | - }, $cards); |
|
946 | - } |
|
947 | - |
|
948 | - /** |
|
949 | - * @param int $bookId |
|
950 | - * @param string $name |
|
951 | - * @return array |
|
952 | - */ |
|
953 | - public function collectCardProperties($bookId, $name) { |
|
954 | - $query = $this->db->getQueryBuilder(); |
|
955 | - $result = $query->selectDistinct('value') |
|
956 | - ->from($this->dbCardsPropertiesTable) |
|
957 | - ->where($query->expr()->eq('name', $query->createNamedParameter($name))) |
|
958 | - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId))) |
|
959 | - ->execute(); |
|
960 | - |
|
961 | - $all = $result->fetchAll(PDO::FETCH_COLUMN); |
|
962 | - $result->closeCursor(); |
|
963 | - |
|
964 | - return $all; |
|
965 | - } |
|
966 | - |
|
967 | - /** |
|
968 | - * get URI from a given contact |
|
969 | - * |
|
970 | - * @param int $id |
|
971 | - * @return string |
|
972 | - */ |
|
973 | - public function getCardUri($id) { |
|
974 | - $query = $this->db->getQueryBuilder(); |
|
975 | - $query->select('uri')->from($this->dbCardsTable) |
|
976 | - ->where($query->expr()->eq('id', $query->createParameter('id'))) |
|
977 | - ->setParameter('id', $id); |
|
978 | - |
|
979 | - $result = $query->execute(); |
|
980 | - $uri = $result->fetch(); |
|
981 | - $result->closeCursor(); |
|
982 | - |
|
983 | - if (!isset($uri['uri'])) { |
|
984 | - throw new \InvalidArgumentException('Card does not exists: ' . $id); |
|
985 | - } |
|
986 | - |
|
987 | - return $uri['uri']; |
|
988 | - } |
|
989 | - |
|
990 | - /** |
|
991 | - * return contact with the given URI |
|
992 | - * |
|
993 | - * @param int $addressBookId |
|
994 | - * @param string $uri |
|
995 | - * @returns array |
|
996 | - */ |
|
997 | - public function getContact($addressBookId, $uri) { |
|
998 | - $result = []; |
|
999 | - $query = $this->db->getQueryBuilder(); |
|
1000 | - $query->select('*')->from($this->dbCardsTable) |
|
1001 | - ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
1002 | - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
1003 | - $queryResult = $query->execute(); |
|
1004 | - $contact = $queryResult->fetch(); |
|
1005 | - $queryResult->closeCursor(); |
|
1006 | - |
|
1007 | - if (is_array($contact)) { |
|
1008 | - $result = $contact; |
|
1009 | - } |
|
1010 | - |
|
1011 | - return $result; |
|
1012 | - } |
|
1013 | - |
|
1014 | - /** |
|
1015 | - * Returns the list of people whom this address book is shared with. |
|
1016 | - * |
|
1017 | - * Every element in this array should have the following properties: |
|
1018 | - * * href - Often a mailto: address |
|
1019 | - * * commonName - Optional, for example a first + last name |
|
1020 | - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. |
|
1021 | - * * readOnly - boolean |
|
1022 | - * * summary - Optional, a description for the share |
|
1023 | - * |
|
1024 | - * @return array |
|
1025 | - */ |
|
1026 | - public function getShares($addressBookId) { |
|
1027 | - return $this->sharingBackend->getShares($addressBookId); |
|
1028 | - } |
|
1029 | - |
|
1030 | - /** |
|
1031 | - * update properties table |
|
1032 | - * |
|
1033 | - * @param int $addressBookId |
|
1034 | - * @param string $cardUri |
|
1035 | - * @param string $vCardSerialized |
|
1036 | - */ |
|
1037 | - protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) { |
|
1038 | - $cardId = $this->getCardId($addressBookId, $cardUri); |
|
1039 | - $vCard = $this->readCard($vCardSerialized); |
|
1040 | - |
|
1041 | - $this->purgeProperties($addressBookId, $cardId); |
|
1042 | - |
|
1043 | - $query = $this->db->getQueryBuilder(); |
|
1044 | - $query->insert($this->dbCardsPropertiesTable) |
|
1045 | - ->values( |
|
1046 | - [ |
|
1047 | - 'addressbookid' => $query->createNamedParameter($addressBookId), |
|
1048 | - 'cardid' => $query->createNamedParameter($cardId), |
|
1049 | - 'name' => $query->createParameter('name'), |
|
1050 | - 'value' => $query->createParameter('value'), |
|
1051 | - 'preferred' => $query->createParameter('preferred') |
|
1052 | - ] |
|
1053 | - ); |
|
1054 | - |
|
1055 | - foreach ($vCard->children() as $property) { |
|
1056 | - if(!in_array($property->name, self::$indexProperties)) { |
|
1057 | - continue; |
|
1058 | - } |
|
1059 | - $preferred = 0; |
|
1060 | - foreach($property->parameters as $parameter) { |
|
1061 | - if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') { |
|
1062 | - $preferred = 1; |
|
1063 | - break; |
|
1064 | - } |
|
1065 | - } |
|
1066 | - $query->setParameter('name', $property->name); |
|
1067 | - $query->setParameter('value', mb_substr($property->getValue(), 0, 254)); |
|
1068 | - $query->setParameter('preferred', $preferred); |
|
1069 | - $query->execute(); |
|
1070 | - } |
|
1071 | - } |
|
1072 | - |
|
1073 | - /** |
|
1074 | - * read vCard data into a vCard object |
|
1075 | - * |
|
1076 | - * @param string $cardData |
|
1077 | - * @return VCard |
|
1078 | - */ |
|
1079 | - protected function readCard($cardData) { |
|
1080 | - return Reader::read($cardData); |
|
1081 | - } |
|
1082 | - |
|
1083 | - /** |
|
1084 | - * delete all properties from a given card |
|
1085 | - * |
|
1086 | - * @param int $addressBookId |
|
1087 | - * @param int $cardId |
|
1088 | - */ |
|
1089 | - protected function purgeProperties($addressBookId, $cardId) { |
|
1090 | - $query = $this->db->getQueryBuilder(); |
|
1091 | - $query->delete($this->dbCardsPropertiesTable) |
|
1092 | - ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId))) |
|
1093 | - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
1094 | - $query->execute(); |
|
1095 | - } |
|
1096 | - |
|
1097 | - /** |
|
1098 | - * get ID from a given contact |
|
1099 | - * |
|
1100 | - * @param int $addressBookId |
|
1101 | - * @param string $uri |
|
1102 | - * @return int |
|
1103 | - */ |
|
1104 | - protected function getCardId($addressBookId, $uri) { |
|
1105 | - $query = $this->db->getQueryBuilder(); |
|
1106 | - $query->select('id')->from($this->dbCardsTable) |
|
1107 | - ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
1108 | - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
1109 | - |
|
1110 | - $result = $query->execute(); |
|
1111 | - $cardIds = $result->fetch(); |
|
1112 | - $result->closeCursor(); |
|
1113 | - |
|
1114 | - if (!isset($cardIds['id'])) { |
|
1115 | - throw new \InvalidArgumentException('Card does not exists: ' . $uri); |
|
1116 | - } |
|
1117 | - |
|
1118 | - return (int)$cardIds['id']; |
|
1119 | - } |
|
1120 | - |
|
1121 | - /** |
|
1122 | - * For shared address books the sharee is set in the ACL of the address book |
|
1123 | - * @param $addressBookId |
|
1124 | - * @param $acl |
|
1125 | - * @return array |
|
1126 | - */ |
|
1127 | - public function applyShareAcl($addressBookId, $acl) { |
|
1128 | - return $this->sharingBackend->applyShareAcl($addressBookId, $acl); |
|
1129 | - } |
|
1130 | - |
|
1131 | - private function convertPrincipal($principalUri, $toV2) { |
|
1132 | - if ($this->principalBackend->getPrincipalPrefix() === 'principals') { |
|
1133 | - list(, $name) = \Sabre\Uri\split($principalUri); |
|
1134 | - if ($toV2 === true) { |
|
1135 | - return "principals/users/$name"; |
|
1136 | - } |
|
1137 | - return "principals/$name"; |
|
1138 | - } |
|
1139 | - return $principalUri; |
|
1140 | - } |
|
1141 | - |
|
1142 | - private function addOwnerPrincipal(&$addressbookInfo) { |
|
1143 | - $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'; |
|
1144 | - $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'; |
|
1145 | - if (isset($addressbookInfo[$ownerPrincipalKey])) { |
|
1146 | - $uri = $addressbookInfo[$ownerPrincipalKey]; |
|
1147 | - } else { |
|
1148 | - $uri = $addressbookInfo['principaluri']; |
|
1149 | - } |
|
1150 | - |
|
1151 | - $principalInformation = $this->principalBackend->getPrincipalByPath($uri); |
|
1152 | - if (isset($principalInformation['{DAV:}displayname'])) { |
|
1153 | - $addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname']; |
|
1154 | - } |
|
1155 | - } |
|
1156 | - |
|
1157 | - /** |
|
1158 | - * Extract UID from vcard |
|
1159 | - * |
|
1160 | - * @param string $cardData the vcard raw data |
|
1161 | - * @return string the uid |
|
1162 | - * @throws BadRequest if no UID is available |
|
1163 | - */ |
|
1164 | - private function getUID($cardData) { |
|
1165 | - if ($cardData != '') { |
|
1166 | - $vCard = Reader::read($cardData); |
|
1167 | - if ($vCard->UID) { |
|
1168 | - $uid = $vCard->UID->getValue(); |
|
1169 | - return $uid; |
|
1170 | - } |
|
1171 | - // should already be handled, but just in case |
|
1172 | - throw new BadRequest('vCards on CardDAV servers MUST have a UID property'); |
|
1173 | - } |
|
1174 | - // should already be handled, but just in case |
|
1175 | - throw new BadRequest('vCard can not be empty'); |
|
1176 | - } |
|
59 | + const PERSONAL_ADDRESSBOOK_URI = 'contacts'; |
|
60 | + const PERSONAL_ADDRESSBOOK_NAME = 'Contacts'; |
|
61 | + |
|
62 | + /** @var Principal */ |
|
63 | + private $principalBackend; |
|
64 | + |
|
65 | + /** @var string */ |
|
66 | + private $dbCardsTable = 'cards'; |
|
67 | + |
|
68 | + /** @var string */ |
|
69 | + private $dbCardsPropertiesTable = 'cards_properties'; |
|
70 | + |
|
71 | + /** @var IDBConnection */ |
|
72 | + private $db; |
|
73 | + |
|
74 | + /** @var Backend */ |
|
75 | + private $sharingBackend; |
|
76 | + |
|
77 | + /** @var array properties to index */ |
|
78 | + public static $indexProperties = [ |
|
79 | + 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME', |
|
80 | + 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD']; |
|
81 | + |
|
82 | + /** |
|
83 | + * @var string[] Map of uid => display name |
|
84 | + */ |
|
85 | + protected $userDisplayNames; |
|
86 | + |
|
87 | + /** @var IUserManager */ |
|
88 | + private $userManager; |
|
89 | + |
|
90 | + /** @var EventDispatcherInterface */ |
|
91 | + private $dispatcher; |
|
92 | + |
|
93 | + /** |
|
94 | + * CardDavBackend constructor. |
|
95 | + * |
|
96 | + * @param IDBConnection $db |
|
97 | + * @param Principal $principalBackend |
|
98 | + * @param IUserManager $userManager |
|
99 | + * @param IGroupManager $groupManager |
|
100 | + * @param EventDispatcherInterface $dispatcher |
|
101 | + */ |
|
102 | + public function __construct(IDBConnection $db, |
|
103 | + Principal $principalBackend, |
|
104 | + IUserManager $userManager, |
|
105 | + IGroupManager $groupManager, |
|
106 | + EventDispatcherInterface $dispatcher) { |
|
107 | + $this->db = $db; |
|
108 | + $this->principalBackend = $principalBackend; |
|
109 | + $this->userManager = $userManager; |
|
110 | + $this->dispatcher = $dispatcher; |
|
111 | + $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook'); |
|
112 | + } |
|
113 | + |
|
114 | + /** |
|
115 | + * Return the number of address books for a principal |
|
116 | + * |
|
117 | + * @param $principalUri |
|
118 | + * @return int |
|
119 | + */ |
|
120 | + public function getAddressBooksForUserCount($principalUri) { |
|
121 | + $principalUri = $this->convertPrincipal($principalUri, true); |
|
122 | + $query = $this->db->getQueryBuilder(); |
|
123 | + $query->select($query->func()->count('*')) |
|
124 | + ->from('addressbooks') |
|
125 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
126 | + |
|
127 | + return (int)$query->execute()->fetchColumn(); |
|
128 | + } |
|
129 | + |
|
130 | + /** |
|
131 | + * Returns the list of address books for a specific user. |
|
132 | + * |
|
133 | + * Every addressbook should have the following properties: |
|
134 | + * id - an arbitrary unique id |
|
135 | + * uri - the 'basename' part of the url |
|
136 | + * principaluri - Same as the passed parameter |
|
137 | + * |
|
138 | + * Any additional clark-notation property may be passed besides this. Some |
|
139 | + * common ones are : |
|
140 | + * {DAV:}displayname |
|
141 | + * {urn:ietf:params:xml:ns:carddav}addressbook-description |
|
142 | + * {http://calendarserver.org/ns/}getctag |
|
143 | + * |
|
144 | + * @param string $principalUri |
|
145 | + * @return array |
|
146 | + */ |
|
147 | + function getAddressBooksForUser($principalUri) { |
|
148 | + $principalUriOriginal = $principalUri; |
|
149 | + $principalUri = $this->convertPrincipal($principalUri, true); |
|
150 | + $query = $this->db->getQueryBuilder(); |
|
151 | + $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
152 | + ->from('addressbooks') |
|
153 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
154 | + |
|
155 | + $addressBooks = []; |
|
156 | + |
|
157 | + $result = $query->execute(); |
|
158 | + while($row = $result->fetch()) { |
|
159 | + $addressBooks[$row['id']] = [ |
|
160 | + 'id' => $row['id'], |
|
161 | + 'uri' => $row['uri'], |
|
162 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], false), |
|
163 | + '{DAV:}displayname' => $row['displayname'], |
|
164 | + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
165 | + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
166 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
167 | + ]; |
|
168 | + |
|
169 | + $this->addOwnerPrincipal($addressBooks[$row['id']]); |
|
170 | + } |
|
171 | + $result->closeCursor(); |
|
172 | + |
|
173 | + // query for shared addressbooks |
|
174 | + $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); |
|
175 | + $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); |
|
176 | + |
|
177 | + $principals = array_map(function ($principal) { |
|
178 | + return urldecode($principal); |
|
179 | + }, $principals); |
|
180 | + $principals[]= $principalUri; |
|
181 | + |
|
182 | + $query = $this->db->getQueryBuilder(); |
|
183 | + $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) |
|
184 | + ->from('dav_shares', 's') |
|
185 | + ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
186 | + ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) |
|
187 | + ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) |
|
188 | + ->setParameter('type', 'addressbook') |
|
189 | + ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) |
|
190 | + ->execute(); |
|
191 | + |
|
192 | + $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; |
|
193 | + while($row = $result->fetch()) { |
|
194 | + if ($row['principaluri'] === $principalUri) { |
|
195 | + continue; |
|
196 | + } |
|
197 | + |
|
198 | + $readOnly = (int) $row['access'] === Backend::ACCESS_READ; |
|
199 | + if (isset($addressBooks[$row['id']])) { |
|
200 | + if ($readOnly) { |
|
201 | + // New share can not have more permissions then the old one. |
|
202 | + continue; |
|
203 | + } |
|
204 | + if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) && |
|
205 | + $addressBooks[$row['id']][$readOnlyPropertyName] === 0) { |
|
206 | + // Old share is already read-write, no more permissions can be gained |
|
207 | + continue; |
|
208 | + } |
|
209 | + } |
|
210 | + |
|
211 | + list(, $name) = \Sabre\Uri\split($row['principaluri']); |
|
212 | + $uri = $row['uri'] . '_shared_by_' . $name; |
|
213 | + $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')'; |
|
214 | + |
|
215 | + $addressBooks[$row['id']] = [ |
|
216 | + 'id' => $row['id'], |
|
217 | + 'uri' => $uri, |
|
218 | + 'principaluri' => $principalUriOriginal, |
|
219 | + '{DAV:}displayname' => $displayName, |
|
220 | + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
221 | + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
222 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
223 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], |
|
224 | + $readOnlyPropertyName => $readOnly, |
|
225 | + ]; |
|
226 | + |
|
227 | + $this->addOwnerPrincipal($addressBooks[$row['id']]); |
|
228 | + } |
|
229 | + $result->closeCursor(); |
|
230 | + |
|
231 | + return array_values($addressBooks); |
|
232 | + } |
|
233 | + |
|
234 | + public function getUsersOwnAddressBooks($principalUri) { |
|
235 | + $principalUri = $this->convertPrincipal($principalUri, true); |
|
236 | + $query = $this->db->getQueryBuilder(); |
|
237 | + $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
238 | + ->from('addressbooks') |
|
239 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
240 | + |
|
241 | + $addressBooks = []; |
|
242 | + |
|
243 | + $result = $query->execute(); |
|
244 | + while($row = $result->fetch()) { |
|
245 | + $addressBooks[$row['id']] = [ |
|
246 | + 'id' => $row['id'], |
|
247 | + 'uri' => $row['uri'], |
|
248 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], false), |
|
249 | + '{DAV:}displayname' => $row['displayname'], |
|
250 | + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
251 | + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
252 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
253 | + ]; |
|
254 | + |
|
255 | + $this->addOwnerPrincipal($addressBooks[$row['id']]); |
|
256 | + } |
|
257 | + $result->closeCursor(); |
|
258 | + |
|
259 | + return array_values($addressBooks); |
|
260 | + } |
|
261 | + |
|
262 | + private function getUserDisplayName($uid) { |
|
263 | + if (!isset($this->userDisplayNames[$uid])) { |
|
264 | + $user = $this->userManager->get($uid); |
|
265 | + |
|
266 | + if ($user instanceof IUser) { |
|
267 | + $this->userDisplayNames[$uid] = $user->getDisplayName(); |
|
268 | + } else { |
|
269 | + $this->userDisplayNames[$uid] = $uid; |
|
270 | + } |
|
271 | + } |
|
272 | + |
|
273 | + return $this->userDisplayNames[$uid]; |
|
274 | + } |
|
275 | + |
|
276 | + /** |
|
277 | + * @param int $addressBookId |
|
278 | + */ |
|
279 | + public function getAddressBookById($addressBookId) { |
|
280 | + $query = $this->db->getQueryBuilder(); |
|
281 | + $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
282 | + ->from('addressbooks') |
|
283 | + ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) |
|
284 | + ->execute(); |
|
285 | + |
|
286 | + $row = $result->fetch(); |
|
287 | + $result->closeCursor(); |
|
288 | + if ($row === false) { |
|
289 | + return null; |
|
290 | + } |
|
291 | + |
|
292 | + $addressBook = [ |
|
293 | + 'id' => $row['id'], |
|
294 | + 'uri' => $row['uri'], |
|
295 | + 'principaluri' => $row['principaluri'], |
|
296 | + '{DAV:}displayname' => $row['displayname'], |
|
297 | + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
298 | + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
299 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
300 | + ]; |
|
301 | + |
|
302 | + $this->addOwnerPrincipal($addressBook); |
|
303 | + |
|
304 | + return $addressBook; |
|
305 | + } |
|
306 | + |
|
307 | + /** |
|
308 | + * @param $addressBookUri |
|
309 | + * @return array|null |
|
310 | + */ |
|
311 | + public function getAddressBooksByUri($principal, $addressBookUri) { |
|
312 | + $query = $this->db->getQueryBuilder(); |
|
313 | + $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) |
|
314 | + ->from('addressbooks') |
|
315 | + ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri))) |
|
316 | + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) |
|
317 | + ->setMaxResults(1) |
|
318 | + ->execute(); |
|
319 | + |
|
320 | + $row = $result->fetch(); |
|
321 | + $result->closeCursor(); |
|
322 | + if ($row === false) { |
|
323 | + return null; |
|
324 | + } |
|
325 | + |
|
326 | + $addressBook = [ |
|
327 | + 'id' => $row['id'], |
|
328 | + 'uri' => $row['uri'], |
|
329 | + 'principaluri' => $row['principaluri'], |
|
330 | + '{DAV:}displayname' => $row['displayname'], |
|
331 | + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
332 | + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
|
333 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
334 | + ]; |
|
335 | + |
|
336 | + $this->addOwnerPrincipal($addressBook); |
|
337 | + |
|
338 | + return $addressBook; |
|
339 | + } |
|
340 | + |
|
341 | + /** |
|
342 | + * Updates properties for an address book. |
|
343 | + * |
|
344 | + * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
345 | + * To do the actual updates, you must tell this object which properties |
|
346 | + * you're going to process with the handle() method. |
|
347 | + * |
|
348 | + * Calling the handle method is like telling the PropPatch object "I |
|
349 | + * promise I can handle updating this property". |
|
350 | + * |
|
351 | + * Read the PropPatch documentation for more info and examples. |
|
352 | + * |
|
353 | + * @param string $addressBookId |
|
354 | + * @param \Sabre\DAV\PropPatch $propPatch |
|
355 | + * @return void |
|
356 | + */ |
|
357 | + function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { |
|
358 | + $supportedProperties = [ |
|
359 | + '{DAV:}displayname', |
|
360 | + '{' . Plugin::NS_CARDDAV . '}addressbook-description', |
|
361 | + ]; |
|
362 | + |
|
363 | + /** |
|
364 | + * @suppress SqlInjectionChecker |
|
365 | + */ |
|
366 | + $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) { |
|
367 | + |
|
368 | + $updates = []; |
|
369 | + foreach($mutations as $property=>$newValue) { |
|
370 | + |
|
371 | + switch($property) { |
|
372 | + case '{DAV:}displayname': |
|
373 | + $updates['displayname'] = $newValue; |
|
374 | + break; |
|
375 | + case '{' . Plugin::NS_CARDDAV . '}addressbook-description': |
|
376 | + $updates['description'] = $newValue; |
|
377 | + break; |
|
378 | + } |
|
379 | + } |
|
380 | + $query = $this->db->getQueryBuilder(); |
|
381 | + $query->update('addressbooks'); |
|
382 | + |
|
383 | + foreach($updates as $key=>$value) { |
|
384 | + $query->set($key, $query->createNamedParameter($value)); |
|
385 | + } |
|
386 | + $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) |
|
387 | + ->execute(); |
|
388 | + |
|
389 | + $this->addChange($addressBookId, "", 2); |
|
390 | + |
|
391 | + return true; |
|
392 | + |
|
393 | + }); |
|
394 | + } |
|
395 | + |
|
396 | + /** |
|
397 | + * Creates a new address book |
|
398 | + * |
|
399 | + * @param string $principalUri |
|
400 | + * @param string $url Just the 'basename' of the url. |
|
401 | + * @param array $properties |
|
402 | + * @return int |
|
403 | + * @throws BadRequest |
|
404 | + */ |
|
405 | + function createAddressBook($principalUri, $url, array $properties) { |
|
406 | + $values = [ |
|
407 | + 'displayname' => null, |
|
408 | + 'description' => null, |
|
409 | + 'principaluri' => $principalUri, |
|
410 | + 'uri' => $url, |
|
411 | + 'synctoken' => 1 |
|
412 | + ]; |
|
413 | + |
|
414 | + foreach($properties as $property=>$newValue) { |
|
415 | + |
|
416 | + switch($property) { |
|
417 | + case '{DAV:}displayname': |
|
418 | + $values['displayname'] = $newValue; |
|
419 | + break; |
|
420 | + case '{' . Plugin::NS_CARDDAV . '}addressbook-description': |
|
421 | + $values['description'] = $newValue; |
|
422 | + break; |
|
423 | + default: |
|
424 | + throw new BadRequest('Unknown property: ' . $property); |
|
425 | + } |
|
426 | + |
|
427 | + } |
|
428 | + |
|
429 | + // Fallback to make sure the displayname is set. Some clients may refuse |
|
430 | + // to work with addressbooks not having a displayname. |
|
431 | + if(is_null($values['displayname'])) { |
|
432 | + $values['displayname'] = $url; |
|
433 | + } |
|
434 | + |
|
435 | + $query = $this->db->getQueryBuilder(); |
|
436 | + $query->insert('addressbooks') |
|
437 | + ->values([ |
|
438 | + 'uri' => $query->createParameter('uri'), |
|
439 | + 'displayname' => $query->createParameter('displayname'), |
|
440 | + 'description' => $query->createParameter('description'), |
|
441 | + 'principaluri' => $query->createParameter('principaluri'), |
|
442 | + 'synctoken' => $query->createParameter('synctoken'), |
|
443 | + ]) |
|
444 | + ->setParameters($values) |
|
445 | + ->execute(); |
|
446 | + |
|
447 | + return $query->getLastInsertId(); |
|
448 | + } |
|
449 | + |
|
450 | + /** |
|
451 | + * Deletes an entire addressbook and all its contents |
|
452 | + * |
|
453 | + * @param mixed $addressBookId |
|
454 | + * @return void |
|
455 | + */ |
|
456 | + function deleteAddressBook($addressBookId) { |
|
457 | + $query = $this->db->getQueryBuilder(); |
|
458 | + $query->delete('cards') |
|
459 | + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) |
|
460 | + ->setParameter('addressbookid', $addressBookId) |
|
461 | + ->execute(); |
|
462 | + |
|
463 | + $query->delete('addressbookchanges') |
|
464 | + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) |
|
465 | + ->setParameter('addressbookid', $addressBookId) |
|
466 | + ->execute(); |
|
467 | + |
|
468 | + $query->delete('addressbooks') |
|
469 | + ->where($query->expr()->eq('id', $query->createParameter('id'))) |
|
470 | + ->setParameter('id', $addressBookId) |
|
471 | + ->execute(); |
|
472 | + |
|
473 | + $this->sharingBackend->deleteAllShares($addressBookId); |
|
474 | + |
|
475 | + $query->delete($this->dbCardsPropertiesTable) |
|
476 | + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
477 | + ->execute(); |
|
478 | + |
|
479 | + } |
|
480 | + |
|
481 | + /** |
|
482 | + * Returns all cards for a specific addressbook id. |
|
483 | + * |
|
484 | + * This method should return the following properties for each card: |
|
485 | + * * carddata - raw vcard data |
|
486 | + * * uri - Some unique url |
|
487 | + * * lastmodified - A unix timestamp |
|
488 | + * |
|
489 | + * It's recommended to also return the following properties: |
|
490 | + * * etag - A unique etag. This must change every time the card changes. |
|
491 | + * * size - The size of the card in bytes. |
|
492 | + * |
|
493 | + * If these last two properties are provided, less time will be spent |
|
494 | + * calculating them. If they are specified, you can also ommit carddata. |
|
495 | + * This may speed up certain requests, especially with large cards. |
|
496 | + * |
|
497 | + * @param mixed $addressBookId |
|
498 | + * @return array |
|
499 | + */ |
|
500 | + function getCards($addressBookId) { |
|
501 | + $query = $this->db->getQueryBuilder(); |
|
502 | + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) |
|
503 | + ->from('cards') |
|
504 | + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
505 | + |
|
506 | + $cards = []; |
|
507 | + |
|
508 | + $result = $query->execute(); |
|
509 | + while($row = $result->fetch()) { |
|
510 | + $row['etag'] = '"' . $row['etag'] . '"'; |
|
511 | + $row['carddata'] = $this->readBlob($row['carddata']); |
|
512 | + $cards[] = $row; |
|
513 | + } |
|
514 | + $result->closeCursor(); |
|
515 | + |
|
516 | + return $cards; |
|
517 | + } |
|
518 | + |
|
519 | + /** |
|
520 | + * Returns a specific card. |
|
521 | + * |
|
522 | + * The same set of properties must be returned as with getCards. The only |
|
523 | + * exception is that 'carddata' is absolutely required. |
|
524 | + * |
|
525 | + * If the card does not exist, you must return false. |
|
526 | + * |
|
527 | + * @param mixed $addressBookId |
|
528 | + * @param string $cardUri |
|
529 | + * @return array |
|
530 | + */ |
|
531 | + function getCard($addressBookId, $cardUri) { |
|
532 | + $query = $this->db->getQueryBuilder(); |
|
533 | + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) |
|
534 | + ->from('cards') |
|
535 | + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
536 | + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) |
|
537 | + ->setMaxResults(1); |
|
538 | + |
|
539 | + $result = $query->execute(); |
|
540 | + $row = $result->fetch(); |
|
541 | + if (!$row) { |
|
542 | + return false; |
|
543 | + } |
|
544 | + $row['etag'] = '"' . $row['etag'] . '"'; |
|
545 | + $row['carddata'] = $this->readBlob($row['carddata']); |
|
546 | + |
|
547 | + return $row; |
|
548 | + } |
|
549 | + |
|
550 | + /** |
|
551 | + * Returns a list of cards. |
|
552 | + * |
|
553 | + * This method should work identical to getCard, but instead return all the |
|
554 | + * cards in the list as an array. |
|
555 | + * |
|
556 | + * If the backend supports this, it may allow for some speed-ups. |
|
557 | + * |
|
558 | + * @param mixed $addressBookId |
|
559 | + * @param string[] $uris |
|
560 | + * @return array |
|
561 | + */ |
|
562 | + function getMultipleCards($addressBookId, array $uris) { |
|
563 | + if (empty($uris)) { |
|
564 | + return []; |
|
565 | + } |
|
566 | + |
|
567 | + $chunks = array_chunk($uris, 100); |
|
568 | + $cards = []; |
|
569 | + |
|
570 | + $query = $this->db->getQueryBuilder(); |
|
571 | + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) |
|
572 | + ->from('cards') |
|
573 | + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
574 | + ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))); |
|
575 | + |
|
576 | + foreach ($chunks as $uris) { |
|
577 | + $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); |
|
578 | + $result = $query->execute(); |
|
579 | + |
|
580 | + while ($row = $result->fetch()) { |
|
581 | + $row['etag'] = '"' . $row['etag'] . '"'; |
|
582 | + $row['carddata'] = $this->readBlob($row['carddata']); |
|
583 | + $cards[] = $row; |
|
584 | + } |
|
585 | + $result->closeCursor(); |
|
586 | + } |
|
587 | + return $cards; |
|
588 | + } |
|
589 | + |
|
590 | + /** |
|
591 | + * Creates a new card. |
|
592 | + * |
|
593 | + * The addressbook id will be passed as the first argument. This is the |
|
594 | + * same id as it is returned from the getAddressBooksForUser method. |
|
595 | + * |
|
596 | + * The cardUri is a base uri, and doesn't include the full path. The |
|
597 | + * cardData argument is the vcard body, and is passed as a string. |
|
598 | + * |
|
599 | + * It is possible to return an ETag from this method. This ETag is for the |
|
600 | + * newly created resource, and must be enclosed with double quotes (that |
|
601 | + * is, the string itself must contain the double quotes). |
|
602 | + * |
|
603 | + * You should only return the ETag if you store the carddata as-is. If a |
|
604 | + * subsequent GET request on the same card does not have the same body, |
|
605 | + * byte-by-byte and you did return an ETag here, clients tend to get |
|
606 | + * confused. |
|
607 | + * |
|
608 | + * If you don't return an ETag, you can just return null. |
|
609 | + * |
|
610 | + * @param mixed $addressBookId |
|
611 | + * @param string $cardUri |
|
612 | + * @param string $cardData |
|
613 | + * @return string |
|
614 | + */ |
|
615 | + function createCard($addressBookId, $cardUri, $cardData) { |
|
616 | + $etag = md5($cardData); |
|
617 | + $uid = $this->getUID($cardData); |
|
618 | + |
|
619 | + $q = $this->db->getQueryBuilder(); |
|
620 | + $q->select('uid') |
|
621 | + ->from('cards') |
|
622 | + ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId))) |
|
623 | + ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid))) |
|
624 | + ->setMaxResults(1); |
|
625 | + $result = $q->execute(); |
|
626 | + $count = (bool) $result->fetchColumn(); |
|
627 | + $result->closeCursor(); |
|
628 | + if ($count) { |
|
629 | + throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.'); |
|
630 | + } |
|
631 | + |
|
632 | + $query = $this->db->getQueryBuilder(); |
|
633 | + $query->insert('cards') |
|
634 | + ->values([ |
|
635 | + 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB), |
|
636 | + 'uri' => $query->createNamedParameter($cardUri), |
|
637 | + 'lastmodified' => $query->createNamedParameter(time()), |
|
638 | + 'addressbookid' => $query->createNamedParameter($addressBookId), |
|
639 | + 'size' => $query->createNamedParameter(strlen($cardData)), |
|
640 | + 'etag' => $query->createNamedParameter($etag), |
|
641 | + 'uid' => $query->createNamedParameter($uid), |
|
642 | + ]) |
|
643 | + ->execute(); |
|
644 | + |
|
645 | + $this->addChange($addressBookId, $cardUri, 1); |
|
646 | + $this->updateProperties($addressBookId, $cardUri, $cardData); |
|
647 | + |
|
648 | + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', |
|
649 | + new GenericEvent(null, [ |
|
650 | + 'addressBookId' => $addressBookId, |
|
651 | + 'cardUri' => $cardUri, |
|
652 | + 'cardData' => $cardData])); |
|
653 | + |
|
654 | + return '"' . $etag . '"'; |
|
655 | + } |
|
656 | + |
|
657 | + /** |
|
658 | + * Updates a card. |
|
659 | + * |
|
660 | + * The addressbook id will be passed as the first argument. This is the |
|
661 | + * same id as it is returned from the getAddressBooksForUser method. |
|
662 | + * |
|
663 | + * The cardUri is a base uri, and doesn't include the full path. The |
|
664 | + * cardData argument is the vcard body, and is passed as a string. |
|
665 | + * |
|
666 | + * It is possible to return an ETag from this method. This ETag should |
|
667 | + * match that of the updated resource, and must be enclosed with double |
|
668 | + * quotes (that is: the string itself must contain the actual quotes). |
|
669 | + * |
|
670 | + * You should only return the ETag if you store the carddata as-is. If a |
|
671 | + * subsequent GET request on the same card does not have the same body, |
|
672 | + * byte-by-byte and you did return an ETag here, clients tend to get |
|
673 | + * confused. |
|
674 | + * |
|
675 | + * If you don't return an ETag, you can just return null. |
|
676 | + * |
|
677 | + * @param mixed $addressBookId |
|
678 | + * @param string $cardUri |
|
679 | + * @param string $cardData |
|
680 | + * @return string |
|
681 | + */ |
|
682 | + function updateCard($addressBookId, $cardUri, $cardData) { |
|
683 | + |
|
684 | + $uid = $this->getUID($cardData); |
|
685 | + $etag = md5($cardData); |
|
686 | + $query = $this->db->getQueryBuilder(); |
|
687 | + $query->update('cards') |
|
688 | + ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB)) |
|
689 | + ->set('lastmodified', $query->createNamedParameter(time())) |
|
690 | + ->set('size', $query->createNamedParameter(strlen($cardData))) |
|
691 | + ->set('etag', $query->createNamedParameter($etag)) |
|
692 | + ->set('uid', $query->createNamedParameter($uid)) |
|
693 | + ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) |
|
694 | + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
695 | + ->execute(); |
|
696 | + |
|
697 | + $this->addChange($addressBookId, $cardUri, 2); |
|
698 | + $this->updateProperties($addressBookId, $cardUri, $cardData); |
|
699 | + |
|
700 | + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', |
|
701 | + new GenericEvent(null, [ |
|
702 | + 'addressBookId' => $addressBookId, |
|
703 | + 'cardUri' => $cardUri, |
|
704 | + 'cardData' => $cardData])); |
|
705 | + |
|
706 | + return '"' . $etag . '"'; |
|
707 | + } |
|
708 | + |
|
709 | + /** |
|
710 | + * Deletes a card |
|
711 | + * |
|
712 | + * @param mixed $addressBookId |
|
713 | + * @param string $cardUri |
|
714 | + * @return bool |
|
715 | + */ |
|
716 | + function deleteCard($addressBookId, $cardUri) { |
|
717 | + try { |
|
718 | + $cardId = $this->getCardId($addressBookId, $cardUri); |
|
719 | + } catch (\InvalidArgumentException $e) { |
|
720 | + $cardId = null; |
|
721 | + } |
|
722 | + $query = $this->db->getQueryBuilder(); |
|
723 | + $ret = $query->delete('cards') |
|
724 | + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) |
|
725 | + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) |
|
726 | + ->execute(); |
|
727 | + |
|
728 | + $this->addChange($addressBookId, $cardUri, 3); |
|
729 | + |
|
730 | + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', |
|
731 | + new GenericEvent(null, [ |
|
732 | + 'addressBookId' => $addressBookId, |
|
733 | + 'cardUri' => $cardUri])); |
|
734 | + |
|
735 | + if ($ret === 1) { |
|
736 | + if ($cardId !== null) { |
|
737 | + $this->purgeProperties($addressBookId, $cardId); |
|
738 | + } |
|
739 | + return true; |
|
740 | + } |
|
741 | + |
|
742 | + return false; |
|
743 | + } |
|
744 | + |
|
745 | + /** |
|
746 | + * The getChanges method returns all the changes that have happened, since |
|
747 | + * the specified syncToken in the specified address book. |
|
748 | + * |
|
749 | + * This function should return an array, such as the following: |
|
750 | + * |
|
751 | + * [ |
|
752 | + * 'syncToken' => 'The current synctoken', |
|
753 | + * 'added' => [ |
|
754 | + * 'new.txt', |
|
755 | + * ], |
|
756 | + * 'modified' => [ |
|
757 | + * 'modified.txt', |
|
758 | + * ], |
|
759 | + * 'deleted' => [ |
|
760 | + * 'foo.php.bak', |
|
761 | + * 'old.txt' |
|
762 | + * ] |
|
763 | + * ]; |
|
764 | + * |
|
765 | + * The returned syncToken property should reflect the *current* syncToken |
|
766 | + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
767 | + * property. This is needed here too, to ensure the operation is atomic. |
|
768 | + * |
|
769 | + * If the $syncToken argument is specified as null, this is an initial |
|
770 | + * sync, and all members should be reported. |
|
771 | + * |
|
772 | + * The modified property is an array of nodenames that have changed since |
|
773 | + * the last token. |
|
774 | + * |
|
775 | + * The deleted property is an array with nodenames, that have been deleted |
|
776 | + * from collection. |
|
777 | + * |
|
778 | + * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
779 | + * 1, you only have to report changes that happened only directly in |
|
780 | + * immediate descendants. If it's 2, it should also include changes from |
|
781 | + * the nodes below the child collections. (grandchildren) |
|
782 | + * |
|
783 | + * The $limit argument allows a client to specify how many results should |
|
784 | + * be returned at most. If the limit is not specified, it should be treated |
|
785 | + * as infinite. |
|
786 | + * |
|
787 | + * If the limit (infinite or not) is higher than you're willing to return, |
|
788 | + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
789 | + * |
|
790 | + * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
791 | + * return null. |
|
792 | + * |
|
793 | + * The limit is 'suggestive'. You are free to ignore it. |
|
794 | + * |
|
795 | + * @param string $addressBookId |
|
796 | + * @param string $syncToken |
|
797 | + * @param int $syncLevel |
|
798 | + * @param int $limit |
|
799 | + * @return array |
|
800 | + */ |
|
801 | + function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { |
|
802 | + // Current synctoken |
|
803 | + $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?'); |
|
804 | + $stmt->execute([ $addressBookId ]); |
|
805 | + $currentToken = $stmt->fetchColumn(0); |
|
806 | + |
|
807 | + if (is_null($currentToken)) return null; |
|
808 | + |
|
809 | + $result = [ |
|
810 | + 'syncToken' => $currentToken, |
|
811 | + 'added' => [], |
|
812 | + 'modified' => [], |
|
813 | + 'deleted' => [], |
|
814 | + ]; |
|
815 | + |
|
816 | + if ($syncToken) { |
|
817 | + |
|
818 | + $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`"; |
|
819 | + if ($limit>0) { |
|
820 | + $query .= " LIMIT " . (int)$limit; |
|
821 | + } |
|
822 | + |
|
823 | + // Fetching all changes |
|
824 | + $stmt = $this->db->prepare($query); |
|
825 | + $stmt->execute([$syncToken, $currentToken, $addressBookId]); |
|
826 | + |
|
827 | + $changes = []; |
|
828 | + |
|
829 | + // This loop ensures that any duplicates are overwritten, only the |
|
830 | + // last change on a node is relevant. |
|
831 | + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
832 | + |
|
833 | + $changes[$row['uri']] = $row['operation']; |
|
834 | + |
|
835 | + } |
|
836 | + |
|
837 | + foreach($changes as $uri => $operation) { |
|
838 | + |
|
839 | + switch($operation) { |
|
840 | + case 1: |
|
841 | + $result['added'][] = $uri; |
|
842 | + break; |
|
843 | + case 2: |
|
844 | + $result['modified'][] = $uri; |
|
845 | + break; |
|
846 | + case 3: |
|
847 | + $result['deleted'][] = $uri; |
|
848 | + break; |
|
849 | + } |
|
850 | + |
|
851 | + } |
|
852 | + } else { |
|
853 | + // No synctoken supplied, this is the initial sync. |
|
854 | + $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?"; |
|
855 | + $stmt = $this->db->prepare($query); |
|
856 | + $stmt->execute([$addressBookId]); |
|
857 | + |
|
858 | + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); |
|
859 | + } |
|
860 | + return $result; |
|
861 | + } |
|
862 | + |
|
863 | + /** |
|
864 | + * Adds a change record to the addressbookchanges table. |
|
865 | + * |
|
866 | + * @param mixed $addressBookId |
|
867 | + * @param string $objectUri |
|
868 | + * @param int $operation 1 = add, 2 = modify, 3 = delete |
|
869 | + * @return void |
|
870 | + */ |
|
871 | + protected function addChange($addressBookId, $objectUri, $operation) { |
|
872 | + $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?'; |
|
873 | + $stmt = $this->db->prepare($sql); |
|
874 | + $stmt->execute([ |
|
875 | + $objectUri, |
|
876 | + $addressBookId, |
|
877 | + $operation, |
|
878 | + $addressBookId |
|
879 | + ]); |
|
880 | + $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); |
|
881 | + $stmt->execute([ |
|
882 | + $addressBookId |
|
883 | + ]); |
|
884 | + } |
|
885 | + |
|
886 | + private function readBlob($cardData) { |
|
887 | + if (is_resource($cardData)) { |
|
888 | + return stream_get_contents($cardData); |
|
889 | + } |
|
890 | + |
|
891 | + return $cardData; |
|
892 | + } |
|
893 | + |
|
894 | + /** |
|
895 | + * @param IShareable $shareable |
|
896 | + * @param string[] $add |
|
897 | + * @param string[] $remove |
|
898 | + */ |
|
899 | + public function updateShares(IShareable $shareable, $add, $remove) { |
|
900 | + $this->sharingBackend->updateShares($shareable, $add, $remove); |
|
901 | + } |
|
902 | + |
|
903 | + /** |
|
904 | + * search contact |
|
905 | + * |
|
906 | + * @param int $addressBookId |
|
907 | + * @param string $pattern which should match within the $searchProperties |
|
908 | + * @param array $searchProperties defines the properties within the query pattern should match |
|
909 | + * @param array $options = array() to define the search behavior |
|
910 | + * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are |
|
911 | + * @return array an array of contacts which are arrays of key-value-pairs |
|
912 | + */ |
|
913 | + public function search($addressBookId, $pattern, $searchProperties, $options = []) { |
|
914 | + $query = $this->db->getQueryBuilder(); |
|
915 | + $query2 = $this->db->getQueryBuilder(); |
|
916 | + |
|
917 | + $query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp'); |
|
918 | + $query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId))); |
|
919 | + $or = $query2->expr()->orX(); |
|
920 | + foreach ($searchProperties as $property) { |
|
921 | + $or->add($query2->expr()->eq('cp.name', $query->createNamedParameter($property))); |
|
922 | + } |
|
923 | + $query2->andWhere($or); |
|
924 | + |
|
925 | + // No need for like when the pattern is empty |
|
926 | + if ('' !== $pattern) { |
|
927 | + if(\array_key_exists('escape_like_param', $options) && $options['escape_like_param'] === false) { |
|
928 | + $query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter($pattern))); |
|
929 | + } else { |
|
930 | + $query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); |
|
931 | + } |
|
932 | + } |
|
933 | + |
|
934 | + $query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c') |
|
935 | + ->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL()))); |
|
936 | + |
|
937 | + $result = $query->execute(); |
|
938 | + $cards = $result->fetchAll(); |
|
939 | + |
|
940 | + $result->closeCursor(); |
|
941 | + |
|
942 | + return array_map(function ($array) { |
|
943 | + $array['carddata'] = $this->readBlob($array['carddata']); |
|
944 | + return $array; |
|
945 | + }, $cards); |
|
946 | + } |
|
947 | + |
|
948 | + /** |
|
949 | + * @param int $bookId |
|
950 | + * @param string $name |
|
951 | + * @return array |
|
952 | + */ |
|
953 | + public function collectCardProperties($bookId, $name) { |
|
954 | + $query = $this->db->getQueryBuilder(); |
|
955 | + $result = $query->selectDistinct('value') |
|
956 | + ->from($this->dbCardsPropertiesTable) |
|
957 | + ->where($query->expr()->eq('name', $query->createNamedParameter($name))) |
|
958 | + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId))) |
|
959 | + ->execute(); |
|
960 | + |
|
961 | + $all = $result->fetchAll(PDO::FETCH_COLUMN); |
|
962 | + $result->closeCursor(); |
|
963 | + |
|
964 | + return $all; |
|
965 | + } |
|
966 | + |
|
967 | + /** |
|
968 | + * get URI from a given contact |
|
969 | + * |
|
970 | + * @param int $id |
|
971 | + * @return string |
|
972 | + */ |
|
973 | + public function getCardUri($id) { |
|
974 | + $query = $this->db->getQueryBuilder(); |
|
975 | + $query->select('uri')->from($this->dbCardsTable) |
|
976 | + ->where($query->expr()->eq('id', $query->createParameter('id'))) |
|
977 | + ->setParameter('id', $id); |
|
978 | + |
|
979 | + $result = $query->execute(); |
|
980 | + $uri = $result->fetch(); |
|
981 | + $result->closeCursor(); |
|
982 | + |
|
983 | + if (!isset($uri['uri'])) { |
|
984 | + throw new \InvalidArgumentException('Card does not exists: ' . $id); |
|
985 | + } |
|
986 | + |
|
987 | + return $uri['uri']; |
|
988 | + } |
|
989 | + |
|
990 | + /** |
|
991 | + * return contact with the given URI |
|
992 | + * |
|
993 | + * @param int $addressBookId |
|
994 | + * @param string $uri |
|
995 | + * @returns array |
|
996 | + */ |
|
997 | + public function getContact($addressBookId, $uri) { |
|
998 | + $result = []; |
|
999 | + $query = $this->db->getQueryBuilder(); |
|
1000 | + $query->select('*')->from($this->dbCardsTable) |
|
1001 | + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
1002 | + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
1003 | + $queryResult = $query->execute(); |
|
1004 | + $contact = $queryResult->fetch(); |
|
1005 | + $queryResult->closeCursor(); |
|
1006 | + |
|
1007 | + if (is_array($contact)) { |
|
1008 | + $result = $contact; |
|
1009 | + } |
|
1010 | + |
|
1011 | + return $result; |
|
1012 | + } |
|
1013 | + |
|
1014 | + /** |
|
1015 | + * Returns the list of people whom this address book is shared with. |
|
1016 | + * |
|
1017 | + * Every element in this array should have the following properties: |
|
1018 | + * * href - Often a mailto: address |
|
1019 | + * * commonName - Optional, for example a first + last name |
|
1020 | + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. |
|
1021 | + * * readOnly - boolean |
|
1022 | + * * summary - Optional, a description for the share |
|
1023 | + * |
|
1024 | + * @return array |
|
1025 | + */ |
|
1026 | + public function getShares($addressBookId) { |
|
1027 | + return $this->sharingBackend->getShares($addressBookId); |
|
1028 | + } |
|
1029 | + |
|
1030 | + /** |
|
1031 | + * update properties table |
|
1032 | + * |
|
1033 | + * @param int $addressBookId |
|
1034 | + * @param string $cardUri |
|
1035 | + * @param string $vCardSerialized |
|
1036 | + */ |
|
1037 | + protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) { |
|
1038 | + $cardId = $this->getCardId($addressBookId, $cardUri); |
|
1039 | + $vCard = $this->readCard($vCardSerialized); |
|
1040 | + |
|
1041 | + $this->purgeProperties($addressBookId, $cardId); |
|
1042 | + |
|
1043 | + $query = $this->db->getQueryBuilder(); |
|
1044 | + $query->insert($this->dbCardsPropertiesTable) |
|
1045 | + ->values( |
|
1046 | + [ |
|
1047 | + 'addressbookid' => $query->createNamedParameter($addressBookId), |
|
1048 | + 'cardid' => $query->createNamedParameter($cardId), |
|
1049 | + 'name' => $query->createParameter('name'), |
|
1050 | + 'value' => $query->createParameter('value'), |
|
1051 | + 'preferred' => $query->createParameter('preferred') |
|
1052 | + ] |
|
1053 | + ); |
|
1054 | + |
|
1055 | + foreach ($vCard->children() as $property) { |
|
1056 | + if(!in_array($property->name, self::$indexProperties)) { |
|
1057 | + continue; |
|
1058 | + } |
|
1059 | + $preferred = 0; |
|
1060 | + foreach($property->parameters as $parameter) { |
|
1061 | + if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') { |
|
1062 | + $preferred = 1; |
|
1063 | + break; |
|
1064 | + } |
|
1065 | + } |
|
1066 | + $query->setParameter('name', $property->name); |
|
1067 | + $query->setParameter('value', mb_substr($property->getValue(), 0, 254)); |
|
1068 | + $query->setParameter('preferred', $preferred); |
|
1069 | + $query->execute(); |
|
1070 | + } |
|
1071 | + } |
|
1072 | + |
|
1073 | + /** |
|
1074 | + * read vCard data into a vCard object |
|
1075 | + * |
|
1076 | + * @param string $cardData |
|
1077 | + * @return VCard |
|
1078 | + */ |
|
1079 | + protected function readCard($cardData) { |
|
1080 | + return Reader::read($cardData); |
|
1081 | + } |
|
1082 | + |
|
1083 | + /** |
|
1084 | + * delete all properties from a given card |
|
1085 | + * |
|
1086 | + * @param int $addressBookId |
|
1087 | + * @param int $cardId |
|
1088 | + */ |
|
1089 | + protected function purgeProperties($addressBookId, $cardId) { |
|
1090 | + $query = $this->db->getQueryBuilder(); |
|
1091 | + $query->delete($this->dbCardsPropertiesTable) |
|
1092 | + ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId))) |
|
1093 | + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
1094 | + $query->execute(); |
|
1095 | + } |
|
1096 | + |
|
1097 | + /** |
|
1098 | + * get ID from a given contact |
|
1099 | + * |
|
1100 | + * @param int $addressBookId |
|
1101 | + * @param string $uri |
|
1102 | + * @return int |
|
1103 | + */ |
|
1104 | + protected function getCardId($addressBookId, $uri) { |
|
1105 | + $query = $this->db->getQueryBuilder(); |
|
1106 | + $query->select('id')->from($this->dbCardsTable) |
|
1107 | + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
1108 | + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); |
|
1109 | + |
|
1110 | + $result = $query->execute(); |
|
1111 | + $cardIds = $result->fetch(); |
|
1112 | + $result->closeCursor(); |
|
1113 | + |
|
1114 | + if (!isset($cardIds['id'])) { |
|
1115 | + throw new \InvalidArgumentException('Card does not exists: ' . $uri); |
|
1116 | + } |
|
1117 | + |
|
1118 | + return (int)$cardIds['id']; |
|
1119 | + } |
|
1120 | + |
|
1121 | + /** |
|
1122 | + * For shared address books the sharee is set in the ACL of the address book |
|
1123 | + * @param $addressBookId |
|
1124 | + * @param $acl |
|
1125 | + * @return array |
|
1126 | + */ |
|
1127 | + public function applyShareAcl($addressBookId, $acl) { |
|
1128 | + return $this->sharingBackend->applyShareAcl($addressBookId, $acl); |
|
1129 | + } |
|
1130 | + |
|
1131 | + private function convertPrincipal($principalUri, $toV2) { |
|
1132 | + if ($this->principalBackend->getPrincipalPrefix() === 'principals') { |
|
1133 | + list(, $name) = \Sabre\Uri\split($principalUri); |
|
1134 | + if ($toV2 === true) { |
|
1135 | + return "principals/users/$name"; |
|
1136 | + } |
|
1137 | + return "principals/$name"; |
|
1138 | + } |
|
1139 | + return $principalUri; |
|
1140 | + } |
|
1141 | + |
|
1142 | + private function addOwnerPrincipal(&$addressbookInfo) { |
|
1143 | + $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'; |
|
1144 | + $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'; |
|
1145 | + if (isset($addressbookInfo[$ownerPrincipalKey])) { |
|
1146 | + $uri = $addressbookInfo[$ownerPrincipalKey]; |
|
1147 | + } else { |
|
1148 | + $uri = $addressbookInfo['principaluri']; |
|
1149 | + } |
|
1150 | + |
|
1151 | + $principalInformation = $this->principalBackend->getPrincipalByPath($uri); |
|
1152 | + if (isset($principalInformation['{DAV:}displayname'])) { |
|
1153 | + $addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname']; |
|
1154 | + } |
|
1155 | + } |
|
1156 | + |
|
1157 | + /** |
|
1158 | + * Extract UID from vcard |
|
1159 | + * |
|
1160 | + * @param string $cardData the vcard raw data |
|
1161 | + * @return string the uid |
|
1162 | + * @throws BadRequest if no UID is available |
|
1163 | + */ |
|
1164 | + private function getUID($cardData) { |
|
1165 | + if ($cardData != '') { |
|
1166 | + $vCard = Reader::read($cardData); |
|
1167 | + if ($vCard->UID) { |
|
1168 | + $uid = $vCard->UID->getValue(); |
|
1169 | + return $uid; |
|
1170 | + } |
|
1171 | + // should already be handled, but just in case |
|
1172 | + throw new BadRequest('vCards on CardDAV servers MUST have a UID property'); |
|
1173 | + } |
|
1174 | + // should already be handled, but just in case |
|
1175 | + throw new BadRequest('vCard can not be empty'); |
|
1176 | + } |
|
1177 | 1177 | } |
@@ -124,7 +124,7 @@ discard block |
||
124 | 124 | ->from('addressbooks') |
125 | 125 | ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
126 | 126 | |
127 | - return (int)$query->execute()->fetchColumn(); |
|
127 | + return (int) $query->execute()->fetchColumn(); |
|
128 | 128 | } |
129 | 129 | |
130 | 130 | /** |
@@ -155,15 +155,15 @@ discard block |
||
155 | 155 | $addressBooks = []; |
156 | 156 | |
157 | 157 | $result = $query->execute(); |
158 | - while($row = $result->fetch()) { |
|
158 | + while ($row = $result->fetch()) { |
|
159 | 159 | $addressBooks[$row['id']] = [ |
160 | 160 | 'id' => $row['id'], |
161 | 161 | 'uri' => $row['uri'], |
162 | 162 | 'principaluri' => $this->convertPrincipal($row['principaluri'], false), |
163 | 163 | '{DAV:}displayname' => $row['displayname'], |
164 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
164 | + '{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'], |
|
165 | 165 | '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
166 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
166 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', |
|
167 | 167 | ]; |
168 | 168 | |
169 | 169 | $this->addOwnerPrincipal($addressBooks[$row['id']]); |
@@ -174,10 +174,10 @@ discard block |
||
174 | 174 | $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); |
175 | 175 | $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); |
176 | 176 | |
177 | - $principals = array_map(function ($principal) { |
|
177 | + $principals = array_map(function($principal) { |
|
178 | 178 | return urldecode($principal); |
179 | 179 | }, $principals); |
180 | - $principals[]= $principalUri; |
|
180 | + $principals[] = $principalUri; |
|
181 | 181 | |
182 | 182 | $query = $this->db->getQueryBuilder(); |
183 | 183 | $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) |
@@ -189,8 +189,8 @@ discard block |
||
189 | 189 | ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) |
190 | 190 | ->execute(); |
191 | 191 | |
192 | - $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; |
|
193 | - while($row = $result->fetch()) { |
|
192 | + $readOnlyPropertyName = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only'; |
|
193 | + while ($row = $result->fetch()) { |
|
194 | 194 | if ($row['principaluri'] === $principalUri) { |
195 | 195 | continue; |
196 | 196 | } |
@@ -209,18 +209,18 @@ discard block |
||
209 | 209 | } |
210 | 210 | |
211 | 211 | list(, $name) = \Sabre\Uri\split($row['principaluri']); |
212 | - $uri = $row['uri'] . '_shared_by_' . $name; |
|
213 | - $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')'; |
|
212 | + $uri = $row['uri'].'_shared_by_'.$name; |
|
213 | + $displayName = $row['displayname'].' ('.$this->getUserDisplayName($name).')'; |
|
214 | 214 | |
215 | 215 | $addressBooks[$row['id']] = [ |
216 | 216 | 'id' => $row['id'], |
217 | 217 | 'uri' => $uri, |
218 | 218 | 'principaluri' => $principalUriOriginal, |
219 | 219 | '{DAV:}displayname' => $displayName, |
220 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
220 | + '{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'], |
|
221 | 221 | '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
222 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
223 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], |
|
222 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', |
|
223 | + '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $row['principaluri'], |
|
224 | 224 | $readOnlyPropertyName => $readOnly, |
225 | 225 | ]; |
226 | 226 | |
@@ -241,15 +241,15 @@ discard block |
||
241 | 241 | $addressBooks = []; |
242 | 242 | |
243 | 243 | $result = $query->execute(); |
244 | - while($row = $result->fetch()) { |
|
244 | + while ($row = $result->fetch()) { |
|
245 | 245 | $addressBooks[$row['id']] = [ |
246 | 246 | 'id' => $row['id'], |
247 | 247 | 'uri' => $row['uri'], |
248 | 248 | 'principaluri' => $this->convertPrincipal($row['principaluri'], false), |
249 | 249 | '{DAV:}displayname' => $row['displayname'], |
250 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
250 | + '{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'], |
|
251 | 251 | '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
252 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
252 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', |
|
253 | 253 | ]; |
254 | 254 | |
255 | 255 | $this->addOwnerPrincipal($addressBooks[$row['id']]); |
@@ -294,9 +294,9 @@ discard block |
||
294 | 294 | 'uri' => $row['uri'], |
295 | 295 | 'principaluri' => $row['principaluri'], |
296 | 296 | '{DAV:}displayname' => $row['displayname'], |
297 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
297 | + '{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'], |
|
298 | 298 | '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
299 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
299 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', |
|
300 | 300 | ]; |
301 | 301 | |
302 | 302 | $this->addOwnerPrincipal($addressBook); |
@@ -328,9 +328,9 @@ discard block |
||
328 | 328 | 'uri' => $row['uri'], |
329 | 329 | 'principaluri' => $row['principaluri'], |
330 | 330 | '{DAV:}displayname' => $row['displayname'], |
331 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], |
|
331 | + '{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'], |
|
332 | 332 | '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], |
333 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
333 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', |
|
334 | 334 | ]; |
335 | 335 | |
336 | 336 | $this->addOwnerPrincipal($addressBook); |
@@ -357,22 +357,22 @@ discard block |
||
357 | 357 | function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { |
358 | 358 | $supportedProperties = [ |
359 | 359 | '{DAV:}displayname', |
360 | - '{' . Plugin::NS_CARDDAV . '}addressbook-description', |
|
360 | + '{'.Plugin::NS_CARDDAV.'}addressbook-description', |
|
361 | 361 | ]; |
362 | 362 | |
363 | 363 | /** |
364 | 364 | * @suppress SqlInjectionChecker |
365 | 365 | */ |
366 | - $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) { |
|
366 | + $propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) { |
|
367 | 367 | |
368 | 368 | $updates = []; |
369 | - foreach($mutations as $property=>$newValue) { |
|
369 | + foreach ($mutations as $property=>$newValue) { |
|
370 | 370 | |
371 | - switch($property) { |
|
371 | + switch ($property) { |
|
372 | 372 | case '{DAV:}displayname': |
373 | 373 | $updates['displayname'] = $newValue; |
374 | 374 | break; |
375 | - case '{' . Plugin::NS_CARDDAV . '}addressbook-description': |
|
375 | + case '{'.Plugin::NS_CARDDAV.'}addressbook-description': |
|
376 | 376 | $updates['description'] = $newValue; |
377 | 377 | break; |
378 | 378 | } |
@@ -380,7 +380,7 @@ discard block |
||
380 | 380 | $query = $this->db->getQueryBuilder(); |
381 | 381 | $query->update('addressbooks'); |
382 | 382 | |
383 | - foreach($updates as $key=>$value) { |
|
383 | + foreach ($updates as $key=>$value) { |
|
384 | 384 | $query->set($key, $query->createNamedParameter($value)); |
385 | 385 | } |
386 | 386 | $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) |
@@ -411,24 +411,24 @@ discard block |
||
411 | 411 | 'synctoken' => 1 |
412 | 412 | ]; |
413 | 413 | |
414 | - foreach($properties as $property=>$newValue) { |
|
414 | + foreach ($properties as $property=>$newValue) { |
|
415 | 415 | |
416 | - switch($property) { |
|
416 | + switch ($property) { |
|
417 | 417 | case '{DAV:}displayname': |
418 | 418 | $values['displayname'] = $newValue; |
419 | 419 | break; |
420 | - case '{' . Plugin::NS_CARDDAV . '}addressbook-description': |
|
420 | + case '{'.Plugin::NS_CARDDAV.'}addressbook-description': |
|
421 | 421 | $values['description'] = $newValue; |
422 | 422 | break; |
423 | 423 | default: |
424 | - throw new BadRequest('Unknown property: ' . $property); |
|
424 | + throw new BadRequest('Unknown property: '.$property); |
|
425 | 425 | } |
426 | 426 | |
427 | 427 | } |
428 | 428 | |
429 | 429 | // Fallback to make sure the displayname is set. Some clients may refuse |
430 | 430 | // to work with addressbooks not having a displayname. |
431 | - if(is_null($values['displayname'])) { |
|
431 | + if (is_null($values['displayname'])) { |
|
432 | 432 | $values['displayname'] = $url; |
433 | 433 | } |
434 | 434 | |
@@ -506,8 +506,8 @@ discard block |
||
506 | 506 | $cards = []; |
507 | 507 | |
508 | 508 | $result = $query->execute(); |
509 | - while($row = $result->fetch()) { |
|
510 | - $row['etag'] = '"' . $row['etag'] . '"'; |
|
509 | + while ($row = $result->fetch()) { |
|
510 | + $row['etag'] = '"'.$row['etag'].'"'; |
|
511 | 511 | $row['carddata'] = $this->readBlob($row['carddata']); |
512 | 512 | $cards[] = $row; |
513 | 513 | } |
@@ -541,7 +541,7 @@ discard block |
||
541 | 541 | if (!$row) { |
542 | 542 | return false; |
543 | 543 | } |
544 | - $row['etag'] = '"' . $row['etag'] . '"'; |
|
544 | + $row['etag'] = '"'.$row['etag'].'"'; |
|
545 | 545 | $row['carddata'] = $this->readBlob($row['carddata']); |
546 | 546 | |
547 | 547 | return $row; |
@@ -578,7 +578,7 @@ discard block |
||
578 | 578 | $result = $query->execute(); |
579 | 579 | |
580 | 580 | while ($row = $result->fetch()) { |
581 | - $row['etag'] = '"' . $row['etag'] . '"'; |
|
581 | + $row['etag'] = '"'.$row['etag'].'"'; |
|
582 | 582 | $row['carddata'] = $this->readBlob($row['carddata']); |
583 | 583 | $cards[] = $row; |
584 | 584 | } |
@@ -651,7 +651,7 @@ discard block |
||
651 | 651 | 'cardUri' => $cardUri, |
652 | 652 | 'cardData' => $cardData])); |
653 | 653 | |
654 | - return '"' . $etag . '"'; |
|
654 | + return '"'.$etag.'"'; |
|
655 | 655 | } |
656 | 656 | |
657 | 657 | /** |
@@ -703,7 +703,7 @@ discard block |
||
703 | 703 | 'cardUri' => $cardUri, |
704 | 704 | 'cardData' => $cardData])); |
705 | 705 | |
706 | - return '"' . $etag . '"'; |
|
706 | + return '"'.$etag.'"'; |
|
707 | 707 | } |
708 | 708 | |
709 | 709 | /** |
@@ -801,7 +801,7 @@ discard block |
||
801 | 801 | function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { |
802 | 802 | // Current synctoken |
803 | 803 | $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?'); |
804 | - $stmt->execute([ $addressBookId ]); |
|
804 | + $stmt->execute([$addressBookId]); |
|
805 | 805 | $currentToken = $stmt->fetchColumn(0); |
806 | 806 | |
807 | 807 | if (is_null($currentToken)) return null; |
@@ -816,8 +816,8 @@ discard block |
||
816 | 816 | if ($syncToken) { |
817 | 817 | |
818 | 818 | $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`"; |
819 | - if ($limit>0) { |
|
820 | - $query .= " LIMIT " . (int)$limit; |
|
819 | + if ($limit > 0) { |
|
820 | + $query .= " LIMIT ".(int) $limit; |
|
821 | 821 | } |
822 | 822 | |
823 | 823 | // Fetching all changes |
@@ -828,15 +828,15 @@ discard block |
||
828 | 828 | |
829 | 829 | // This loop ensures that any duplicates are overwritten, only the |
830 | 830 | // last change on a node is relevant. |
831 | - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
831 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
832 | 832 | |
833 | 833 | $changes[$row['uri']] = $row['operation']; |
834 | 834 | |
835 | 835 | } |
836 | 836 | |
837 | - foreach($changes as $uri => $operation) { |
|
837 | + foreach ($changes as $uri => $operation) { |
|
838 | 838 | |
839 | - switch($operation) { |
|
839 | + switch ($operation) { |
|
840 | 840 | case 1: |
841 | 841 | $result['added'][] = $uri; |
842 | 842 | break; |
@@ -924,10 +924,10 @@ discard block |
||
924 | 924 | |
925 | 925 | // No need for like when the pattern is empty |
926 | 926 | if ('' !== $pattern) { |
927 | - if(\array_key_exists('escape_like_param', $options) && $options['escape_like_param'] === false) { |
|
927 | + if (\array_key_exists('escape_like_param', $options) && $options['escape_like_param'] === false) { |
|
928 | 928 | $query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter($pattern))); |
929 | 929 | } else { |
930 | - $query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); |
|
930 | + $query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter('%'.$this->db->escapeLikeParameter($pattern).'%'))); |
|
931 | 931 | } |
932 | 932 | } |
933 | 933 | |
@@ -939,7 +939,7 @@ discard block |
||
939 | 939 | |
940 | 940 | $result->closeCursor(); |
941 | 941 | |
942 | - return array_map(function ($array) { |
|
942 | + return array_map(function($array) { |
|
943 | 943 | $array['carddata'] = $this->readBlob($array['carddata']); |
944 | 944 | return $array; |
945 | 945 | }, $cards); |
@@ -981,7 +981,7 @@ discard block |
||
981 | 981 | $result->closeCursor(); |
982 | 982 | |
983 | 983 | if (!isset($uri['uri'])) { |
984 | - throw new \InvalidArgumentException('Card does not exists: ' . $id); |
|
984 | + throw new \InvalidArgumentException('Card does not exists: '.$id); |
|
985 | 985 | } |
986 | 986 | |
987 | 987 | return $uri['uri']; |
@@ -1053,11 +1053,11 @@ discard block |
||
1053 | 1053 | ); |
1054 | 1054 | |
1055 | 1055 | foreach ($vCard->children() as $property) { |
1056 | - if(!in_array($property->name, self::$indexProperties)) { |
|
1056 | + if (!in_array($property->name, self::$indexProperties)) { |
|
1057 | 1057 | continue; |
1058 | 1058 | } |
1059 | 1059 | $preferred = 0; |
1060 | - foreach($property->parameters as $parameter) { |
|
1060 | + foreach ($property->parameters as $parameter) { |
|
1061 | 1061 | if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') { |
1062 | 1062 | $preferred = 1; |
1063 | 1063 | break; |
@@ -1112,10 +1112,10 @@ discard block |
||
1112 | 1112 | $result->closeCursor(); |
1113 | 1113 | |
1114 | 1114 | if (!isset($cardIds['id'])) { |
1115 | - throw new \InvalidArgumentException('Card does not exists: ' . $uri); |
|
1115 | + throw new \InvalidArgumentException('Card does not exists: '.$uri); |
|
1116 | 1116 | } |
1117 | 1117 | |
1118 | - return (int)$cardIds['id']; |
|
1118 | + return (int) $cardIds['id']; |
|
1119 | 1119 | } |
1120 | 1120 | |
1121 | 1121 | /** |
@@ -1140,8 +1140,8 @@ discard block |
||
1140 | 1140 | } |
1141 | 1141 | |
1142 | 1142 | private function addOwnerPrincipal(&$addressbookInfo) { |
1143 | - $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'; |
|
1144 | - $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'; |
|
1143 | + $ownerPrincipalKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal'; |
|
1144 | + $displaynameKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}owner-displayname'; |
|
1145 | 1145 | if (isset($addressbookInfo[$ownerPrincipalKey])) { |
1146 | 1146 | $uri = $addressbookInfo[$ownerPrincipalKey]; |
1147 | 1147 | } else { |
@@ -53,249 +53,249 @@ |
||
53 | 53 | */ |
54 | 54 | class RequestHandlerController extends Controller { |
55 | 55 | |
56 | - /** @var ILogger */ |
|
57 | - private $logger; |
|
58 | - |
|
59 | - /** @var IUserManager */ |
|
60 | - private $userManager; |
|
61 | - |
|
62 | - /** @var IGroupManager */ |
|
63 | - private $groupManager; |
|
64 | - |
|
65 | - /** @var IURLGenerator */ |
|
66 | - private $urlGenerator; |
|
67 | - |
|
68 | - /** @var ICloudFederationProviderManager */ |
|
69 | - private $cloudFederationProviderManager; |
|
70 | - |
|
71 | - /** @var Config */ |
|
72 | - private $config; |
|
73 | - |
|
74 | - /** @var ICloudFederationFactory */ |
|
75 | - private $factory; |
|
76 | - |
|
77 | - /** @var ICloudIdManager */ |
|
78 | - private $cloudIdManager; |
|
79 | - |
|
80 | - public function __construct($appName, |
|
81 | - IRequest $request, |
|
82 | - ILogger $logger, |
|
83 | - IUserManager $userManager, |
|
84 | - IGroupManager $groupManager, |
|
85 | - IURLGenerator $urlGenerator, |
|
86 | - ICloudFederationProviderManager $cloudFederationProviderManager, |
|
87 | - Config $config, |
|
88 | - ICloudFederationFactory $factory, |
|
89 | - ICloudIdManager $cloudIdManager |
|
90 | - ) { |
|
91 | - parent::__construct($appName, $request); |
|
92 | - |
|
93 | - $this->logger = $logger; |
|
94 | - $this->userManager = $userManager; |
|
95 | - $this->groupManager = $groupManager; |
|
96 | - $this->urlGenerator = $urlGenerator; |
|
97 | - $this->cloudFederationProviderManager = $cloudFederationProviderManager; |
|
98 | - $this->config = $config; |
|
99 | - $this->factory = $factory; |
|
100 | - $this->cloudIdManager = $cloudIdManager; |
|
101 | - } |
|
102 | - |
|
103 | - /** |
|
104 | - * add share |
|
105 | - * |
|
106 | - * @NoCSRFRequired |
|
107 | - * @PublicPage |
|
108 | - * @BruteForceProtection(action=receiveFederatedShare) |
|
109 | - * |
|
110 | - * @param string $shareWith |
|
111 | - * @param string $name resource name (e.g. document.odt) |
|
112 | - * @param string $description share description (optional) |
|
113 | - * @param string $providerId resource UID on the provider side |
|
114 | - * @param string $owner provider specific UID of the user who owns the resource |
|
115 | - * @param string $ownerDisplayName display name of the user who shared the item |
|
116 | - * @param string $sharedBy provider specific UID of the user who shared the resource |
|
117 | - * @param string $sharedByDisplayName display name of the user who shared the resource |
|
118 | - * @param array $protocol (e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]) |
|
119 | - * @param string $shareType ('group' or 'user' share) |
|
120 | - * @param $resourceType ('file', 'calendar',...) |
|
121 | - * @return Http\DataResponse|JSONResponse |
|
122 | - * |
|
123 | - * Example: curl -H "Content-Type: application/json" -X POST -d '{"shareWith":"admin1@serve1","name":"welcome server2.txt","description":"desc","providerId":"2","owner":"admin2@http://localhost/server2","ownerDisplayName":"admin2 display","shareType":"user","resourceType":"file","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' http://localhost/server/index.php/ocm/shares |
|
124 | - */ |
|
125 | - public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { |
|
126 | - |
|
127 | - // check if all required parameters are set |
|
128 | - if ($shareWith === null || |
|
129 | - $name === null || |
|
130 | - $providerId === null || |
|
131 | - $owner === null || |
|
132 | - $resourceType === null || |
|
133 | - $shareType === null || |
|
134 | - !is_array($protocol) || |
|
135 | - !isset($protocol['name']) || |
|
136 | - !isset($protocol['options']) || |
|
137 | - !is_array($protocol['options']) || |
|
138 | - !isset($protocol['options']['sharedSecret']) |
|
139 | - ) { |
|
140 | - return new JSONResponse( |
|
141 | - ['message' => 'Missing arguments'], |
|
142 | - Http::STATUS_BAD_REQUEST |
|
143 | - ); |
|
144 | - } |
|
145 | - |
|
146 | - $supportedShareTypes = $this->config->getSupportedShareTypes($resourceType); |
|
147 | - if (!in_array($shareType, $supportedShareTypes)) { |
|
148 | - return new JSONResponse( |
|
149 | - ['message' => 'Share type "' . $shareType . '" not implemented'], |
|
150 | - Http::STATUS_NOT_IMPLEMENTED |
|
151 | - ); |
|
152 | - } |
|
153 | - |
|
154 | - $cloudId = $this->cloudIdManager->resolveCloudId($shareWith); |
|
155 | - $shareWith = $cloudId->getUser(); |
|
156 | - |
|
157 | - if ($shareType === 'user') { |
|
158 | - $shareWith = $this->mapUid($shareWith); |
|
159 | - |
|
160 | - if (!$this->userManager->userExists($shareWith)) { |
|
161 | - return new JSONResponse( |
|
162 | - ['message' => 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], |
|
163 | - Http::STATUS_BAD_REQUEST |
|
164 | - ); |
|
165 | - } |
|
166 | - } |
|
167 | - |
|
168 | - if ($shareType === 'group') { |
|
169 | - if(!$this->groupManager->groupExists($shareWith)) { |
|
170 | - return new JSONResponse( |
|
171 | - ['message' => 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], |
|
172 | - Http::STATUS_BAD_REQUEST |
|
173 | - ); |
|
174 | - } |
|
175 | - } |
|
176 | - |
|
177 | - // if no explicit display name is given, we use the uid as display name |
|
178 | - $ownerDisplayName = $ownerDisplayName === null ? $owner : $ownerDisplayName; |
|
179 | - $sharedByDisplayName = $sharedByDisplayName === null ? $sharedBy : $sharedByDisplayName; |
|
180 | - |
|
181 | - // sharedBy* parameter is optional, if nothing is set we assume that it is the same user as the owner |
|
182 | - if ($sharedBy === null) { |
|
183 | - $sharedBy = $owner; |
|
184 | - $sharedByDisplayName = $ownerDisplayName; |
|
185 | - } |
|
186 | - |
|
187 | - try { |
|
188 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); |
|
189 | - $share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType); |
|
190 | - $share->setProtocol($protocol); |
|
191 | - $provider->shareReceived($share); |
|
192 | - } catch (ProviderDoesNotExistsException $e) { |
|
193 | - return new JSONResponse( |
|
194 | - ['message' => $e->getMessage()], |
|
195 | - Http::STATUS_NOT_IMPLEMENTED |
|
196 | - ); |
|
197 | - } catch (ProviderCouldNotAddShareException $e) { |
|
198 | - return new JSONResponse( |
|
199 | - ['message' => $e->getMessage()], |
|
200 | - $e->getCode() |
|
201 | - ); |
|
202 | - } catch (\Exception $e) { |
|
203 | - return new JSONResponse( |
|
204 | - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], |
|
205 | - Http::STATUS_BAD_REQUEST |
|
206 | - ); |
|
207 | - } |
|
208 | - |
|
209 | - $user = $this->userManager->get($shareWith); |
|
210 | - $recipientDisplayName = ''; |
|
211 | - if($user) { |
|
212 | - $recipientDisplayName = $user->getDisplayName(); |
|
213 | - } |
|
214 | - |
|
215 | - return new JSONResponse( |
|
216 | - ['recipientDisplayName' => $recipientDisplayName], |
|
217 | - Http::STATUS_CREATED); |
|
218 | - |
|
219 | - } |
|
220 | - |
|
221 | - /** |
|
222 | - * receive notification about existing share |
|
223 | - * |
|
224 | - * @NoCSRFRequired |
|
225 | - * @PublicPage |
|
226 | - * @BruteForceProtection(action=receiveFederatedShareNotification) |
|
227 | - * |
|
228 | - * @param string $notificationType (notification type, e.g. SHARE_ACCEPTED) |
|
229 | - * @param string $resourceType (calendar, file, contact,...) |
|
230 | - * @param string $providerId id of the share |
|
231 | - * @param array $notification the actual payload of the notification |
|
232 | - * @return JSONResponse |
|
233 | - */ |
|
234 | - public function receiveNotification($notificationType, $resourceType, $providerId, array $notification) { |
|
235 | - |
|
236 | - // check if all required parameters are set |
|
237 | - if ($notificationType === null || |
|
238 | - $resourceType === null || |
|
239 | - $providerId === null || |
|
240 | - !is_array($notification) |
|
241 | - ) { |
|
242 | - return new JSONResponse( |
|
243 | - ['message' => 'Missing arguments'], |
|
244 | - Http::STATUS_BAD_REQUEST |
|
245 | - ); |
|
246 | - } |
|
247 | - |
|
248 | - try { |
|
249 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); |
|
250 | - $result = $provider->notificationReceived($notificationType, $providerId, $notification); |
|
251 | - } catch (ProviderDoesNotExistsException $e) { |
|
252 | - return new JSONResponse( |
|
253 | - ['message' => $e->getMessage()], |
|
254 | - Http::STATUS_BAD_REQUEST |
|
255 | - ); |
|
256 | - } catch (ShareNotFound $e) { |
|
257 | - return new JSONResponse( |
|
258 | - ['message' => $e->getMessage()], |
|
259 | - Http::STATUS_BAD_REQUEST |
|
260 | - ); |
|
261 | - } catch (ActionNotSupportedException $e) { |
|
262 | - return new JSONResponse( |
|
263 | - ['message' => $e->getMessage()], |
|
264 | - Http::STATUS_NOT_IMPLEMENTED |
|
265 | - ); |
|
266 | - } catch (BadRequestException $e) { |
|
267 | - return new JSONResponse($e->getReturnMessage(), Http::STATUS_BAD_REQUEST); |
|
268 | - } catch (AuthenticationFailedException $e) { |
|
269 | - return new JSONResponse(["message" => "RESOURCE_NOT_FOUND"], Http::STATUS_FORBIDDEN); |
|
270 | - } |
|
271 | - catch (\Exception $e) { |
|
272 | - return new JSONResponse( |
|
273 | - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], |
|
274 | - Http::STATUS_BAD_REQUEST |
|
275 | - ); |
|
276 | - } |
|
277 | - |
|
278 | - return new JSONResponse($result,Http::STATUS_CREATED); |
|
279 | - |
|
280 | - } |
|
281 | - |
|
282 | - /** |
|
283 | - * map login name to internal LDAP UID if a LDAP backend is in use |
|
284 | - * |
|
285 | - * @param string $uid |
|
286 | - * @return string mixed |
|
287 | - */ |
|
288 | - private function mapUid($uid) { |
|
289 | - // FIXME this should be a method in the user management instead |
|
290 | - $this->logger->debug('shareWith before, ' . $uid, ['app' => $this->appName]); |
|
291 | - \OCP\Util::emitHook( |
|
292 | - '\OCA\Files_Sharing\API\Server2Server', |
|
293 | - 'preLoginNameUsedAsUserName', |
|
294 | - ['uid' => &$uid] |
|
295 | - ); |
|
296 | - $this->logger->debug('shareWith after, ' . $uid, ['app' => $this->appName]); |
|
297 | - |
|
298 | - return $uid; |
|
299 | - } |
|
56 | + /** @var ILogger */ |
|
57 | + private $logger; |
|
58 | + |
|
59 | + /** @var IUserManager */ |
|
60 | + private $userManager; |
|
61 | + |
|
62 | + /** @var IGroupManager */ |
|
63 | + private $groupManager; |
|
64 | + |
|
65 | + /** @var IURLGenerator */ |
|
66 | + private $urlGenerator; |
|
67 | + |
|
68 | + /** @var ICloudFederationProviderManager */ |
|
69 | + private $cloudFederationProviderManager; |
|
70 | + |
|
71 | + /** @var Config */ |
|
72 | + private $config; |
|
73 | + |
|
74 | + /** @var ICloudFederationFactory */ |
|
75 | + private $factory; |
|
76 | + |
|
77 | + /** @var ICloudIdManager */ |
|
78 | + private $cloudIdManager; |
|
79 | + |
|
80 | + public function __construct($appName, |
|
81 | + IRequest $request, |
|
82 | + ILogger $logger, |
|
83 | + IUserManager $userManager, |
|
84 | + IGroupManager $groupManager, |
|
85 | + IURLGenerator $urlGenerator, |
|
86 | + ICloudFederationProviderManager $cloudFederationProviderManager, |
|
87 | + Config $config, |
|
88 | + ICloudFederationFactory $factory, |
|
89 | + ICloudIdManager $cloudIdManager |
|
90 | + ) { |
|
91 | + parent::__construct($appName, $request); |
|
92 | + |
|
93 | + $this->logger = $logger; |
|
94 | + $this->userManager = $userManager; |
|
95 | + $this->groupManager = $groupManager; |
|
96 | + $this->urlGenerator = $urlGenerator; |
|
97 | + $this->cloudFederationProviderManager = $cloudFederationProviderManager; |
|
98 | + $this->config = $config; |
|
99 | + $this->factory = $factory; |
|
100 | + $this->cloudIdManager = $cloudIdManager; |
|
101 | + } |
|
102 | + |
|
103 | + /** |
|
104 | + * add share |
|
105 | + * |
|
106 | + * @NoCSRFRequired |
|
107 | + * @PublicPage |
|
108 | + * @BruteForceProtection(action=receiveFederatedShare) |
|
109 | + * |
|
110 | + * @param string $shareWith |
|
111 | + * @param string $name resource name (e.g. document.odt) |
|
112 | + * @param string $description share description (optional) |
|
113 | + * @param string $providerId resource UID on the provider side |
|
114 | + * @param string $owner provider specific UID of the user who owns the resource |
|
115 | + * @param string $ownerDisplayName display name of the user who shared the item |
|
116 | + * @param string $sharedBy provider specific UID of the user who shared the resource |
|
117 | + * @param string $sharedByDisplayName display name of the user who shared the resource |
|
118 | + * @param array $protocol (e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]) |
|
119 | + * @param string $shareType ('group' or 'user' share) |
|
120 | + * @param $resourceType ('file', 'calendar',...) |
|
121 | + * @return Http\DataResponse|JSONResponse |
|
122 | + * |
|
123 | + * Example: curl -H "Content-Type: application/json" -X POST -d '{"shareWith":"admin1@serve1","name":"welcome server2.txt","description":"desc","providerId":"2","owner":"admin2@http://localhost/server2","ownerDisplayName":"admin2 display","shareType":"user","resourceType":"file","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' http://localhost/server/index.php/ocm/shares |
|
124 | + */ |
|
125 | + public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { |
|
126 | + |
|
127 | + // check if all required parameters are set |
|
128 | + if ($shareWith === null || |
|
129 | + $name === null || |
|
130 | + $providerId === null || |
|
131 | + $owner === null || |
|
132 | + $resourceType === null || |
|
133 | + $shareType === null || |
|
134 | + !is_array($protocol) || |
|
135 | + !isset($protocol['name']) || |
|
136 | + !isset($protocol['options']) || |
|
137 | + !is_array($protocol['options']) || |
|
138 | + !isset($protocol['options']['sharedSecret']) |
|
139 | + ) { |
|
140 | + return new JSONResponse( |
|
141 | + ['message' => 'Missing arguments'], |
|
142 | + Http::STATUS_BAD_REQUEST |
|
143 | + ); |
|
144 | + } |
|
145 | + |
|
146 | + $supportedShareTypes = $this->config->getSupportedShareTypes($resourceType); |
|
147 | + if (!in_array($shareType, $supportedShareTypes)) { |
|
148 | + return new JSONResponse( |
|
149 | + ['message' => 'Share type "' . $shareType . '" not implemented'], |
|
150 | + Http::STATUS_NOT_IMPLEMENTED |
|
151 | + ); |
|
152 | + } |
|
153 | + |
|
154 | + $cloudId = $this->cloudIdManager->resolveCloudId($shareWith); |
|
155 | + $shareWith = $cloudId->getUser(); |
|
156 | + |
|
157 | + if ($shareType === 'user') { |
|
158 | + $shareWith = $this->mapUid($shareWith); |
|
159 | + |
|
160 | + if (!$this->userManager->userExists($shareWith)) { |
|
161 | + return new JSONResponse( |
|
162 | + ['message' => 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], |
|
163 | + Http::STATUS_BAD_REQUEST |
|
164 | + ); |
|
165 | + } |
|
166 | + } |
|
167 | + |
|
168 | + if ($shareType === 'group') { |
|
169 | + if(!$this->groupManager->groupExists($shareWith)) { |
|
170 | + return new JSONResponse( |
|
171 | + ['message' => 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], |
|
172 | + Http::STATUS_BAD_REQUEST |
|
173 | + ); |
|
174 | + } |
|
175 | + } |
|
176 | + |
|
177 | + // if no explicit display name is given, we use the uid as display name |
|
178 | + $ownerDisplayName = $ownerDisplayName === null ? $owner : $ownerDisplayName; |
|
179 | + $sharedByDisplayName = $sharedByDisplayName === null ? $sharedBy : $sharedByDisplayName; |
|
180 | + |
|
181 | + // sharedBy* parameter is optional, if nothing is set we assume that it is the same user as the owner |
|
182 | + if ($sharedBy === null) { |
|
183 | + $sharedBy = $owner; |
|
184 | + $sharedByDisplayName = $ownerDisplayName; |
|
185 | + } |
|
186 | + |
|
187 | + try { |
|
188 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); |
|
189 | + $share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType); |
|
190 | + $share->setProtocol($protocol); |
|
191 | + $provider->shareReceived($share); |
|
192 | + } catch (ProviderDoesNotExistsException $e) { |
|
193 | + return new JSONResponse( |
|
194 | + ['message' => $e->getMessage()], |
|
195 | + Http::STATUS_NOT_IMPLEMENTED |
|
196 | + ); |
|
197 | + } catch (ProviderCouldNotAddShareException $e) { |
|
198 | + return new JSONResponse( |
|
199 | + ['message' => $e->getMessage()], |
|
200 | + $e->getCode() |
|
201 | + ); |
|
202 | + } catch (\Exception $e) { |
|
203 | + return new JSONResponse( |
|
204 | + ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], |
|
205 | + Http::STATUS_BAD_REQUEST |
|
206 | + ); |
|
207 | + } |
|
208 | + |
|
209 | + $user = $this->userManager->get($shareWith); |
|
210 | + $recipientDisplayName = ''; |
|
211 | + if($user) { |
|
212 | + $recipientDisplayName = $user->getDisplayName(); |
|
213 | + } |
|
214 | + |
|
215 | + return new JSONResponse( |
|
216 | + ['recipientDisplayName' => $recipientDisplayName], |
|
217 | + Http::STATUS_CREATED); |
|
218 | + |
|
219 | + } |
|
220 | + |
|
221 | + /** |
|
222 | + * receive notification about existing share |
|
223 | + * |
|
224 | + * @NoCSRFRequired |
|
225 | + * @PublicPage |
|
226 | + * @BruteForceProtection(action=receiveFederatedShareNotification) |
|
227 | + * |
|
228 | + * @param string $notificationType (notification type, e.g. SHARE_ACCEPTED) |
|
229 | + * @param string $resourceType (calendar, file, contact,...) |
|
230 | + * @param string $providerId id of the share |
|
231 | + * @param array $notification the actual payload of the notification |
|
232 | + * @return JSONResponse |
|
233 | + */ |
|
234 | + public function receiveNotification($notificationType, $resourceType, $providerId, array $notification) { |
|
235 | + |
|
236 | + // check if all required parameters are set |
|
237 | + if ($notificationType === null || |
|
238 | + $resourceType === null || |
|
239 | + $providerId === null || |
|
240 | + !is_array($notification) |
|
241 | + ) { |
|
242 | + return new JSONResponse( |
|
243 | + ['message' => 'Missing arguments'], |
|
244 | + Http::STATUS_BAD_REQUEST |
|
245 | + ); |
|
246 | + } |
|
247 | + |
|
248 | + try { |
|
249 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); |
|
250 | + $result = $provider->notificationReceived($notificationType, $providerId, $notification); |
|
251 | + } catch (ProviderDoesNotExistsException $e) { |
|
252 | + return new JSONResponse( |
|
253 | + ['message' => $e->getMessage()], |
|
254 | + Http::STATUS_BAD_REQUEST |
|
255 | + ); |
|
256 | + } catch (ShareNotFound $e) { |
|
257 | + return new JSONResponse( |
|
258 | + ['message' => $e->getMessage()], |
|
259 | + Http::STATUS_BAD_REQUEST |
|
260 | + ); |
|
261 | + } catch (ActionNotSupportedException $e) { |
|
262 | + return new JSONResponse( |
|
263 | + ['message' => $e->getMessage()], |
|
264 | + Http::STATUS_NOT_IMPLEMENTED |
|
265 | + ); |
|
266 | + } catch (BadRequestException $e) { |
|
267 | + return new JSONResponse($e->getReturnMessage(), Http::STATUS_BAD_REQUEST); |
|
268 | + } catch (AuthenticationFailedException $e) { |
|
269 | + return new JSONResponse(["message" => "RESOURCE_NOT_FOUND"], Http::STATUS_FORBIDDEN); |
|
270 | + } |
|
271 | + catch (\Exception $e) { |
|
272 | + return new JSONResponse( |
|
273 | + ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], |
|
274 | + Http::STATUS_BAD_REQUEST |
|
275 | + ); |
|
276 | + } |
|
277 | + |
|
278 | + return new JSONResponse($result,Http::STATUS_CREATED); |
|
279 | + |
|
280 | + } |
|
281 | + |
|
282 | + /** |
|
283 | + * map login name to internal LDAP UID if a LDAP backend is in use |
|
284 | + * |
|
285 | + * @param string $uid |
|
286 | + * @return string mixed |
|
287 | + */ |
|
288 | + private function mapUid($uid) { |
|
289 | + // FIXME this should be a method in the user management instead |
|
290 | + $this->logger->debug('shareWith before, ' . $uid, ['app' => $this->appName]); |
|
291 | + \OCP\Util::emitHook( |
|
292 | + '\OCA\Files_Sharing\API\Server2Server', |
|
293 | + 'preLoginNameUsedAsUserName', |
|
294 | + ['uid' => &$uid] |
|
295 | + ); |
|
296 | + $this->logger->debug('shareWith after, ' . $uid, ['app' => $this->appName]); |
|
297 | + |
|
298 | + return $uid; |
|
299 | + } |
|
300 | 300 | |
301 | 301 | } |
@@ -12,10 +12,10 @@ discard block |
||
12 | 12 | */ ?> |
13 | 13 | <input type="hidden" name="permissions" value="" id="permissions"> |
14 | 14 | <input type="hidden" id="free_space" value="<?php isset($_['freeSpace']) ? p($_['freeSpace']) : '' ?>"> |
15 | - <?php if(isset($_['dirToken'])):?> |
|
15 | + <?php if (isset($_['dirToken'])):?> |
|
16 | 16 | <input type="hidden" id="publicUploadRequestToken" name="requesttoken" value="<?php p($_['requesttoken']) ?>" /> |
17 | 17 | <input type="hidden" id="dirToken" name="dirToken" value="<?php p($_['dirToken']) ?>" /> |
18 | - <?php endif;?> |
|
18 | + <?php endif; ?> |
|
19 | 19 | <input type="hidden" class="max_human_file_size" |
20 | 20 | value="(max <?php isset($_['uploadMaxHumanFilesize']) ? p($_['uploadMaxHumanFilesize']) : ''; ?>)"> |
21 | 21 | </div> |
@@ -77,6 +77,6 @@ discard block |
||
77 | 77 | <div id="editor"></div><!-- FIXME Do not use this div in your app! It is deprecated and will be removed in the future! --> |
78 | 78 | <div id="uploadsize-message" title="<?php p($l->t('Upload too large'))?>"> |
79 | 79 | <p> |
80 | - <?php p($l->t('The files you are trying to upload exceed the maximum size for file uploads on this server.'));?> |
|
80 | + <?php p($l->t('The files you are trying to upload exceed the maximum size for file uploads on this server.')); ?> |
|
81 | 81 | </p> |
82 | 82 | </div> |
@@ -51,275 +51,275 @@ |
||
51 | 51 | |
52 | 52 | class Scan extends Base { |
53 | 53 | |
54 | - /** @var IUserManager $userManager */ |
|
55 | - private $userManager; |
|
56 | - /** @var float */ |
|
57 | - protected $execTime = 0; |
|
58 | - /** @var int */ |
|
59 | - protected $foldersCounter = 0; |
|
60 | - /** @var int */ |
|
61 | - protected $filesCounter = 0; |
|
62 | - |
|
63 | - public function __construct(IUserManager $userManager) { |
|
64 | - $this->userManager = $userManager; |
|
65 | - parent::__construct(); |
|
66 | - } |
|
67 | - |
|
68 | - protected function configure() { |
|
69 | - parent::configure(); |
|
70 | - |
|
71 | - $this |
|
72 | - ->setName('files:scan') |
|
73 | - ->setDescription('rescan filesystem') |
|
74 | - ->addArgument( |
|
75 | - 'user_id', |
|
76 | - InputArgument::OPTIONAL | InputArgument::IS_ARRAY, |
|
77 | - 'will rescan all files of the given user(s)' |
|
78 | - ) |
|
79 | - ->addOption( |
|
80 | - 'path', |
|
81 | - 'p', |
|
82 | - InputArgument::OPTIONAL, |
|
83 | - 'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored' |
|
84 | - ) |
|
85 | - ->addOption( |
|
86 | - 'all', |
|
87 | - null, |
|
88 | - InputOption::VALUE_NONE, |
|
89 | - 'will rescan all files of all known users' |
|
90 | - )->addOption( |
|
91 | - 'unscanned', |
|
92 | - null, |
|
93 | - InputOption::VALUE_NONE, |
|
94 | - 'only scan files which are marked as not fully scanned' |
|
95 | - )->addOption( |
|
96 | - 'shallow', |
|
97 | - null, |
|
98 | - InputOption::VALUE_NONE, |
|
99 | - 'do not scan folders recursively' |
|
100 | - )->addOption( |
|
101 | - 'home-only', |
|
102 | - null, |
|
103 | - InputOption::VALUE_NONE, |
|
104 | - 'only scan the home storage, ignoring any mounted external storage or share' |
|
105 | - ); |
|
106 | - } |
|
107 | - |
|
108 | - public function checkScanWarning($fullPath, OutputInterface $output) { |
|
109 | - $normalizedPath = basename(\OC\Files\Filesystem::normalizePath($fullPath)); |
|
110 | - $path = basename($fullPath); |
|
111 | - |
|
112 | - if ($normalizedPath !== $path) { |
|
113 | - $output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>'); |
|
114 | - } |
|
115 | - } |
|
116 | - |
|
117 | - protected function scanFiles($user, $path, OutputInterface $output, $backgroundScan = false, $recursive = true, $homeOnly = false) { |
|
118 | - $connection = $this->reconnectToDatabase($output); |
|
119 | - $scanner = new \OC\Files\Utils\Scanner($user, $connection, \OC::$server->query(IEventDispatcher::class), \OC::$server->getLogger()); |
|
120 | - |
|
121 | - # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception |
|
122 | - |
|
123 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
|
124 | - $output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); |
|
125 | - ++$this->filesCounter; |
|
126 | - $this->abortIfInterrupted(); |
|
127 | - }); |
|
128 | - |
|
129 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
|
130 | - $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); |
|
131 | - ++$this->foldersCounter; |
|
132 | - $this->abortIfInterrupted(); |
|
133 | - }); |
|
134 | - |
|
135 | - $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) { |
|
136 | - $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE); |
|
137 | - }); |
|
138 | - |
|
139 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
|
140 | - $this->checkScanWarning($path, $output); |
|
141 | - }); |
|
142 | - |
|
143 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
|
144 | - $this->checkScanWarning($path, $output); |
|
145 | - }); |
|
146 | - |
|
147 | - try { |
|
148 | - if ($backgroundScan) { |
|
149 | - $scanner->backgroundScan($path); |
|
150 | - } else { |
|
151 | - $scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null); |
|
152 | - } |
|
153 | - } catch (ForbiddenException $e) { |
|
154 | - $output->writeln("<error>Home storage for user $user not writable</error>"); |
|
155 | - $output->writeln('Make sure you\'re running the scan command only as the user the web server runs as'); |
|
156 | - } catch (InterruptedException $e) { |
|
157 | - # exit the function if ctrl-c has been pressed |
|
158 | - $output->writeln('Interrupted by user'); |
|
159 | - } catch (NotFoundException $e) { |
|
160 | - $output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>'); |
|
161 | - } catch (\Exception $e) { |
|
162 | - $output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>'); |
|
163 | - $output->writeln('<error>' . $e->getTraceAsString() . '</error>'); |
|
164 | - } |
|
165 | - } |
|
166 | - |
|
167 | - public function filterHomeMount(IMountPoint $mountPoint) { |
|
168 | - // any mountpoint inside '/$user/files/' |
|
169 | - return substr_count($mountPoint->getMountPoint(), '/') <= 3; |
|
170 | - } |
|
171 | - |
|
172 | - protected function execute(InputInterface $input, OutputInterface $output) { |
|
173 | - $inputPath = $input->getOption('path'); |
|
174 | - if ($inputPath) { |
|
175 | - $inputPath = '/' . trim($inputPath, '/'); |
|
176 | - list(, $user,) = explode('/', $inputPath, 3); |
|
177 | - $users = [$user]; |
|
178 | - } else if ($input->getOption('all')) { |
|
179 | - $users = $this->userManager->search(''); |
|
180 | - } else { |
|
181 | - $users = $input->getArgument('user_id'); |
|
182 | - } |
|
183 | - |
|
184 | - # restrict the verbosity level to VERBOSITY_VERBOSE |
|
185 | - if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) { |
|
186 | - $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); |
|
187 | - } |
|
188 | - |
|
189 | - # check quantity of users to be process and show it on the command line |
|
190 | - $users_total = count($users); |
|
191 | - if ($users_total === 0) { |
|
192 | - $output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>'); |
|
193 | - return; |
|
194 | - } |
|
195 | - |
|
196 | - $this->initTools(); |
|
197 | - |
|
198 | - $user_count = 0; |
|
199 | - foreach ($users as $user) { |
|
200 | - if (is_object($user)) { |
|
201 | - $user = $user->getUID(); |
|
202 | - } |
|
203 | - $path = $inputPath ? $inputPath : '/' . $user; |
|
204 | - ++$user_count; |
|
205 | - if ($this->userManager->userExists($user)) { |
|
206 | - $output->writeln("Starting scan for user $user_count out of $users_total ($user)"); |
|
207 | - $this->scanFiles($user, $path, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only')); |
|
208 | - $output->writeln('', OutputInterface::VERBOSITY_VERBOSE); |
|
209 | - } else { |
|
210 | - $output->writeln("<error>Unknown user $user_count $user</error>"); |
|
211 | - $output->writeln('', OutputInterface::VERBOSITY_VERBOSE); |
|
212 | - } |
|
213 | - |
|
214 | - try { |
|
215 | - $this->abortIfInterrupted(); |
|
216 | - } catch (InterruptedException $e) { |
|
217 | - break; |
|
218 | - } |
|
219 | - } |
|
220 | - |
|
221 | - $this->presentStats($output); |
|
222 | - } |
|
223 | - |
|
224 | - /** |
|
225 | - * Initialises some useful tools for the Command |
|
226 | - */ |
|
227 | - protected function initTools() { |
|
228 | - // Start the timer |
|
229 | - $this->execTime = -microtime(true); |
|
230 | - // Convert PHP errors to exceptions |
|
231 | - set_error_handler([$this, 'exceptionErrorHandler'], E_ALL); |
|
232 | - } |
|
233 | - |
|
234 | - /** |
|
235 | - * Processes PHP errors as exceptions in order to be able to keep track of problems |
|
236 | - * |
|
237 | - * @see https://secure.php.net/manual/en/function.set-error-handler.php |
|
238 | - * |
|
239 | - * @param int $severity the level of the error raised |
|
240 | - * @param string $message |
|
241 | - * @param string $file the filename that the error was raised in |
|
242 | - * @param int $line the line number the error was raised |
|
243 | - * |
|
244 | - * @throws \ErrorException |
|
245 | - */ |
|
246 | - public function exceptionErrorHandler($severity, $message, $file, $line) { |
|
247 | - if (!(error_reporting() & $severity)) { |
|
248 | - // This error code is not included in error_reporting |
|
249 | - return; |
|
250 | - } |
|
251 | - throw new \ErrorException($message, 0, $severity, $file, $line); |
|
252 | - } |
|
253 | - |
|
254 | - /** |
|
255 | - * @param OutputInterface $output |
|
256 | - */ |
|
257 | - protected function presentStats(OutputInterface $output) { |
|
258 | - // Stop the timer |
|
259 | - $this->execTime += microtime(true); |
|
260 | - |
|
261 | - $headers = [ |
|
262 | - 'Folders', 'Files', 'Elapsed time' |
|
263 | - ]; |
|
264 | - |
|
265 | - $this->showSummary($headers, null, $output); |
|
266 | - } |
|
267 | - |
|
268 | - /** |
|
269 | - * Shows a summary of operations |
|
270 | - * |
|
271 | - * @param string[] $headers |
|
272 | - * @param string[] $rows |
|
273 | - * @param OutputInterface $output |
|
274 | - */ |
|
275 | - protected function showSummary($headers, $rows, OutputInterface $output) { |
|
276 | - $niceDate = $this->formatExecTime(); |
|
277 | - if (!$rows) { |
|
278 | - $rows = [ |
|
279 | - $this->foldersCounter, |
|
280 | - $this->filesCounter, |
|
281 | - $niceDate, |
|
282 | - ]; |
|
283 | - } |
|
284 | - $table = new Table($output); |
|
285 | - $table |
|
286 | - ->setHeaders($headers) |
|
287 | - ->setRows([$rows]); |
|
288 | - $table->render(); |
|
289 | - } |
|
290 | - |
|
291 | - |
|
292 | - /** |
|
293 | - * Formats microtime into a human readable format |
|
294 | - * |
|
295 | - * @return string |
|
296 | - */ |
|
297 | - protected function formatExecTime() { |
|
298 | - $secs = round($this->execTime); |
|
299 | - # convert seconds into HH:MM:SS form |
|
300 | - return sprintf('%02d:%02d:%02d', ($secs/3600), ($secs/60%60), $secs%60); |
|
301 | - } |
|
302 | - |
|
303 | - /** |
|
304 | - * @return \OCP\IDBConnection |
|
305 | - */ |
|
306 | - protected function reconnectToDatabase(OutputInterface $output) { |
|
307 | - /** @var Connection | IDBConnection $connection */ |
|
308 | - $connection = \OC::$server->getDatabaseConnection(); |
|
309 | - try { |
|
310 | - $connection->close(); |
|
311 | - } catch (\Exception $ex) { |
|
312 | - $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>"); |
|
313 | - } |
|
314 | - while (!$connection->isConnected()) { |
|
315 | - try { |
|
316 | - $connection->connect(); |
|
317 | - } catch (\Exception $ex) { |
|
318 | - $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>"); |
|
319 | - sleep(60); |
|
320 | - } |
|
321 | - } |
|
322 | - return $connection; |
|
323 | - } |
|
54 | + /** @var IUserManager $userManager */ |
|
55 | + private $userManager; |
|
56 | + /** @var float */ |
|
57 | + protected $execTime = 0; |
|
58 | + /** @var int */ |
|
59 | + protected $foldersCounter = 0; |
|
60 | + /** @var int */ |
|
61 | + protected $filesCounter = 0; |
|
62 | + |
|
63 | + public function __construct(IUserManager $userManager) { |
|
64 | + $this->userManager = $userManager; |
|
65 | + parent::__construct(); |
|
66 | + } |
|
67 | + |
|
68 | + protected function configure() { |
|
69 | + parent::configure(); |
|
70 | + |
|
71 | + $this |
|
72 | + ->setName('files:scan') |
|
73 | + ->setDescription('rescan filesystem') |
|
74 | + ->addArgument( |
|
75 | + 'user_id', |
|
76 | + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, |
|
77 | + 'will rescan all files of the given user(s)' |
|
78 | + ) |
|
79 | + ->addOption( |
|
80 | + 'path', |
|
81 | + 'p', |
|
82 | + InputArgument::OPTIONAL, |
|
83 | + 'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored' |
|
84 | + ) |
|
85 | + ->addOption( |
|
86 | + 'all', |
|
87 | + null, |
|
88 | + InputOption::VALUE_NONE, |
|
89 | + 'will rescan all files of all known users' |
|
90 | + )->addOption( |
|
91 | + 'unscanned', |
|
92 | + null, |
|
93 | + InputOption::VALUE_NONE, |
|
94 | + 'only scan files which are marked as not fully scanned' |
|
95 | + )->addOption( |
|
96 | + 'shallow', |
|
97 | + null, |
|
98 | + InputOption::VALUE_NONE, |
|
99 | + 'do not scan folders recursively' |
|
100 | + )->addOption( |
|
101 | + 'home-only', |
|
102 | + null, |
|
103 | + InputOption::VALUE_NONE, |
|
104 | + 'only scan the home storage, ignoring any mounted external storage or share' |
|
105 | + ); |
|
106 | + } |
|
107 | + |
|
108 | + public function checkScanWarning($fullPath, OutputInterface $output) { |
|
109 | + $normalizedPath = basename(\OC\Files\Filesystem::normalizePath($fullPath)); |
|
110 | + $path = basename($fullPath); |
|
111 | + |
|
112 | + if ($normalizedPath !== $path) { |
|
113 | + $output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>'); |
|
114 | + } |
|
115 | + } |
|
116 | + |
|
117 | + protected function scanFiles($user, $path, OutputInterface $output, $backgroundScan = false, $recursive = true, $homeOnly = false) { |
|
118 | + $connection = $this->reconnectToDatabase($output); |
|
119 | + $scanner = new \OC\Files\Utils\Scanner($user, $connection, \OC::$server->query(IEventDispatcher::class), \OC::$server->getLogger()); |
|
120 | + |
|
121 | + # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception |
|
122 | + |
|
123 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
|
124 | + $output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); |
|
125 | + ++$this->filesCounter; |
|
126 | + $this->abortIfInterrupted(); |
|
127 | + }); |
|
128 | + |
|
129 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
|
130 | + $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); |
|
131 | + ++$this->foldersCounter; |
|
132 | + $this->abortIfInterrupted(); |
|
133 | + }); |
|
134 | + |
|
135 | + $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) { |
|
136 | + $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE); |
|
137 | + }); |
|
138 | + |
|
139 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
|
140 | + $this->checkScanWarning($path, $output); |
|
141 | + }); |
|
142 | + |
|
143 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
|
144 | + $this->checkScanWarning($path, $output); |
|
145 | + }); |
|
146 | + |
|
147 | + try { |
|
148 | + if ($backgroundScan) { |
|
149 | + $scanner->backgroundScan($path); |
|
150 | + } else { |
|
151 | + $scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null); |
|
152 | + } |
|
153 | + } catch (ForbiddenException $e) { |
|
154 | + $output->writeln("<error>Home storage for user $user not writable</error>"); |
|
155 | + $output->writeln('Make sure you\'re running the scan command only as the user the web server runs as'); |
|
156 | + } catch (InterruptedException $e) { |
|
157 | + # exit the function if ctrl-c has been pressed |
|
158 | + $output->writeln('Interrupted by user'); |
|
159 | + } catch (NotFoundException $e) { |
|
160 | + $output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>'); |
|
161 | + } catch (\Exception $e) { |
|
162 | + $output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>'); |
|
163 | + $output->writeln('<error>' . $e->getTraceAsString() . '</error>'); |
|
164 | + } |
|
165 | + } |
|
166 | + |
|
167 | + public function filterHomeMount(IMountPoint $mountPoint) { |
|
168 | + // any mountpoint inside '/$user/files/' |
|
169 | + return substr_count($mountPoint->getMountPoint(), '/') <= 3; |
|
170 | + } |
|
171 | + |
|
172 | + protected function execute(InputInterface $input, OutputInterface $output) { |
|
173 | + $inputPath = $input->getOption('path'); |
|
174 | + if ($inputPath) { |
|
175 | + $inputPath = '/' . trim($inputPath, '/'); |
|
176 | + list(, $user,) = explode('/', $inputPath, 3); |
|
177 | + $users = [$user]; |
|
178 | + } else if ($input->getOption('all')) { |
|
179 | + $users = $this->userManager->search(''); |
|
180 | + } else { |
|
181 | + $users = $input->getArgument('user_id'); |
|
182 | + } |
|
183 | + |
|
184 | + # restrict the verbosity level to VERBOSITY_VERBOSE |
|
185 | + if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) { |
|
186 | + $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); |
|
187 | + } |
|
188 | + |
|
189 | + # check quantity of users to be process and show it on the command line |
|
190 | + $users_total = count($users); |
|
191 | + if ($users_total === 0) { |
|
192 | + $output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>'); |
|
193 | + return; |
|
194 | + } |
|
195 | + |
|
196 | + $this->initTools(); |
|
197 | + |
|
198 | + $user_count = 0; |
|
199 | + foreach ($users as $user) { |
|
200 | + if (is_object($user)) { |
|
201 | + $user = $user->getUID(); |
|
202 | + } |
|
203 | + $path = $inputPath ? $inputPath : '/' . $user; |
|
204 | + ++$user_count; |
|
205 | + if ($this->userManager->userExists($user)) { |
|
206 | + $output->writeln("Starting scan for user $user_count out of $users_total ($user)"); |
|
207 | + $this->scanFiles($user, $path, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only')); |
|
208 | + $output->writeln('', OutputInterface::VERBOSITY_VERBOSE); |
|
209 | + } else { |
|
210 | + $output->writeln("<error>Unknown user $user_count $user</error>"); |
|
211 | + $output->writeln('', OutputInterface::VERBOSITY_VERBOSE); |
|
212 | + } |
|
213 | + |
|
214 | + try { |
|
215 | + $this->abortIfInterrupted(); |
|
216 | + } catch (InterruptedException $e) { |
|
217 | + break; |
|
218 | + } |
|
219 | + } |
|
220 | + |
|
221 | + $this->presentStats($output); |
|
222 | + } |
|
223 | + |
|
224 | + /** |
|
225 | + * Initialises some useful tools for the Command |
|
226 | + */ |
|
227 | + protected function initTools() { |
|
228 | + // Start the timer |
|
229 | + $this->execTime = -microtime(true); |
|
230 | + // Convert PHP errors to exceptions |
|
231 | + set_error_handler([$this, 'exceptionErrorHandler'], E_ALL); |
|
232 | + } |
|
233 | + |
|
234 | + /** |
|
235 | + * Processes PHP errors as exceptions in order to be able to keep track of problems |
|
236 | + * |
|
237 | + * @see https://secure.php.net/manual/en/function.set-error-handler.php |
|
238 | + * |
|
239 | + * @param int $severity the level of the error raised |
|
240 | + * @param string $message |
|
241 | + * @param string $file the filename that the error was raised in |
|
242 | + * @param int $line the line number the error was raised |
|
243 | + * |
|
244 | + * @throws \ErrorException |
|
245 | + */ |
|
246 | + public function exceptionErrorHandler($severity, $message, $file, $line) { |
|
247 | + if (!(error_reporting() & $severity)) { |
|
248 | + // This error code is not included in error_reporting |
|
249 | + return; |
|
250 | + } |
|
251 | + throw new \ErrorException($message, 0, $severity, $file, $line); |
|
252 | + } |
|
253 | + |
|
254 | + /** |
|
255 | + * @param OutputInterface $output |
|
256 | + */ |
|
257 | + protected function presentStats(OutputInterface $output) { |
|
258 | + // Stop the timer |
|
259 | + $this->execTime += microtime(true); |
|
260 | + |
|
261 | + $headers = [ |
|
262 | + 'Folders', 'Files', 'Elapsed time' |
|
263 | + ]; |
|
264 | + |
|
265 | + $this->showSummary($headers, null, $output); |
|
266 | + } |
|
267 | + |
|
268 | + /** |
|
269 | + * Shows a summary of operations |
|
270 | + * |
|
271 | + * @param string[] $headers |
|
272 | + * @param string[] $rows |
|
273 | + * @param OutputInterface $output |
|
274 | + */ |
|
275 | + protected function showSummary($headers, $rows, OutputInterface $output) { |
|
276 | + $niceDate = $this->formatExecTime(); |
|
277 | + if (!$rows) { |
|
278 | + $rows = [ |
|
279 | + $this->foldersCounter, |
|
280 | + $this->filesCounter, |
|
281 | + $niceDate, |
|
282 | + ]; |
|
283 | + } |
|
284 | + $table = new Table($output); |
|
285 | + $table |
|
286 | + ->setHeaders($headers) |
|
287 | + ->setRows([$rows]); |
|
288 | + $table->render(); |
|
289 | + } |
|
290 | + |
|
291 | + |
|
292 | + /** |
|
293 | + * Formats microtime into a human readable format |
|
294 | + * |
|
295 | + * @return string |
|
296 | + */ |
|
297 | + protected function formatExecTime() { |
|
298 | + $secs = round($this->execTime); |
|
299 | + # convert seconds into HH:MM:SS form |
|
300 | + return sprintf('%02d:%02d:%02d', ($secs/3600), ($secs/60%60), $secs%60); |
|
301 | + } |
|
302 | + |
|
303 | + /** |
|
304 | + * @return \OCP\IDBConnection |
|
305 | + */ |
|
306 | + protected function reconnectToDatabase(OutputInterface $output) { |
|
307 | + /** @var Connection | IDBConnection $connection */ |
|
308 | + $connection = \OC::$server->getDatabaseConnection(); |
|
309 | + try { |
|
310 | + $connection->close(); |
|
311 | + } catch (\Exception $ex) { |
|
312 | + $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>"); |
|
313 | + } |
|
314 | + while (!$connection->isConnected()) { |
|
315 | + try { |
|
316 | + $connection->connect(); |
|
317 | + } catch (\Exception $ex) { |
|
318 | + $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>"); |
|
319 | + sleep(60); |
|
320 | + } |
|
321 | + } |
|
322 | + return $connection; |
|
323 | + } |
|
324 | 324 | |
325 | 325 | } |
@@ -110,7 +110,7 @@ discard block |
||
110 | 110 | $path = basename($fullPath); |
111 | 111 | |
112 | 112 | if ($normalizedPath !== $path) { |
113 | - $output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>'); |
|
113 | + $output->writeln("\t<error>Entry \"".$fullPath.'" will not be accessible due to incompatible encoding</error>'); |
|
114 | 114 | } |
115 | 115 | } |
116 | 116 | |
@@ -120,27 +120,27 @@ discard block |
||
120 | 120 | |
121 | 121 | # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception |
122 | 122 | |
123 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
|
123 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function($path) use ($output) { |
|
124 | 124 | $output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); |
125 | 125 | ++$this->filesCounter; |
126 | 126 | $this->abortIfInterrupted(); |
127 | 127 | }); |
128 | 128 | |
129 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
|
129 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function($path) use ($output) { |
|
130 | 130 | $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); |
131 | 131 | ++$this->foldersCounter; |
132 | 132 | $this->abortIfInterrupted(); |
133 | 133 | }); |
134 | 134 | |
135 | - $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) { |
|
136 | - $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE); |
|
135 | + $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function(StorageNotAvailableException $e) use ($output) { |
|
136 | + $output->writeln('Error while scanning, storage not available ('.$e->getMessage().')', OutputInterface::VERBOSITY_VERBOSE); |
|
137 | 137 | }); |
138 | 138 | |
139 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
|
139 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function($path) use ($output) { |
|
140 | 140 | $this->checkScanWarning($path, $output); |
141 | 141 | }); |
142 | 142 | |
143 | - $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
|
143 | + $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function($path) use ($output) { |
|
144 | 144 | $this->checkScanWarning($path, $output); |
145 | 145 | }); |
146 | 146 | |
@@ -157,10 +157,10 @@ discard block |
||
157 | 157 | # exit the function if ctrl-c has been pressed |
158 | 158 | $output->writeln('Interrupted by user'); |
159 | 159 | } catch (NotFoundException $e) { |
160 | - $output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>'); |
|
160 | + $output->writeln('<error>Path not found: '.$e->getMessage().'</error>'); |
|
161 | 161 | } catch (\Exception $e) { |
162 | - $output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>'); |
|
163 | - $output->writeln('<error>' . $e->getTraceAsString() . '</error>'); |
|
162 | + $output->writeln('<error>Exception during scan: '.$e->getMessage().'</error>'); |
|
163 | + $output->writeln('<error>'.$e->getTraceAsString().'</error>'); |
|
164 | 164 | } |
165 | 165 | } |
166 | 166 | |
@@ -172,7 +172,7 @@ discard block |
||
172 | 172 | protected function execute(InputInterface $input, OutputInterface $output) { |
173 | 173 | $inputPath = $input->getOption('path'); |
174 | 174 | if ($inputPath) { |
175 | - $inputPath = '/' . trim($inputPath, '/'); |
|
175 | + $inputPath = '/'.trim($inputPath, '/'); |
|
176 | 176 | list(, $user,) = explode('/', $inputPath, 3); |
177 | 177 | $users = [$user]; |
178 | 178 | } else if ($input->getOption('all')) { |
@@ -200,7 +200,7 @@ discard block |
||
200 | 200 | if (is_object($user)) { |
201 | 201 | $user = $user->getUID(); |
202 | 202 | } |
203 | - $path = $inputPath ? $inputPath : '/' . $user; |
|
203 | + $path = $inputPath ? $inputPath : '/'.$user; |
|
204 | 204 | ++$user_count; |
205 | 205 | if ($this->userManager->userExists($user)) { |
206 | 206 | $output->writeln("Starting scan for user $user_count out of $users_total ($user)"); |
@@ -297,7 +297,7 @@ discard block |
||
297 | 297 | protected function formatExecTime() { |
298 | 298 | $secs = round($this->execTime); |
299 | 299 | # convert seconds into HH:MM:SS form |
300 | - return sprintf('%02d:%02d:%02d', ($secs/3600), ($secs/60%60), $secs%60); |
|
300 | + return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60); |
|
301 | 301 | } |
302 | 302 | |
303 | 303 | /** |
@@ -44,137 +44,137 @@ |
||
44 | 44 | use OCP\Security\ISecureRandom; |
45 | 45 | |
46 | 46 | class OauthApiController extends Controller { |
47 | - /** @var AccessTokenMapper */ |
|
48 | - private $accessTokenMapper; |
|
49 | - /** @var ClientMapper */ |
|
50 | - private $clientMapper; |
|
51 | - /** @var ICrypto */ |
|
52 | - private $crypto; |
|
53 | - /** @var TokenProvider */ |
|
54 | - private $tokenProvider; |
|
55 | - /** @var ISecureRandom */ |
|
56 | - private $secureRandom; |
|
57 | - /** @var ITimeFactory */ |
|
58 | - private $time; |
|
59 | - /** @var Throttler */ |
|
60 | - private $throttler; |
|
61 | - |
|
62 | - public function __construct(string $appName, |
|
63 | - IRequest $request, |
|
64 | - ICrypto $crypto, |
|
65 | - AccessTokenMapper $accessTokenMapper, |
|
66 | - ClientMapper $clientMapper, |
|
67 | - TokenProvider $tokenProvider, |
|
68 | - ISecureRandom $secureRandom, |
|
69 | - ITimeFactory $time, |
|
70 | - Throttler $throttler) { |
|
71 | - parent::__construct($appName, $request); |
|
72 | - $this->crypto = $crypto; |
|
73 | - $this->accessTokenMapper = $accessTokenMapper; |
|
74 | - $this->clientMapper = $clientMapper; |
|
75 | - $this->tokenProvider = $tokenProvider; |
|
76 | - $this->secureRandom = $secureRandom; |
|
77 | - $this->time = $time; |
|
78 | - $this->throttler = $throttler; |
|
79 | - } |
|
80 | - |
|
81 | - /** |
|
82 | - * @PublicPage |
|
83 | - * @NoCSRFRequired |
|
84 | - * |
|
85 | - * @param string $grant_type |
|
86 | - * @param string $code |
|
87 | - * @param string $refresh_token |
|
88 | - * @param string $client_id |
|
89 | - * @param string $client_secret |
|
90 | - * @return JSONResponse |
|
91 | - */ |
|
92 | - public function getToken($grant_type, $code, $refresh_token, $client_id, $client_secret): JSONResponse { |
|
93 | - |
|
94 | - // We only handle two types |
|
95 | - if ($grant_type !== 'authorization_code' && $grant_type !== 'refresh_token') { |
|
96 | - return new JSONResponse([ |
|
97 | - 'error' => 'invalid_grant', |
|
98 | - ], Http::STATUS_BAD_REQUEST); |
|
99 | - } |
|
100 | - |
|
101 | - // We handle the initial and refresh tokens the same way |
|
102 | - if ($grant_type === 'refresh_token') { |
|
103 | - $code = $refresh_token; |
|
104 | - } |
|
105 | - |
|
106 | - try { |
|
107 | - $accessToken = $this->accessTokenMapper->getByCode($code); |
|
108 | - } catch (AccessTokenNotFoundException $e) { |
|
109 | - return new JSONResponse([ |
|
110 | - 'error' => 'invalid_request', |
|
111 | - ], Http::STATUS_BAD_REQUEST); |
|
112 | - } |
|
113 | - |
|
114 | - try { |
|
115 | - $client = $this->clientMapper->getByUid($accessToken->getClientId()); |
|
116 | - } catch (ClientNotFoundException $e) { |
|
117 | - return new JSONResponse([ |
|
118 | - 'error' => 'invalid_request', |
|
119 | - ], Http::STATUS_BAD_REQUEST); |
|
120 | - } |
|
121 | - |
|
122 | - if (isset($this->request->server['PHP_AUTH_USER'])) { |
|
123 | - $client_id = $this->request->server['PHP_AUTH_USER']; |
|
124 | - $client_secret = $this->request->server['PHP_AUTH_PW']; |
|
125 | - } |
|
126 | - |
|
127 | - // The client id and secret must match. Else we don't provide an access token! |
|
128 | - if ($client->getClientIdentifier() !== $client_id || $client->getSecret() !== $client_secret) { |
|
129 | - return new JSONResponse([ |
|
130 | - 'error' => 'invalid_client', |
|
131 | - ], Http::STATUS_BAD_REQUEST); |
|
132 | - } |
|
133 | - |
|
134 | - $decryptedToken = $this->crypto->decrypt($accessToken->getEncryptedToken(), $code); |
|
135 | - |
|
136 | - // Obtain the appToken assoicated |
|
137 | - try { |
|
138 | - $appToken = $this->tokenProvider->getTokenById($accessToken->getTokenId()); |
|
139 | - } catch (ExpiredTokenException $e) { |
|
140 | - $appToken = $e->getToken(); |
|
141 | - } catch (InvalidTokenException $e) { |
|
142 | - //We can't do anything... |
|
143 | - $this->accessTokenMapper->delete($accessToken); |
|
144 | - return new JSONResponse([ |
|
145 | - 'error' => 'invalid_request', |
|
146 | - ], Http::STATUS_BAD_REQUEST); |
|
147 | - } |
|
148 | - |
|
149 | - // Rotate the apptoken (so the old one becomes invalid basically) |
|
150 | - $newToken = $this->secureRandom->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); |
|
151 | - |
|
152 | - $appToken = $this->tokenProvider->rotate( |
|
153 | - $appToken, |
|
154 | - $decryptedToken, |
|
155 | - $newToken |
|
156 | - ); |
|
157 | - |
|
158 | - // Expiration is in 1 hour again |
|
159 | - $appToken->setExpires($this->time->getTime() + 3600); |
|
160 | - $this->tokenProvider->updateToken($appToken); |
|
161 | - |
|
162 | - // Generate a new refresh token and encrypt the new apptoken in the DB |
|
163 | - $newCode = $this->secureRandom->generate(128, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); |
|
164 | - $accessToken->setHashedCode(hash('sha512', $newCode)); |
|
165 | - $accessToken->setEncryptedToken($this->crypto->encrypt($newToken, $newCode)); |
|
166 | - $this->accessTokenMapper->update($accessToken); |
|
167 | - |
|
168 | - $this->throttler->resetDelay($this->request->getRemoteAddress(), 'login', ['user' => $appToken->getUID()]); |
|
169 | - |
|
170 | - return new JSONResponse( |
|
171 | - [ |
|
172 | - 'access_token' => $newToken, |
|
173 | - 'token_type' => 'Bearer', |
|
174 | - 'expires_in' => 3600, |
|
175 | - 'refresh_token' => $newCode, |
|
176 | - 'user_id' => $appToken->getUID(), |
|
177 | - ] |
|
178 | - ); |
|
179 | - } |
|
47 | + /** @var AccessTokenMapper */ |
|
48 | + private $accessTokenMapper; |
|
49 | + /** @var ClientMapper */ |
|
50 | + private $clientMapper; |
|
51 | + /** @var ICrypto */ |
|
52 | + private $crypto; |
|
53 | + /** @var TokenProvider */ |
|
54 | + private $tokenProvider; |
|
55 | + /** @var ISecureRandom */ |
|
56 | + private $secureRandom; |
|
57 | + /** @var ITimeFactory */ |
|
58 | + private $time; |
|
59 | + /** @var Throttler */ |
|
60 | + private $throttler; |
|
61 | + |
|
62 | + public function __construct(string $appName, |
|
63 | + IRequest $request, |
|
64 | + ICrypto $crypto, |
|
65 | + AccessTokenMapper $accessTokenMapper, |
|
66 | + ClientMapper $clientMapper, |
|
67 | + TokenProvider $tokenProvider, |
|
68 | + ISecureRandom $secureRandom, |
|
69 | + ITimeFactory $time, |
|
70 | + Throttler $throttler) { |
|
71 | + parent::__construct($appName, $request); |
|
72 | + $this->crypto = $crypto; |
|
73 | + $this->accessTokenMapper = $accessTokenMapper; |
|
74 | + $this->clientMapper = $clientMapper; |
|
75 | + $this->tokenProvider = $tokenProvider; |
|
76 | + $this->secureRandom = $secureRandom; |
|
77 | + $this->time = $time; |
|
78 | + $this->throttler = $throttler; |
|
79 | + } |
|
80 | + |
|
81 | + /** |
|
82 | + * @PublicPage |
|
83 | + * @NoCSRFRequired |
|
84 | + * |
|
85 | + * @param string $grant_type |
|
86 | + * @param string $code |
|
87 | + * @param string $refresh_token |
|
88 | + * @param string $client_id |
|
89 | + * @param string $client_secret |
|
90 | + * @return JSONResponse |
|
91 | + */ |
|
92 | + public function getToken($grant_type, $code, $refresh_token, $client_id, $client_secret): JSONResponse { |
|
93 | + |
|
94 | + // We only handle two types |
|
95 | + if ($grant_type !== 'authorization_code' && $grant_type !== 'refresh_token') { |
|
96 | + return new JSONResponse([ |
|
97 | + 'error' => 'invalid_grant', |
|
98 | + ], Http::STATUS_BAD_REQUEST); |
|
99 | + } |
|
100 | + |
|
101 | + // We handle the initial and refresh tokens the same way |
|
102 | + if ($grant_type === 'refresh_token') { |
|
103 | + $code = $refresh_token; |
|
104 | + } |
|
105 | + |
|
106 | + try { |
|
107 | + $accessToken = $this->accessTokenMapper->getByCode($code); |
|
108 | + } catch (AccessTokenNotFoundException $e) { |
|
109 | + return new JSONResponse([ |
|
110 | + 'error' => 'invalid_request', |
|
111 | + ], Http::STATUS_BAD_REQUEST); |
|
112 | + } |
|
113 | + |
|
114 | + try { |
|
115 | + $client = $this->clientMapper->getByUid($accessToken->getClientId()); |
|
116 | + } catch (ClientNotFoundException $e) { |
|
117 | + return new JSONResponse([ |
|
118 | + 'error' => 'invalid_request', |
|
119 | + ], Http::STATUS_BAD_REQUEST); |
|
120 | + } |
|
121 | + |
|
122 | + if (isset($this->request->server['PHP_AUTH_USER'])) { |
|
123 | + $client_id = $this->request->server['PHP_AUTH_USER']; |
|
124 | + $client_secret = $this->request->server['PHP_AUTH_PW']; |
|
125 | + } |
|
126 | + |
|
127 | + // The client id and secret must match. Else we don't provide an access token! |
|
128 | + if ($client->getClientIdentifier() !== $client_id || $client->getSecret() !== $client_secret) { |
|
129 | + return new JSONResponse([ |
|
130 | + 'error' => 'invalid_client', |
|
131 | + ], Http::STATUS_BAD_REQUEST); |
|
132 | + } |
|
133 | + |
|
134 | + $decryptedToken = $this->crypto->decrypt($accessToken->getEncryptedToken(), $code); |
|
135 | + |
|
136 | + // Obtain the appToken assoicated |
|
137 | + try { |
|
138 | + $appToken = $this->tokenProvider->getTokenById($accessToken->getTokenId()); |
|
139 | + } catch (ExpiredTokenException $e) { |
|
140 | + $appToken = $e->getToken(); |
|
141 | + } catch (InvalidTokenException $e) { |
|
142 | + //We can't do anything... |
|
143 | + $this->accessTokenMapper->delete($accessToken); |
|
144 | + return new JSONResponse([ |
|
145 | + 'error' => 'invalid_request', |
|
146 | + ], Http::STATUS_BAD_REQUEST); |
|
147 | + } |
|
148 | + |
|
149 | + // Rotate the apptoken (so the old one becomes invalid basically) |
|
150 | + $newToken = $this->secureRandom->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); |
|
151 | + |
|
152 | + $appToken = $this->tokenProvider->rotate( |
|
153 | + $appToken, |
|
154 | + $decryptedToken, |
|
155 | + $newToken |
|
156 | + ); |
|
157 | + |
|
158 | + // Expiration is in 1 hour again |
|
159 | + $appToken->setExpires($this->time->getTime() + 3600); |
|
160 | + $this->tokenProvider->updateToken($appToken); |
|
161 | + |
|
162 | + // Generate a new refresh token and encrypt the new apptoken in the DB |
|
163 | + $newCode = $this->secureRandom->generate(128, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); |
|
164 | + $accessToken->setHashedCode(hash('sha512', $newCode)); |
|
165 | + $accessToken->setEncryptedToken($this->crypto->encrypt($newToken, $newCode)); |
|
166 | + $this->accessTokenMapper->update($accessToken); |
|
167 | + |
|
168 | + $this->throttler->resetDelay($this->request->getRemoteAddress(), 'login', ['user' => $appToken->getUID()]); |
|
169 | + |
|
170 | + return new JSONResponse( |
|
171 | + [ |
|
172 | + 'access_token' => $newToken, |
|
173 | + 'token_type' => 'Bearer', |
|
174 | + 'expires_in' => 3600, |
|
175 | + 'refresh_token' => $newCode, |
|
176 | + 'user_id' => $appToken->getUID(), |
|
177 | + ] |
|
178 | + ); |
|
179 | + } |
|
180 | 180 | } |
@@ -27,82 +27,82 @@ discard block |
||
27 | 27 | ?> |
28 | 28 | |
29 | 29 | <div class="section" id="shareAPI"> |
30 | - <h2><?php p($l->t('Sharing'));?></h2> |
|
30 | + <h2><?php p($l->t('Sharing')); ?></h2> |
|
31 | 31 | <a target="_blank" rel="noreferrer noopener" class="icon-info" |
32 | - title="<?php p($l->t('Open documentation'));?>" |
|
32 | + title="<?php p($l->t('Open documentation')); ?>" |
|
33 | 33 | href="<?php p(link_to_docs('admin-sharing')); ?>"></a> |
34 | - <p class="settings-hint"><?php p($l->t('As admin you can fine-tune the sharing behavior. Please see the documentation for more information.'));?></p> |
|
34 | + <p class="settings-hint"><?php p($l->t('As admin you can fine-tune the sharing behavior. Please see the documentation for more information.')); ?></p> |
|
35 | 35 | <p id="enable"> |
36 | 36 | <input type="checkbox" name="shareapi_enabled" id="shareAPIEnabled" class="checkbox" |
37 | 37 | value="1" <?php if ($_['shareAPIEnabled'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
38 | - <label for="shareAPIEnabled"><?php p($l->t('Allow apps to use the Share API'));?></label><br/> |
|
38 | + <label for="shareAPIEnabled"><?php p($l->t('Allow apps to use the Share API')); ?></label><br/> |
|
39 | 39 | </p> |
40 | 40 | |
41 | 41 | <p id="internalShareSettings" class="indent <?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
42 | 42 | <input type="checkbox" name="shareapi_default_internal_expire_date" id="shareapiDefaultInternalExpireDate" class="checkbox" |
43 | 43 | value="1" <?php if ($_['shareDefaultInternalExpireDateSet'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
44 | - <label for="shareapiDefaultInternalExpireDate"><?php p($l->t('Set default expiration date for shares'));?></label><br/> |
|
44 | + <label for="shareapiDefaultInternalExpireDate"><?php p($l->t('Set default expiration date for shares')); ?></label><br/> |
|
45 | 45 | </p> |
46 | - <p id="setDefaultInternalExpireDate" class="double-indent <?php if ($_['shareDefaultInternalExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
46 | + <p id="setDefaultInternalExpireDate" class="double-indent <?php if ($_['shareDefaultInternalExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
47 | 47 | <?php p($l->t('Expire after ')); ?> |
48 | 48 | <input type="text" name='shareapi_internal_expire_after_n_days' id="shareapiInternalExpireAfterNDays" placeholder="<?php p('7')?>" |
49 | 49 | value='<?php p($_['shareInternalExpireAfterNDays']) ?>' /> |
50 | 50 | <?php p($l->t('days')); ?> |
51 | 51 | <input type="checkbox" name="shareapi_internal_enforce_expire_date" id="shareapiInternalEnforceExpireDate" class="checkbox" |
52 | 52 | value="1" <?php if ($_['shareInternalEnforceExpireDate'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
53 | - <label for="shareapiInternalEnforceExpireDate"><?php p($l->t('Enforce expiration date'));?></label><br/> |
|
53 | + <label for="shareapiInternalEnforceExpireDate"><?php p($l->t('Enforce expiration date')); ?></label><br/> |
|
54 | 54 | </p> |
55 | 55 | |
56 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
56 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
57 | 57 | <input type="checkbox" name="shareapi_allow_links" id="allowLinks" class="checkbox" |
58 | 58 | value="1" <?php if ($_['allowLinks'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
59 | - <label for="allowLinks"><?php p($l->t('Allow users to share via link'));?></label><br/> |
|
59 | + <label for="allowLinks"><?php p($l->t('Allow users to share via link')); ?></label><br/> |
|
60 | 60 | </p> |
61 | 61 | |
62 | 62 | <p id="publicLinkSettings" class="indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
63 | 63 | <input type="checkbox" name="shareapi_allow_public_upload" id="allowPublicUpload" class="checkbox" |
64 | 64 | value="1" <?php if ($_['allowPublicUpload'] == 'yes') print_unescaped('checked="checked"'); ?> /> |
65 | - <label for="allowPublicUpload"><?php p($l->t('Allow public uploads'));?></label><br/> |
|
65 | + <label for="allowPublicUpload"><?php p($l->t('Allow public uploads')); ?></label><br/> |
|
66 | 66 | <input type="checkbox" name="shareapi_enable_link_password_by_default" id="enableLinkPasswordByDefault" class="checkbox" |
67 | 67 | value="1" <?php if ($_['enableLinkPasswordByDefault'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
68 | - <label for="enableLinkPasswordByDefault"><?php p($l->t('Always ask for a password'));?></label><br/> |
|
68 | + <label for="enableLinkPasswordByDefault"><?php p($l->t('Always ask for a password')); ?></label><br/> |
|
69 | 69 | <input type="checkbox" name="shareapi_enforce_links_password" id="enforceLinkPassword" class="checkbox" |
70 | 70 | value="1" <?php if ($_['enforceLinkPassword']) print_unescaped('checked="checked"'); ?> /> |
71 | - <label for="enforceLinkPassword"><?php p($l->t('Enforce password protection'));?></label><br/> |
|
71 | + <label for="enforceLinkPassword"><?php p($l->t('Enforce password protection')); ?></label><br/> |
|
72 | 72 | |
73 | 73 | <input type="checkbox" name="shareapi_default_expire_date" id="shareapiDefaultExpireDate" class="checkbox" |
74 | 74 | value="1" <?php if ($_['shareDefaultExpireDateSet'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
75 | - <label for="shareapiDefaultExpireDate"><?php p($l->t('Set default expiration date for link shares'));?></label><br/> |
|
75 | + <label for="shareapiDefaultExpireDate"><?php p($l->t('Set default expiration date for link shares')); ?></label><br/> |
|
76 | 76 | |
77 | 77 | </p> |
78 | - <p id="setDefaultExpireDate" class="double-indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareDefaultExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
78 | + <p id="setDefaultExpireDate" class="double-indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareDefaultExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
79 | 79 | <?php p($l->t('Expire after ')); ?> |
80 | 80 | <input type="text" name='shareapi_expire_after_n_days' id="shareapiExpireAfterNDays" placeholder="<?php p('7')?>" |
81 | 81 | value='<?php p($_['shareExpireAfterNDays']) ?>' /> |
82 | 82 | <?php p($l->t('days')); ?> |
83 | 83 | <input type="checkbox" name="shareapi_enforce_expire_date" id="shareapiEnforceExpireDate" class="checkbox" |
84 | 84 | value="1" <?php if ($_['shareEnforceExpireDate'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
85 | - <label for="shareapiEnforceExpireDate"><?php p($l->t('Enforce expiration date'));?></label><br/> |
|
85 | + <label for="shareapiEnforceExpireDate"><?php p($l->t('Enforce expiration date')); ?></label><br/> |
|
86 | 86 | </p> |
87 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
87 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
88 | 88 | <input type="checkbox" name="shareapi_allow_resharing" id="allowResharing" class="checkbox" |
89 | 89 | value="1" <?php if ($_['allowResharing'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
90 | - <label for="allowResharing"><?php p($l->t('Allow resharing'));?></label><br/> |
|
90 | + <label for="allowResharing"><?php p($l->t('Allow resharing')); ?></label><br/> |
|
91 | 91 | </p> |
92 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
92 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
93 | 93 | <input type="checkbox" name="shareapi_allow_group_sharing" id="allowGroupSharing" class="checkbox" |
94 | 94 | value="1" <?php if ($_['allowGroupSharing'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
95 | - <label for="allowGroupSharing"><?php p($l->t('Allow sharing with groups'));?></label><br /> |
|
95 | + <label for="allowGroupSharing"><?php p($l->t('Allow sharing with groups')); ?></label><br /> |
|
96 | 96 | </p> |
97 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
97 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
98 | 98 | <input type="checkbox" name="shareapi_only_share_with_group_members" id="onlyShareWithGroupMembers" class="checkbox" |
99 | 99 | value="1" <?php if ($_['onlyShareWithGroupMembers']) print_unescaped('checked="checked"'); ?> /> |
100 | - <label for="onlyShareWithGroupMembers"><?php p($l->t('Restrict users to only share with users in their groups'));?></label><br/> |
|
100 | + <label for="onlyShareWithGroupMembers"><?php p($l->t('Restrict users to only share with users in their groups')); ?></label><br/> |
|
101 | 101 | </p> |
102 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
102 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
103 | 103 | <input type="checkbox" name="shareapi_exclude_groups" id="shareapiExcludeGroups" class="checkbox" |
104 | 104 | value="1" <?php if ($_['shareExcludeGroups']) print_unescaped('checked="checked"'); ?> /> |
105 | - <label for="shareapiExcludeGroups"><?php p($l->t('Exclude groups from sharing'));?></label><br/> |
|
105 | + <label for="shareapiExcludeGroups"><?php p($l->t('Exclude groups from sharing')); ?></label><br/> |
|
106 | 106 | </p> |
107 | 107 | <p id="selectExcludedGroups" class="indent <?php if (!$_['shareExcludeGroups'] || $_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
108 | 108 | <input name="shareapi_exclude_groups_list" type="hidden" id="excludedGroups" value="<?php p($_['shareExcludedGroupsList']) ?>" style="width: 400px" class="noJSAutoUpdate"/> |
@@ -110,35 +110,35 @@ discard block |
||
110 | 110 | <em><?php p($l->t('These groups will still be able to receive shares, but not to initiate them.')); ?></em> |
111 | 111 | </p> |
112 | 112 | |
113 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
113 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
114 | 114 | <input type="checkbox" name="shareapi_allow_share_dialog_user_enumeration" value="1" id="shareapi_allow_share_dialog_user_enumeration" class="checkbox" |
115 | 115 | <?php if ($_['allowShareDialogUserEnumeration'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
116 | - <label for="shareapi_allow_share_dialog_user_enumeration"><?php p($l->t('Allow username autocompletion in share dialog. If this is disabled the full username or email address needs to be entered.'));?></label><br /> |
|
116 | + <label for="shareapi_allow_share_dialog_user_enumeration"><?php p($l->t('Allow username autocompletion in share dialog. If this is disabled the full username or email address needs to be entered.')); ?></label><br /> |
|
117 | 117 | </p> |
118 | 118 | |
119 | - <p id="shareapi_restrict_user_enumeration_to_group_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') p('hidden');?>"> |
|
119 | + <p id="shareapi_restrict_user_enumeration_to_group_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') p('hidden'); ?>"> |
|
120 | 120 | <input type="checkbox" name="shareapi_restrict_user_enumeration_to_group" value="1" id="shareapi_restrict_user_enumeration_to_group" class="checkbox" |
121 | 121 | <?php if ($_['restrictUserEnumerationToGroup'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
122 | - <label for="shareapi_restrict_user_enumeration_to_group"><?php p($l->t('Restrict username autocompletion to users within the same groups'));?></label><br /> |
|
122 | + <label for="shareapi_restrict_user_enumeration_to_group"><?php p($l->t('Restrict username autocompletion to users within the same groups')); ?></label><br /> |
|
123 | 123 | </p> |
124 | 124 | |
125 | 125 | <p> |
126 | 126 | <input type="checkbox" id="publicShareDisclaimer" class="checkbox noJSAutoUpdate" |
127 | 127 | <?php if ($_['publicShareDisclaimerText'] !== null) print_unescaped('checked="checked"'); ?> /> |
128 | - <label for="publicShareDisclaimer"><?php p($l->t('Show disclaimer text on the public link upload page. (Only shown when the file list is hidden.)'));?></label> |
|
128 | + <label for="publicShareDisclaimer"><?php p($l->t('Show disclaimer text on the public link upload page. (Only shown when the file list is hidden.)')); ?></label> |
|
129 | 129 | <span id="publicShareDisclaimerStatus" class="msg" style="display:none"></span> |
130 | 130 | <br/> |
131 | 131 | <textarea placeholder="<?php p($l->t('This text will be shown on the public link upload page when the file list is hidden.')) ?>" id="publicShareDisclaimerText" <?php if ($_['publicShareDisclaimerText'] === null) { print_unescaped('class="hidden"'); } ?>><?php p($_['publicShareDisclaimerText']) ?></textarea> |
132 | 132 | </p> |
133 | 133 | |
134 | - <h3><?php p($l->t('Default share permissions'));?></h3> |
|
134 | + <h3><?php p($l->t('Default share permissions')); ?></h3> |
|
135 | 135 | <input type="hidden" name="shareapi_default_permissions" id="shareApiDefaultPermissions" class="checkbox" |
136 | 136 | value="<?php p($_['shareApiDefaultPermissions']) ?>" /> |
137 | 137 | <p id="shareApiDefaultPermissionsSection" class="indent <?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
138 | 138 | <?php foreach ($_['shareApiDefaultPermissionsCheckboxes'] as $perm): ?> |
139 | 139 | <input type="checkbox" name="shareapi_default_permission_<?php p($perm['id']) ?>" id="shareapi_default_permission_<?php p($perm['id']) ?>" |
140 | 140 | class="noautosave checkbox" value="<?php p($perm['value']) ?>" <?php if (($_['shareApiDefaultPermissions'] & $perm['value']) !== 0) print_unescaped('checked="checked"'); ?> /> |
141 | - <label for="shareapi_default_permission_<?php p($perm['id']) ?>"><?php p($perm['label']);?></label> |
|
141 | + <label for="shareapi_default_permission_<?php p($perm['id']) ?>"><?php p($perm['label']); ?></label> |
|
142 | 142 | <?php endforeach ?> |
143 | 143 | </p> |
144 | 144 | </div> |
@@ -34,97 +34,181 @@ discard block |
||
34 | 34 | <p class="settings-hint"><?php p($l->t('As admin you can fine-tune the sharing behavior. Please see the documentation for more information.'));?></p> |
35 | 35 | <p id="enable"> |
36 | 36 | <input type="checkbox" name="shareapi_enabled" id="shareAPIEnabled" class="checkbox" |
37 | - value="1" <?php if ($_['shareAPIEnabled'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
37 | + value="1" <?php if ($_['shareAPIEnabled'] === 'yes') { |
|
38 | + print_unescaped('checked="checked"'); |
|
39 | +} |
|
40 | +?> /> |
|
38 | 41 | <label for="shareAPIEnabled"><?php p($l->t('Allow apps to use the Share API'));?></label><br/> |
39 | 42 | </p> |
40 | 43 | |
41 | - <p id="internalShareSettings" class="indent <?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
44 | + <p id="internalShareSettings" class="indent <?php if ($_['shareAPIEnabled'] === 'no') { |
|
45 | + p('hidden'); |
|
46 | +} |
|
47 | +?>"> |
|
42 | 48 | <input type="checkbox" name="shareapi_default_internal_expire_date" id="shareapiDefaultInternalExpireDate" class="checkbox" |
43 | - value="1" <?php if ($_['shareDefaultInternalExpireDateSet'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
49 | + value="1" <?php if ($_['shareDefaultInternalExpireDateSet'] === 'yes') { |
|
50 | + print_unescaped('checked="checked"'); |
|
51 | +} |
|
52 | +?> /> |
|
44 | 53 | <label for="shareapiDefaultInternalExpireDate"><?php p($l->t('Set default expiration date for shares'));?></label><br/> |
45 | 54 | </p> |
46 | - <p id="setDefaultInternalExpireDate" class="double-indent <?php if ($_['shareDefaultInternalExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
55 | + <p id="setDefaultInternalExpireDate" class="double-indent <?php if ($_['shareDefaultInternalExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') { |
|
56 | + p('hidden'); |
|
57 | +} |
|
58 | +?>"> |
|
47 | 59 | <?php p($l->t('Expire after ')); ?> |
48 | 60 | <input type="text" name='shareapi_internal_expire_after_n_days' id="shareapiInternalExpireAfterNDays" placeholder="<?php p('7')?>" |
49 | 61 | value='<?php p($_['shareInternalExpireAfterNDays']) ?>' /> |
50 | 62 | <?php p($l->t('days')); ?> |
51 | 63 | <input type="checkbox" name="shareapi_internal_enforce_expire_date" id="shareapiInternalEnforceExpireDate" class="checkbox" |
52 | - value="1" <?php if ($_['shareInternalEnforceExpireDate'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
64 | + value="1" <?php if ($_['shareInternalEnforceExpireDate'] === 'yes') { |
|
65 | + print_unescaped('checked="checked"'); |
|
66 | +} |
|
67 | +?> /> |
|
53 | 68 | <label for="shareapiInternalEnforceExpireDate"><?php p($l->t('Enforce expiration date'));?></label><br/> |
54 | 69 | </p> |
55 | 70 | |
56 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
71 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') { |
|
72 | + p('hidden'); |
|
73 | +} |
|
74 | +?>"> |
|
57 | 75 | <input type="checkbox" name="shareapi_allow_links" id="allowLinks" class="checkbox" |
58 | - value="1" <?php if ($_['allowLinks'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
76 | + value="1" <?php if ($_['allowLinks'] === 'yes') { |
|
77 | + print_unescaped('checked="checked"'); |
|
78 | +} |
|
79 | +?> /> |
|
59 | 80 | <label for="allowLinks"><?php p($l->t('Allow users to share via link'));?></label><br/> |
60 | 81 | </p> |
61 | 82 | |
62 | - <p id="publicLinkSettings" class="indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
83 | + <p id="publicLinkSettings" class="indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareAPIEnabled'] === 'no') { |
|
84 | + p('hidden'); |
|
85 | +} |
|
86 | +?>"> |
|
63 | 87 | <input type="checkbox" name="shareapi_allow_public_upload" id="allowPublicUpload" class="checkbox" |
64 | - value="1" <?php if ($_['allowPublicUpload'] == 'yes') print_unescaped('checked="checked"'); ?> /> |
|
88 | + value="1" <?php if ($_['allowPublicUpload'] == 'yes') { |
|
89 | + print_unescaped('checked="checked"'); |
|
90 | +} |
|
91 | +?> /> |
|
65 | 92 | <label for="allowPublicUpload"><?php p($l->t('Allow public uploads'));?></label><br/> |
66 | 93 | <input type="checkbox" name="shareapi_enable_link_password_by_default" id="enableLinkPasswordByDefault" class="checkbox" |
67 | - value="1" <?php if ($_['enableLinkPasswordByDefault'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
94 | + value="1" <?php if ($_['enableLinkPasswordByDefault'] === 'yes') { |
|
95 | + print_unescaped('checked="checked"'); |
|
96 | +} |
|
97 | +?> /> |
|
68 | 98 | <label for="enableLinkPasswordByDefault"><?php p($l->t('Always ask for a password'));?></label><br/> |
69 | 99 | <input type="checkbox" name="shareapi_enforce_links_password" id="enforceLinkPassword" class="checkbox" |
70 | - value="1" <?php if ($_['enforceLinkPassword']) print_unescaped('checked="checked"'); ?> /> |
|
100 | + value="1" <?php if ($_['enforceLinkPassword']) { |
|
101 | + print_unescaped('checked="checked"'); |
|
102 | +} |
|
103 | +?> /> |
|
71 | 104 | <label for="enforceLinkPassword"><?php p($l->t('Enforce password protection'));?></label><br/> |
72 | 105 | |
73 | 106 | <input type="checkbox" name="shareapi_default_expire_date" id="shareapiDefaultExpireDate" class="checkbox" |
74 | - value="1" <?php if ($_['shareDefaultExpireDateSet'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
107 | + value="1" <?php if ($_['shareDefaultExpireDateSet'] === 'yes') { |
|
108 | + print_unescaped('checked="checked"'); |
|
109 | +} |
|
110 | +?> /> |
|
75 | 111 | <label for="shareapiDefaultExpireDate"><?php p($l->t('Set default expiration date for link shares'));?></label><br/> |
76 | 112 | |
77 | 113 | </p> |
78 | - <p id="setDefaultExpireDate" class="double-indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareDefaultExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
114 | + <p id="setDefaultExpireDate" class="double-indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareDefaultExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') { |
|
115 | + p('hidden'); |
|
116 | +} |
|
117 | +?>"> |
|
79 | 118 | <?php p($l->t('Expire after ')); ?> |
80 | 119 | <input type="text" name='shareapi_expire_after_n_days' id="shareapiExpireAfterNDays" placeholder="<?php p('7')?>" |
81 | 120 | value='<?php p($_['shareExpireAfterNDays']) ?>' /> |
82 | 121 | <?php p($l->t('days')); ?> |
83 | 122 | <input type="checkbox" name="shareapi_enforce_expire_date" id="shareapiEnforceExpireDate" class="checkbox" |
84 | - value="1" <?php if ($_['shareEnforceExpireDate'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
123 | + value="1" <?php if ($_['shareEnforceExpireDate'] === 'yes') { |
|
124 | + print_unescaped('checked="checked"'); |
|
125 | +} |
|
126 | +?> /> |
|
85 | 127 | <label for="shareapiEnforceExpireDate"><?php p($l->t('Enforce expiration date'));?></label><br/> |
86 | 128 | </p> |
87 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
129 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') { |
|
130 | + p('hidden'); |
|
131 | +} |
|
132 | +?>"> |
|
88 | 133 | <input type="checkbox" name="shareapi_allow_resharing" id="allowResharing" class="checkbox" |
89 | - value="1" <?php if ($_['allowResharing'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
134 | + value="1" <?php if ($_['allowResharing'] === 'yes') { |
|
135 | + print_unescaped('checked="checked"'); |
|
136 | +} |
|
137 | +?> /> |
|
90 | 138 | <label for="allowResharing"><?php p($l->t('Allow resharing'));?></label><br/> |
91 | 139 | </p> |
92 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
140 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') { |
|
141 | + p('hidden'); |
|
142 | +} |
|
143 | +?>"> |
|
93 | 144 | <input type="checkbox" name="shareapi_allow_group_sharing" id="allowGroupSharing" class="checkbox" |
94 | - value="1" <?php if ($_['allowGroupSharing'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
145 | + value="1" <?php if ($_['allowGroupSharing'] === 'yes') { |
|
146 | + print_unescaped('checked="checked"'); |
|
147 | +} |
|
148 | +?> /> |
|
95 | 149 | <label for="allowGroupSharing"><?php p($l->t('Allow sharing with groups'));?></label><br /> |
96 | 150 | </p> |
97 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
151 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') { |
|
152 | + p('hidden'); |
|
153 | +} |
|
154 | +?>"> |
|
98 | 155 | <input type="checkbox" name="shareapi_only_share_with_group_members" id="onlyShareWithGroupMembers" class="checkbox" |
99 | - value="1" <?php if ($_['onlyShareWithGroupMembers']) print_unescaped('checked="checked"'); ?> /> |
|
156 | + value="1" <?php if ($_['onlyShareWithGroupMembers']) { |
|
157 | + print_unescaped('checked="checked"'); |
|
158 | +} |
|
159 | +?> /> |
|
100 | 160 | <label for="onlyShareWithGroupMembers"><?php p($l->t('Restrict users to only share with users in their groups'));?></label><br/> |
101 | 161 | </p> |
102 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
162 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') { |
|
163 | + p('hidden'); |
|
164 | +} |
|
165 | +?>"> |
|
103 | 166 | <input type="checkbox" name="shareapi_exclude_groups" id="shareapiExcludeGroups" class="checkbox" |
104 | - value="1" <?php if ($_['shareExcludeGroups']) print_unescaped('checked="checked"'); ?> /> |
|
167 | + value="1" <?php if ($_['shareExcludeGroups']) { |
|
168 | + print_unescaped('checked="checked"'); |
|
169 | +} |
|
170 | +?> /> |
|
105 | 171 | <label for="shareapiExcludeGroups"><?php p($l->t('Exclude groups from sharing'));?></label><br/> |
106 | 172 | </p> |
107 | - <p id="selectExcludedGroups" class="indent <?php if (!$_['shareExcludeGroups'] || $_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
173 | + <p id="selectExcludedGroups" class="indent <?php if (!$_['shareExcludeGroups'] || $_['shareAPIEnabled'] === 'no') { |
|
174 | + p('hidden'); |
|
175 | +} |
|
176 | +?>"> |
|
108 | 177 | <input name="shareapi_exclude_groups_list" type="hidden" id="excludedGroups" value="<?php p($_['shareExcludedGroupsList']) ?>" style="width: 400px" class="noJSAutoUpdate"/> |
109 | 178 | <br /> |
110 | 179 | <em><?php p($l->t('These groups will still be able to receive shares, but not to initiate them.')); ?></em> |
111 | 180 | </p> |
112 | 181 | |
113 | - <p class="<?php if ($_['shareAPIEnabled'] === 'no') p('hidden');?>"> |
|
182 | + <p class="<?php if ($_['shareAPIEnabled'] === 'no') { |
|
183 | + p('hidden'); |
|
184 | +} |
|
185 | +?>"> |
|
114 | 186 | <input type="checkbox" name="shareapi_allow_share_dialog_user_enumeration" value="1" id="shareapi_allow_share_dialog_user_enumeration" class="checkbox" |
115 | - <?php if ($_['allowShareDialogUserEnumeration'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
187 | + <?php if ($_['allowShareDialogUserEnumeration'] === 'yes') { |
|
188 | + print_unescaped('checked="checked"'); |
|
189 | +} |
|
190 | +?> /> |
|
116 | 191 | <label for="shareapi_allow_share_dialog_user_enumeration"><?php p($l->t('Allow username autocompletion in share dialog. If this is disabled the full username or email address needs to be entered.'));?></label><br /> |
117 | 192 | </p> |
118 | 193 | |
119 | - <p id="shareapi_restrict_user_enumeration_to_group_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') p('hidden');?>"> |
|
194 | + <p id="shareapi_restrict_user_enumeration_to_group_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') { |
|
195 | + p('hidden'); |
|
196 | +} |
|
197 | +?>"> |
|
120 | 198 | <input type="checkbox" name="shareapi_restrict_user_enumeration_to_group" value="1" id="shareapi_restrict_user_enumeration_to_group" class="checkbox" |
121 | - <?php if ($_['restrictUserEnumerationToGroup'] === 'yes') print_unescaped('checked="checked"'); ?> /> |
|
199 | + <?php if ($_['restrictUserEnumerationToGroup'] === 'yes') { |
|
200 | + print_unescaped('checked="checked"'); |
|
201 | +} |
|
202 | +?> /> |
|
122 | 203 | <label for="shareapi_restrict_user_enumeration_to_group"><?php p($l->t('Restrict username autocompletion to users within the same groups'));?></label><br /> |
123 | 204 | </p> |
124 | 205 | |
125 | 206 | <p> |
126 | 207 | <input type="checkbox" id="publicShareDisclaimer" class="checkbox noJSAutoUpdate" |
127 | - <?php if ($_['publicShareDisclaimerText'] !== null) print_unescaped('checked="checked"'); ?> /> |
|
208 | + <?php if ($_['publicShareDisclaimerText'] !== null) { |
|
209 | + print_unescaped('checked="checked"'); |
|
210 | +} |
|
211 | +?> /> |
|
128 | 212 | <label for="publicShareDisclaimer"><?php p($l->t('Show disclaimer text on the public link upload page. (Only shown when the file list is hidden.)'));?></label> |
129 | 213 | <span id="publicShareDisclaimerStatus" class="msg" style="display:none"></span> |
130 | 214 | <br/> |
@@ -134,10 +218,16 @@ discard block |
||
134 | 218 | <h3><?php p($l->t('Default share permissions'));?></h3> |
135 | 219 | <input type="hidden" name="shareapi_default_permissions" id="shareApiDefaultPermissions" class="checkbox" |
136 | 220 | value="<?php p($_['shareApiDefaultPermissions']) ?>" /> |
137 | - <p id="shareApiDefaultPermissionsSection" class="indent <?php if ($_['shareAPIEnabled'] === 'no') p('hidden'); ?>"> |
|
221 | + <p id="shareApiDefaultPermissionsSection" class="indent <?php if ($_['shareAPIEnabled'] === 'no') { |
|
222 | + p('hidden'); |
|
223 | +} |
|
224 | +?>"> |
|
138 | 225 | <?php foreach ($_['shareApiDefaultPermissionsCheckboxes'] as $perm): ?> |
139 | 226 | <input type="checkbox" name="shareapi_default_permission_<?php p($perm['id']) ?>" id="shareapi_default_permission_<?php p($perm['id']) ?>" |
140 | - class="noautosave checkbox" value="<?php p($perm['value']) ?>" <?php if (($_['shareApiDefaultPermissions'] & $perm['value']) !== 0) print_unescaped('checked="checked"'); ?> /> |
|
227 | + class="noautosave checkbox" value="<?php p($perm['value']) ?>" <?php if (($_['shareApiDefaultPermissions'] & $perm['value']) !== 0) { |
|
228 | + print_unescaped('checked="checked"'); |
|
229 | +} |
|
230 | +?> /> |
|
141 | 231 | <label for="shareapi_default_permission_<?php p($perm['id']) ?>"><?php p($perm['label']);?></label> |
142 | 232 | <?php endforeach ?> |
143 | 233 | </p> |
@@ -28,24 +28,24 @@ discard block |
||
28 | 28 | ?> |
29 | 29 | |
30 | 30 | <div id="security-warning" class="section"> |
31 | - <h2 class="inlineblock"><?php p($l->t('Security & setup warnings'));?></h2> |
|
32 | - <a target="_blank" rel="noreferrer" class="icon-info" title="<?php p($l->t('Open documentation'));?>" href="<?php p(link_to_docs('admin-warnings')); ?>"></a> |
|
33 | - <p class="settings-hint"><?php p($l->t('It\'s important for the security and performance of your instance that everything is configured correctly. To help you with that we are doing some automatic checks. Please see the linked documentation for more information.'));?></p> |
|
31 | + <h2 class="inlineblock"><?php p($l->t('Security & setup warnings')); ?></h2> |
|
32 | + <a target="_blank" rel="noreferrer" class="icon-info" title="<?php p($l->t('Open documentation')); ?>" href="<?php p(link_to_docs('admin-warnings')); ?>"></a> |
|
33 | + <p class="settings-hint"><?php p($l->t('It\'s important for the security and performance of your instance that everything is configured correctly. To help you with that we are doing some automatic checks. Please see the linked documentation for more information.')); ?></p> |
|
34 | 34 | |
35 | 35 | <div id="security-warning-state-ok" class="hidden"> |
36 | - <span class="icon icon-checkmark-white"></span><span class="message"><?php p($l->t('All checks passed.'));?></span> |
|
36 | + <span class="icon icon-checkmark-white"></span><span class="message"><?php p($l->t('All checks passed.')); ?></span> |
|
37 | 37 | </div> |
38 | 38 | <div id="security-warning-state-failure" class="hidden"> |
39 | - <span class="icon icon-close-white"></span><span class="message"><?php p($l->t('There are some errors regarding your setup.'));?></span> |
|
39 | + <span class="icon icon-close-white"></span><span class="message"><?php p($l->t('There are some errors regarding your setup.')); ?></span> |
|
40 | 40 | </div> |
41 | 41 | <div id="security-warning-state-warning" class="hidden"> |
42 | - <span class="icon icon-error-white"></span><span class="message"><?php p($l->t('There are some warnings regarding your setup.'));?></span> |
|
42 | + <span class="icon icon-error-white"></span><span class="message"><?php p($l->t('There are some warnings regarding your setup.')); ?></span> |
|
43 | 43 | </div> |
44 | 44 | <div id="security-warning-state-loading"> |
45 | - <span class="icon loading"></span><span class="message"><?php p($l->t('Checking for system and security issues.'));?></span> |
|
45 | + <span class="icon loading"></span><span class="message"><?php p($l->t('Checking for system and security issues.')); ?></span> |
|
46 | 46 | </div> |
47 | 47 | |
48 | - <div id="postsetupchecks" data-check-wellknown="<?php if($_['checkForWorkingWellKnownSetup']) { p('true'); } else { p('false'); } ?>"> |
|
48 | + <div id="postsetupchecks" data-check-wellknown="<?php if ($_['checkForWorkingWellKnownSetup']) { p('true'); } else { p('false'); } ?>"> |
|
49 | 49 | <ul class="errors hidden"></ul> |
50 | 50 | <ul class="warnings hidden"></ul> |
51 | 51 | <ul class="info hidden"></ul> |
@@ -55,13 +55,13 @@ discard block |
||
55 | 55 | </p> |
56 | 56 | |
57 | 57 | <p class="extra-top-margin"> |
58 | - <?php print_unescaped($l->t('Check the security of your Nextcloud over <a target="_blank" rel="noreferrer noopener" href="%s">our security scan ↗</a>.', ['https://scan.nextcloud.com']));?> |
|
58 | + <?php print_unescaped($l->t('Check the security of your Nextcloud over <a target="_blank" rel="noreferrer noopener" href="%s">our security scan ↗</a>.', ['https://scan.nextcloud.com'])); ?> |
|
59 | 59 | </p> |
60 | 60 | |
61 | 61 | </div> |
62 | 62 | |
63 | 63 | <div id="version" class="section"> |
64 | 64 | <!-- should be the last part, so Updater can follow if enabled (it has no heading therefore). --> |
65 | - <h2><?php p($l->t('Version'));?></h2> |
|
65 | + <h2><?php p($l->t('Version')); ?></h2> |
|
66 | 66 | <p><strong><a href="<?php print_unescaped($theme->getBaseUrl()); ?>" rel="noreferrer noopener" target="_blank"><?php p($theme->getTitle()); ?></a> <?php p(OC_Util::getHumanVersion()) ?></strong></p> |
67 | 67 | </div> |
@@ -22,7 +22,7 @@ discard block |
||
22 | 22 | */ |
23 | 23 | |
24 | 24 | style('settings', 'settings'); |
25 | -script('settings', [ 'settings', 'admin', 'log', 'certificates']); |
|
25 | +script('settings', ['settings', 'admin', 'log', 'certificates']); |
|
26 | 26 | script('core', 'setupchecks'); |
27 | 27 | script('files', 'jquery.fileupload'); |
28 | 28 | |
@@ -30,14 +30,14 @@ discard block |
||
30 | 30 | |
31 | 31 | <div id="app-navigation"> |
32 | 32 | <ul> |
33 | - <?php if(!empty($_['forms']['admin'])) { ?> |
|
33 | + <?php if (!empty($_['forms']['admin'])) { ?> |
|
34 | 34 | <li class="app-navigation-caption"><?php p($l->t('Personal')); ?></li> |
35 | 35 | <?php |
36 | 36 | } |
37 | - foreach($_['forms']['personal'] as $form) { |
|
37 | + foreach ($_['forms']['personal'] as $form) { |
|
38 | 38 | if (isset($form['anchor'])) { |
39 | 39 | $anchor = \OC::$server->getURLGenerator()->linkToRoute('settings.PersonalSettings.index', ['section' => $form['anchor']]); |
40 | - $class = 'nav-icon-' . $form['anchor']; |
|
40 | + $class = 'nav-icon-'.$form['anchor']; |
|
41 | 41 | $sectionName = $form['section-name']; |
42 | 42 | $active = $form['active'] ? ' class="active"' : ''; |
43 | 43 | ?> |
@@ -57,16 +57,16 @@ discard block |
||
57 | 57 | ?> |
58 | 58 | |
59 | 59 | <?php |
60 | - if(!empty($_['forms']['admin'])) { |
|
60 | + if (!empty($_['forms']['admin'])) { |
|
61 | 61 | ?> |
62 | 62 | <li class="app-navigation-caption"><?php p($l->t('Administration')); ?></li> |
63 | 63 | <?php |
64 | 64 | } |
65 | - foreach($_['forms']['admin'] as $form) { |
|
65 | + foreach ($_['forms']['admin'] as $form) { |
|
66 | 66 | if (isset($form['anchor'])) { |
67 | 67 | |
68 | 68 | $anchor = \OC::$server->getURLGenerator()->linkToRoute('settings.AdminSettings.index', ['section' => $form['anchor']]); |
69 | - $class = 'nav-icon-' . $form['anchor']; |
|
69 | + $class = 'nav-icon-'.$form['anchor']; |
|
70 | 70 | $sectionName = $form['section-name']; |
71 | 71 | $active = $form['active'] ? ' class="active"' : ''; |
72 | 72 | ?> |