]> git.immae.eu Git - github/wallabag/wallabag.git/blob - src/Wallabag/ApiBundle/Controller/WallabagRestController.php
Merge pull request #2372 from pmartin/api-get-entry-as-epub
[github/wallabag/wallabag.git] / src / Wallabag / ApiBundle / Controller / WallabagRestController.php
1 <?php
2
3 namespace Wallabag\ApiBundle\Controller;
4
5 use FOS\RestBundle\Controller\FOSRestController;
6 use Hateoas\Configuration\Route as HateoasRoute;
7 use Hateoas\Representation\Factory\PagerfantaFactory;
8 use Nelmio\ApiDocBundle\Annotation\ApiDoc;
9 use Symfony\Component\HttpFoundation\Request;
10 use Symfony\Component\HttpFoundation\JsonResponse;
11 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
12 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
13 use Wallabag\CoreBundle\Entity\Entry;
14 use Wallabag\CoreBundle\Entity\Tag;
15 use FOS\RestBundle\Controller\Annotations\Route;
16
17 class WallabagRestController extends FOSRestController
18 {
19 private function validateAuthentication()
20 {
21 if (false === $this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
22 throw new AccessDeniedException();
23 }
24 }
25
26 /**
27 * Check if an entry exist by url.
28 *
29 * @ApiDoc(
30 * parameters={
31 * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="Url to check if it exists"},
32 * {"name"="urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Urls (as an array) to check if it exists"}
33 * }
34 * )
35 *
36 * @return JsonResponse
37 */
38 public function getEntriesExistsAction(Request $request)
39 {
40 $this->validateAuthentication();
41
42 $urls = $request->query->get('urls', []);
43
44 // handle multiple urls first
45 if (!empty($urls)) {
46 $results = [];
47 foreach ($urls as $url) {
48 $res = $this->getDoctrine()
49 ->getRepository('WallabagCoreBundle:Entry')
50 ->findByUrlAndUserId($url, $this->getUser()->getId());
51
52 $results[$url] = false === $res ? false : true;
53 }
54
55 $json = $this->get('serializer')->serialize($results, 'json');
56
57 return (new JsonResponse())->setJson($json);
58 }
59
60 // let's see if it is a simple url?
61 $url = $request->query->get('url', '');
62
63 if (empty($url)) {
64 throw $this->createAccessDeniedException('URL is empty?, logged user id: '.$this->getUser()->getId());
65 }
66
67 $res = $this->getDoctrine()
68 ->getRepository('WallabagCoreBundle:Entry')
69 ->findByUrlAndUserId($url, $this->getUser()->getId());
70
71 $exists = false === $res ? false : true;
72
73 $json = $this->get('serializer')->serialize(['exists' => $exists], 'json');
74
75 return (new JsonResponse())->setJson($json);
76 }
77
78 /**
79 * Retrieve all entries. It could be filtered by many options.
80 *
81 * @ApiDoc(
82 * parameters={
83 * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0, all entries by default", "description"="filter by archived status."},
84 * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0, all entries by default", "description"="filter by starred status."},
85 * {"name"="sort", "dataType"="string", "required"=false, "format"="'created' or 'updated', default 'created'", "description"="sort entries by date."},
86 * {"name"="order", "dataType"="string", "required"=false, "format"="'asc' or 'desc', default 'desc'", "description"="order of sort."},
87 * {"name"="page", "dataType"="integer", "required"=false, "format"="default '1'", "description"="what page you want."},
88 * {"name"="perPage", "dataType"="integer", "required"=false, "format"="default'30'", "description"="results per page."},
89 * {"name"="tags", "dataType"="string", "required"=false, "format"="api,rest", "description"="a list of tags url encoded. Will returns entries that matches ALL tags."},
90 * {"name"="since", "dataType"="integer", "required"=false, "format"="default '0'", "description"="The timestamp since when you want entries updated."},
91 * }
92 * )
93 *
94 * @return JsonResponse
95 */
96 public function getEntriesAction(Request $request)
97 {
98 $this->validateAuthentication();
99
100 $isArchived = (null === $request->query->get('archive')) ? null : (bool) $request->query->get('archive');
101 $isStarred = (null === $request->query->get('starred')) ? null : (bool) $request->query->get('starred');
102 $sort = $request->query->get('sort', 'created');
103 $order = $request->query->get('order', 'desc');
104 $page = (int) $request->query->get('page', 1);
105 $perPage = (int) $request->query->get('perPage', 30);
106 $tags = $request->query->get('tags', '');
107 $since = $request->query->get('since', 0);
108
109 $pager = $this->getDoctrine()
110 ->getRepository('WallabagCoreBundle:Entry')
111 ->findEntries($this->getUser()->getId(), $isArchived, $isStarred, $sort, $order, $since, $tags);
112
113 $pager->setCurrentPage($page);
114 $pager->setMaxPerPage($perPage);
115
116 $pagerfantaFactory = new PagerfantaFactory('page', 'perPage');
117 $paginatedCollection = $pagerfantaFactory->createRepresentation(
118 $pager,
119 new HateoasRoute(
120 'api_get_entries',
121 [
122 'archive' => $isArchived,
123 'starred' => $isStarred,
124 'sort' => $sort,
125 'order' => $order,
126 'page' => $page,
127 'perPage' => $perPage,
128 'tags' => $tags,
129 'since' => $since,
130 ],
131 UrlGeneratorInterface::ABSOLUTE_URL
132 )
133 );
134
135 $json = $this->get('serializer')->serialize($paginatedCollection, 'json');
136
137 return (new JsonResponse())->setJson($json);
138 }
139
140 /**
141 * Retrieve a single entry.
142 *
143 * @ApiDoc(
144 * requirements={
145 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
146 * }
147 * )
148 *
149 * @return JsonResponse
150 */
151 public function getEntryAction(Entry $entry)
152 {
153 $this->validateAuthentication();
154 $this->validateUserAccess($entry->getUser()->getId());
155
156 $json = $this->get('serializer')->serialize($entry, 'json');
157
158 return (new JsonResponse())->setJson($json);
159 }
160
161 /**
162 * Retrieve a single entry as a predefined format.
163 *
164 * @ApiDoc(
165 * requirements={
166 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
167 * }
168 * )
169 *
170 * @Route(requirements={"_format"="epub|mobi|pdf|txt|csv"})
171 *
172 * @return Response
173 */
174 public function getEntryExportAction(Entry $entry, Request $request)
175 {
176 $this->validateAuthentication();
177 $this->validateUserAccess($entry->getUser()->getId());
178
179 return $this->get('wallabag_core.helper.entries_export')
180 ->setEntries($entry)
181 ->updateTitle('entry')
182 ->exportAs($request->attributes->get('_format'));
183 }
184
185 /**
186 * Create an entry.
187 *
188 * @ApiDoc(
189 * parameters={
190 * {"name"="url", "dataType"="string", "required"=true, "format"="http://www.test.com/article.html", "description"="Url for the entry."},
191 * {"name"="title", "dataType"="string", "required"=false, "description"="Optional, we'll get the title from the page."},
192 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
193 * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already starred"},
194 * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already archived"},
195 * }
196 * )
197 *
198 * @return JsonResponse
199 */
200 public function postEntriesAction(Request $request)
201 {
202 $this->validateAuthentication();
203
204 $url = $request->request->get('url');
205 $title = $request->request->get('title');
206 $isArchived = $request->request->get('archive');
207 $isStarred = $request->request->get('starred');
208
209 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId($url, $this->getUser()->getId());
210
211 if (false === $entry) {
212 $entry = $this->get('wallabag_core.content_proxy')->updateEntry(
213 new Entry($this->getUser()),
214 $url
215 );
216 }
217
218 if (!is_null($title)) {
219 $entry->setTitle($title);
220 }
221
222 $tags = $request->request->get('tags', '');
223 if (!empty($tags)) {
224 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags);
225 }
226
227 if (!is_null($isStarred)) {
228 $entry->setStarred((bool) $isStarred);
229 }
230
231 if (!is_null($isArchived)) {
232 $entry->setArchived((bool) $isArchived);
233 }
234
235 $em = $this->getDoctrine()->getManager();
236 $em->persist($entry);
237
238 $em->flush();
239
240 $json = $this->get('serializer')->serialize($entry, 'json');
241
242 return (new JsonResponse())->setJson($json);
243 }
244
245 /**
246 * Change several properties of an entry.
247 *
248 * @ApiDoc(
249 * requirements={
250 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
251 * },
252 * parameters={
253 * {"name"="title", "dataType"="string", "required"=false},
254 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
255 * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="archived the entry."},
256 * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="starred the entry."},
257 * }
258 * )
259 *
260 * @return JsonResponse
261 */
262 public function patchEntriesAction(Entry $entry, Request $request)
263 {
264 $this->validateAuthentication();
265 $this->validateUserAccess($entry->getUser()->getId());
266
267 $title = $request->request->get('title');
268 $isArchived = $request->request->get('archive');
269 $isStarred = $request->request->get('starred');
270
271 if (!is_null($title)) {
272 $entry->setTitle($title);
273 }
274
275 if (!is_null($isArchived)) {
276 $entry->setArchived((bool) $isArchived);
277 }
278
279 if (!is_null($isStarred)) {
280 $entry->setStarred((bool) $isStarred);
281 }
282
283 $tags = $request->request->get('tags', '');
284 if (!empty($tags)) {
285 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags);
286 }
287
288 $em = $this->getDoctrine()->getManager();
289 $em->flush();
290
291 $json = $this->get('serializer')->serialize($entry, 'json');
292
293 return (new JsonResponse())->setJson($json);
294 }
295
296 /**
297 * Delete **permanently** an entry.
298 *
299 * @ApiDoc(
300 * requirements={
301 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
302 * }
303 * )
304 *
305 * @return JsonResponse
306 */
307 public function deleteEntriesAction(Entry $entry)
308 {
309 $this->validateAuthentication();
310 $this->validateUserAccess($entry->getUser()->getId());
311
312 $em = $this->getDoctrine()->getManager();
313 $em->remove($entry);
314 $em->flush();
315
316 $json = $this->get('serializer')->serialize($entry, 'json');
317
318 return (new JsonResponse())->setJson($json);
319 }
320
321 /**
322 * Retrieve all tags for an entry.
323 *
324 * @ApiDoc(
325 * requirements={
326 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
327 * }
328 * )
329 *
330 * @return JsonResponse
331 */
332 public function getEntriesTagsAction(Entry $entry)
333 {
334 $this->validateAuthentication();
335 $this->validateUserAccess($entry->getUser()->getId());
336
337 $json = $this->get('serializer')->serialize($entry->getTags(), 'json');
338
339 return (new JsonResponse())->setJson($json);
340 }
341
342 /**
343 * Add one or more tags to an entry.
344 *
345 * @ApiDoc(
346 * requirements={
347 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
348 * },
349 * parameters={
350 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
351 * }
352 * )
353 *
354 * @return JsonResponse
355 */
356 public function postEntriesTagsAction(Request $request, Entry $entry)
357 {
358 $this->validateAuthentication();
359 $this->validateUserAccess($entry->getUser()->getId());
360
361 $tags = $request->request->get('tags', '');
362 if (!empty($tags)) {
363 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags);
364 }
365
366 $em = $this->getDoctrine()->getManager();
367 $em->persist($entry);
368 $em->flush();
369
370 $json = $this->get('serializer')->serialize($entry, 'json');
371
372 return (new JsonResponse())->setJson($json);
373 }
374
375 /**
376 * Permanently remove one tag for an entry.
377 *
378 * @ApiDoc(
379 * requirements={
380 * {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag ID"},
381 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
382 * }
383 * )
384 *
385 * @return JsonResponse
386 */
387 public function deleteEntriesTagsAction(Entry $entry, Tag $tag)
388 {
389 $this->validateAuthentication();
390 $this->validateUserAccess($entry->getUser()->getId());
391
392 $entry->removeTag($tag);
393 $em = $this->getDoctrine()->getManager();
394 $em->persist($entry);
395 $em->flush();
396
397 $json = $this->get('serializer')->serialize($entry, 'json');
398
399 return (new JsonResponse())->setJson($json);
400 }
401
402 /**
403 * Retrieve all tags.
404 *
405 * @ApiDoc()
406 *
407 * @return JsonResponse
408 */
409 public function getTagsAction()
410 {
411 $this->validateAuthentication();
412
413 $tags = $this->getDoctrine()
414 ->getRepository('WallabagCoreBundle:Tag')
415 ->findAllTags($this->getUser()->getId());
416
417 $json = $this->get('serializer')->serialize($tags, 'json');
418
419 return (new JsonResponse())->setJson($json);
420 }
421
422 /**
423 * Permanently remove one tag from **every** entry.
424 *
425 * @ApiDoc(
426 * requirements={
427 * {"name"="tag", "dataType"="string", "required"=true, "requirement"="\w+", "description"="Tag as a string"}
428 * }
429 * )
430 *
431 * @return JsonResponse
432 */
433 public function deleteTagLabelAction(Request $request)
434 {
435 $this->validateAuthentication();
436 $label = $request->request->get('tag', '');
437
438 $tag = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findOneByLabel($label);
439
440 if (empty($tag)) {
441 throw $this->createNotFoundException('Tag not found');
442 }
443
444 $this->getDoctrine()
445 ->getRepository('WallabagCoreBundle:Entry')
446 ->removeTag($this->getUser()->getId(), $tag);
447
448 $this->cleanOrphanTag($tag);
449
450 $json = $this->get('serializer')->serialize($tag, 'json');
451
452 return (new JsonResponse())->setJson($json);
453 }
454
455 /**
456 * Permanently remove some tags from **every** entry.
457 *
458 * @ApiDoc(
459 * requirements={
460 * {"name"="tags", "dataType"="string", "required"=true, "format"="tag1,tag2", "description"="Tags as strings (comma splitted)"}
461 * }
462 * )
463 *
464 * @return JsonResponse
465 */
466 public function deleteTagsLabelAction(Request $request)
467 {
468 $this->validateAuthentication();
469
470 $tagsLabels = $request->request->get('tags', '');
471
472 $tags = [];
473
474 foreach (explode(',', $tagsLabels) as $tagLabel) {
475 $tagEntity = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findOneByLabel($tagLabel);
476
477 if (!empty($tagEntity)) {
478 $tags[] = $tagEntity;
479 }
480 }
481
482 if (empty($tags)) {
483 throw $this->createNotFoundException('Tags not found');
484 }
485
486 $this->getDoctrine()
487 ->getRepository('WallabagCoreBundle:Entry')
488 ->removeTags($this->getUser()->getId(), $tags);
489
490 $this->cleanOrphanTag($tags);
491
492 $json = $this->get('serializer')->serialize($tags, 'json');
493
494 return (new JsonResponse())->setJson($json);
495 }
496
497 /**
498 * Permanently remove one tag from **every** entry.
499 *
500 * @ApiDoc(
501 * requirements={
502 * {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag"}
503 * }
504 * )
505 *
506 * @return JsonResponse
507 */
508 public function deleteTagAction(Tag $tag)
509 {
510 $this->validateAuthentication();
511
512 $this->getDoctrine()
513 ->getRepository('WallabagCoreBundle:Entry')
514 ->removeTag($this->getUser()->getId(), $tag);
515
516 $this->cleanOrphanTag($tag);
517
518 $json = $this->get('serializer')->serialize($tag, 'json');
519
520 return (new JsonResponse())->setJson($json);
521 }
522
523 /**
524 * Retrieve version number.
525 *
526 * @ApiDoc()
527 *
528 * @return JsonResponse
529 */
530 public function getVersionAction()
531 {
532 $version = $this->container->getParameter('wallabag_core.version');
533
534 $json = $this->get('serializer')->serialize($version, 'json');
535
536 return (new JsonResponse())->setJson($json);
537 }
538
539 /**
540 * Remove orphan tag in case no entries are associated to it.
541 *
542 * @param Tag|array $tags
543 */
544 private function cleanOrphanTag($tags)
545 {
546 if (!is_array($tags)) {
547 $tags = [$tags];
548 }
549
550 $em = $this->getDoctrine()->getManager();
551
552 foreach ($tags as $tag) {
553 if (count($tag->getEntries()) === 0) {
554 $em->remove($tag);
555 }
556 }
557
558 $em->flush();
559 }
560
561 /**
562 * Validate that the first id is equal to the second one.
563 * If not, throw exception. It means a user try to access information from an other user.
564 *
565 * @param int $requestUserId User id from the requested source
566 */
567 private function validateUserAccess($requestUserId)
568 {
569 $user = $this->get('security.token_storage')->getToken()->getUser();
570 if ($requestUserId != $user->getId()) {
571 throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$requestUserId.', logged user id: '.$user->getId());
572 }
573 }
574 }