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