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