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