1<?php
2
3declare(strict_types=1);
4
5namespace OCA\Notes\Controller;
6
7use OCA\Notes\AppInfo\Application;
8use OCA\Notes\Db\Meta;
9use OCA\Notes\Service\Note;
10use OCA\Notes\Service\NotesService;
11use OCA\Notes\Service\MetaNote;
12use OCA\Notes\Service\MetaService;
13use OCA\Notes\Service\Util;
14
15use OCP\AppFramework\Http;
16use OCP\AppFramework\Http\JSONResponse;
17use OCP\IRequest;
18use OCP\IUserSession;
19
20use Psr\Log\LoggerInterface;
21
22class Helper {
23
24	/** @var NotesService */
25	private $notesService;
26	/** @var MetaService */
27	private $metaService;
28	/** @var LoggerInterface */
29	public $logger;
30	/** @var IUserSession */
31	private $userSession;
32
33	public function __construct(
34		NotesService $notesService,
35		MetaService $metaService,
36		IUserSession $userSession,
37		LoggerInterface $logger
38	) {
39		$this->notesService = $notesService;
40		$this->metaService = $metaService;
41		$this->userSession = $userSession;
42		$this->logger = $logger;
43	}
44
45	public function getUID() : string {
46		return $this->userSession->getUser()->getUID();
47	}
48
49	public function getNoteWithETagCheck(int $id, IRequest $request) : Note {
50		$userId = $this->getUID();
51		$note = $this->notesService->get($userId, $id);
52		$ifMatch = $request->getHeader('If-Match');
53		if ($ifMatch) {
54			$matchEtags = preg_split('/,\s*/', $ifMatch);
55			$meta = $this->metaService->update($userId, $note);
56			if (!in_array('"'.$meta->getEtag().'"', $matchEtags)) {
57				throw new ETagDoesNotMatchException($note);
58			}
59		}
60		return $note;
61	}
62
63	public function getNoteData(Note $note, array $exclude = [], Meta $meta = null) : array {
64		if ($meta === null) {
65			$meta = $this->metaService->update($this->getUID(), $note);
66		}
67		$data = $note->getData($exclude);
68		$data['etag'] = $meta->getEtag();
69		return $data;
70	}
71
72	public function getNotesAndCategories(
73		int $pruneBefore,
74		array $exclude,
75		string $category = null,
76		int $chunkSize = 0,
77		string $chunkCursorStr = null
78	) : array {
79		$userId = $this->getUID();
80		$chunkCursor = $chunkCursorStr ? ChunkCursor::fromString($chunkCursorStr) : null;
81		$lastUpdate = $chunkCursor->timeStart ?? new \DateTime();
82		$data = $this->notesService->getAll($userId);
83		$metaNotes = $this->metaService->getAll($userId, $data['notes']);
84
85		// if a category is requested, then ignore all other notes
86		if ($category !== null) {
87			$metaNotes = array_filter($metaNotes, function (MetaNote $m) use ($category) {
88				return $m->note->getCategory() === $category;
89			});
90		}
91
92		// list of notes that should be sent to the client
93		$fullNotes = array_filter($metaNotes, function (MetaNote $m) use ($pruneBefore, $chunkCursor) {
94			$isPruned = $pruneBefore && $m->meta->getLastUpdate() < $pruneBefore;
95			$noteLastUpdate = (int)$m->meta->getLastUpdate();
96			$isBeforeCursor = $chunkCursor && (
97				$noteLastUpdate < $chunkCursor->noteLastUpdate
98				|| ($noteLastUpdate === $chunkCursor->noteLastUpdate
99				&& $m->note->getId() <= $chunkCursor->noteId)
100			);
101			return !$isPruned && !$isBeforeCursor;
102		});
103
104		// sort the list for slicing the next chunk
105		uasort($fullNotes, function (MetaNote $a, MetaNote $b) {
106			return $a->meta->getLastUpdate() <=> $b->meta->getLastUpdate()
107				?: $a->note->getId() <=> $b->note->getId();
108		});
109
110		// slice the next chunk
111		$chunkedNotes = $chunkSize ? array_slice($fullNotes, 0, $chunkSize, true) : $fullNotes;
112		$numPendingNotes = count($fullNotes) - count($chunkedNotes);
113
114		// if the chunk does not contain all remaining notes, then generate new chunk cursor
115		$newChunkCursor = $numPendingNotes ? ChunkCursor::fromNote($lastUpdate, end($chunkedNotes)) : null;
116
117		// load data for the current chunk
118		$notesData = array_map(function (MetaNote $m) use ($exclude) {
119			return $this->getNoteData($m->note, $exclude, $m->meta);
120		}, $chunkedNotes);
121
122		return [
123			'categories' => $data['categories'],
124			'notesAll' => $metaNotes,
125			'notesData' => $notesData,
126			'lastUpdate' => $lastUpdate,
127			'chunkCursor' => $newChunkCursor,
128			'numPendingNotes' => $numPendingNotes,
129		];
130	}
131
132	public function logException(\Throwable $e) : void {
133		$this->logger->error('Controller failed with '.get_class($e), [ 'exception' => $e ]);
134	}
135
136	public function createErrorResponse(\Throwable $e, int $statusCode) : JSONResponse {
137		$response = [
138			'errorType' => get_class($e)
139		];
140		return new JSONResponse($response, $statusCode);
141	}
142
143	public function handleErrorResponse(callable $respond) : JSONResponse {
144		try {
145			$data = Util::retryIfLocked($respond);
146			$response = $data instanceof JSONResponse ? $data : new JSONResponse($data);
147		} catch (\OCA\Notes\Controller\ETagDoesNotMatchException $e) {
148			$response = new JSONResponse($this->getNoteData($e->note), Http::STATUS_PRECONDITION_FAILED);
149		} catch (\OCA\Notes\Service\NoteDoesNotExistException $e) {
150			$this->logException($e);
151			$response = $this->createErrorResponse($e, Http::STATUS_NOT_FOUND);
152		} catch (\OCA\Notes\Service\InsufficientStorageException $e) {
153			$this->logException($e);
154			$response = $this->createErrorResponse($e, Http::STATUS_INSUFFICIENT_STORAGE);
155		} catch (\OCA\Notes\Service\NoteNotWritableException $e) {
156			$this->logException($e);
157			$response = $this->createErrorResponse($e, Http::STATUS_FORBIDDEN);
158		} catch (\OCP\Lock\LockedException $e) {
159			$this->logException($e);
160			$response = $this->createErrorResponse($e, Http::STATUS_LOCKED);
161		} catch (\Throwable $e) {
162			$this->logException($e);
163			$response = $this->createErrorResponse($e, Http::STATUS_INTERNAL_SERVER_ERROR);
164		}
165		$response->addHeader('X-Notes-API-Versions', implode(', ', Application::$API_VERSIONS));
166		return $response;
167	}
168}
169