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