diff options
Diffstat (limited to 'src')
28 files changed, 511 insertions, 128 deletions
diff --git a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php index c13a034f..2b4b0e8d 100644 --- a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php +++ b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php | |||
@@ -7,6 +7,8 @@ use Symfony\Component\HttpFoundation\JsonResponse; | |||
7 | use Symfony\Component\HttpFoundation\Request; | 7 | use Symfony\Component\HttpFoundation\Request; |
8 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; | 8 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; |
9 | use Wallabag\AnnotationBundle\Entity\Annotation; | 9 | use Wallabag\AnnotationBundle\Entity\Annotation; |
10 | use Wallabag\AnnotationBundle\Form\EditAnnotationType; | ||
11 | use Wallabag\AnnotationBundle\Form\NewAnnotationType; | ||
10 | use Wallabag\CoreBundle\Entity\Entry; | 12 | use Wallabag\CoreBundle\Entity\Entry; |
11 | 13 | ||
12 | class WallabagAnnotationController extends FOSRestController | 14 | class WallabagAnnotationController extends FOSRestController |
@@ -49,25 +51,25 @@ class WallabagAnnotationController extends FOSRestController | |||
49 | $data = json_decode($request->getContent(), true); | 51 | $data = json_decode($request->getContent(), true); |
50 | 52 | ||
51 | $em = $this->getDoctrine()->getManager(); | 53 | $em = $this->getDoctrine()->getManager(); |
52 | |||
53 | $annotation = new Annotation($this->getUser()); | 54 | $annotation = new Annotation($this->getUser()); |
55 | $annotation->setEntry($entry); | ||
54 | 56 | ||
55 | $annotation->setText($data['text']); | 57 | $form = $this->get('form.factory')->createNamed('', NewAnnotationType::class, $annotation, [ |
56 | if (array_key_exists('quote', $data)) { | 58 | 'csrf_protection' => false, |
57 | $annotation->setQuote($data['quote']); | 59 | 'allow_extra_fields' => true, |
58 | } | 60 | ]); |
59 | if (array_key_exists('ranges', $data)) { | 61 | $form->submit($data); |
60 | $annotation->setRanges($data['ranges']); | ||
61 | } | ||
62 | 62 | ||
63 | $annotation->setEntry($entry); | 63 | if ($form->isValid()) { |
64 | $em->persist($annotation); | ||
65 | $em->flush(); | ||
64 | 66 | ||
65 | $em->persist($annotation); | 67 | $json = $this->get('serializer')->serialize($annotation, 'json'); |
66 | $em->flush(); | ||
67 | 68 | ||
68 | $json = $this->get('serializer')->serialize($annotation, 'json'); | 69 | return JsonResponse::fromJsonString($json); |
70 | } | ||
69 | 71 | ||
70 | return (new JsonResponse())->setJson($json); | 72 | return $form; |
71 | } | 73 | } |
72 | 74 | ||
73 | /** | 75 | /** |
@@ -86,16 +88,23 @@ class WallabagAnnotationController extends FOSRestController | |||
86 | { | 88 | { |
87 | $data = json_decode($request->getContent(), true); | 89 | $data = json_decode($request->getContent(), true); |
88 | 90 | ||
89 | if (!is_null($data['text'])) { | 91 | $form = $this->get('form.factory')->createNamed('', EditAnnotationType::class, $annotation, [ |
90 | $annotation->setText($data['text']); | 92 | 'csrf_protection' => false, |
91 | } | 93 | 'allow_extra_fields' => true, |
94 | ]); | ||
95 | $form->submit($data); | ||
92 | 96 | ||
93 | $em = $this->getDoctrine()->getManager(); | 97 | if ($form->isValid()) { |
94 | $em->flush(); | 98 | $em = $this->getDoctrine()->getManager(); |
99 | $em->persist($annotation); | ||
100 | $em->flush(); | ||
95 | 101 | ||
96 | $json = $this->get('serializer')->serialize($annotation, 'json'); | 102 | $json = $this->get('serializer')->serialize($annotation, 'json'); |
97 | 103 | ||
98 | return (new JsonResponse())->setJson($json); | 104 | return JsonResponse::fromJsonString($json); |
105 | } | ||
106 | |||
107 | return $form; | ||
99 | } | 108 | } |
100 | 109 | ||
101 | /** | 110 | /** |
diff --git a/src/Wallabag/AnnotationBundle/Entity/Annotation.php b/src/Wallabag/AnnotationBundle/Entity/Annotation.php index 0838f5aa..c8e41649 100644 --- a/src/Wallabag/AnnotationBundle/Entity/Annotation.php +++ b/src/Wallabag/AnnotationBundle/Entity/Annotation.php | |||
@@ -8,6 +8,7 @@ use JMS\Serializer\Annotation\Exclude; | |||
8 | use JMS\Serializer\Annotation\VirtualProperty; | 8 | use JMS\Serializer\Annotation\VirtualProperty; |
9 | use JMS\Serializer\Annotation\SerializedName; | 9 | use JMS\Serializer\Annotation\SerializedName; |
10 | use JMS\Serializer\Annotation\Groups; | 10 | use JMS\Serializer\Annotation\Groups; |
11 | use Symfony\Component\Validator\Constraints as Assert; | ||
11 | use Wallabag\UserBundle\Entity\User; | 12 | use Wallabag\UserBundle\Entity\User; |
12 | use Wallabag\CoreBundle\Entity\Entry; | 13 | use Wallabag\CoreBundle\Entity\Entry; |
13 | 14 | ||
@@ -56,7 +57,11 @@ class Annotation | |||
56 | /** | 57 | /** |
57 | * @var string | 58 | * @var string |
58 | * | 59 | * |
59 | * @ORM\Column(name="quote", type="string") | 60 | * @Assert\Length( |
61 | * max = 10000, | ||
62 | * maxMessage = "validator.quote_length_too_high" | ||
63 | * ) | ||
64 | * @ORM\Column(name="quote", type="text") | ||
60 | * | 65 | * |
61 | * @Groups({"entries_for_user", "export_all"}) | 66 | * @Groups({"entries_for_user", "export_all"}) |
62 | */ | 67 | */ |
diff --git a/src/Wallabag/AnnotationBundle/Form/EditAnnotationType.php b/src/Wallabag/AnnotationBundle/Form/EditAnnotationType.php new file mode 100644 index 00000000..3b587478 --- /dev/null +++ b/src/Wallabag/AnnotationBundle/Form/EditAnnotationType.php | |||
@@ -0,0 +1,18 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\AnnotationBundle\Form; | ||
4 | |||
5 | use Symfony\Component\Form\AbstractType; | ||
6 | use Symfony\Component\Form\FormBuilderInterface; | ||
7 | |||
8 | class EditAnnotationType extends AbstractType | ||
9 | { | ||
10 | public function buildForm(FormBuilderInterface $builder, array $options) | ||
11 | { | ||
12 | $builder | ||
13 | ->add('text', null, [ | ||
14 | 'empty_data' => '', | ||
15 | ]) | ||
16 | ; | ||
17 | } | ||
18 | } | ||
diff --git a/src/Wallabag/AnnotationBundle/Form/NewAnnotationType.php b/src/Wallabag/AnnotationBundle/Form/NewAnnotationType.php new file mode 100644 index 00000000..c73c3ded --- /dev/null +++ b/src/Wallabag/AnnotationBundle/Form/NewAnnotationType.php | |||
@@ -0,0 +1,35 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\AnnotationBundle\Form; | ||
4 | |||
5 | use Symfony\Component\Form\AbstractType; | ||
6 | use Symfony\Component\Form\Extension\Core\Type\CollectionType; | ||
7 | use Symfony\Component\Form\FormBuilderInterface; | ||
8 | use Symfony\Component\OptionsResolver\OptionsResolver; | ||
9 | use Wallabag\AnnotationBundle\Entity\Annotation; | ||
10 | |||
11 | class NewAnnotationType extends AbstractType | ||
12 | { | ||
13 | public function buildForm(FormBuilderInterface $builder, array $options) | ||
14 | { | ||
15 | $builder | ||
16 | ->add('text', null, [ | ||
17 | 'empty_data' => '', | ||
18 | ]) | ||
19 | ->add('quote', null, [ | ||
20 | 'empty_data' => null, | ||
21 | ]) | ||
22 | ->add('ranges', CollectionType::class, [ | ||
23 | 'entry_type' => RangeType::class, | ||
24 | 'allow_add' => true, | ||
25 | ]) | ||
26 | ; | ||
27 | } | ||
28 | |||
29 | public function configureOptions(OptionsResolver $resolver) | ||
30 | { | ||
31 | $resolver->setDefaults([ | ||
32 | 'data_class' => Annotation::class, | ||
33 | ]); | ||
34 | } | ||
35 | } | ||
diff --git a/src/Wallabag/AnnotationBundle/Form/RangeType.php b/src/Wallabag/AnnotationBundle/Form/RangeType.php new file mode 100644 index 00000000..0647375e --- /dev/null +++ b/src/Wallabag/AnnotationBundle/Form/RangeType.php | |||
@@ -0,0 +1,19 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\AnnotationBundle\Form; | ||
4 | |||
5 | use Symfony\Component\Form\AbstractType; | ||
6 | use Symfony\Component\Form\FormBuilderInterface; | ||
7 | |||
8 | class RangeType extends AbstractType | ||
9 | { | ||
10 | public function buildForm(FormBuilderInterface $builder, array $options) | ||
11 | { | ||
12 | $builder | ||
13 | ->add('start') | ||
14 | ->add('startOffset') | ||
15 | ->add('end') | ||
16 | ->add('endOffset') | ||
17 | ; | ||
18 | } | ||
19 | } | ||
diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 93c8157e..09b73ccb 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php | |||
@@ -299,65 +299,18 @@ class EntryRestController extends WallabagRestController | |||
299 | $this->validateAuthentication(); | 299 | $this->validateAuthentication(); |
300 | 300 | ||
301 | $url = $request->request->get('url'); | 301 | $url = $request->request->get('url'); |
302 | $title = $request->request->get('title'); | ||
303 | $tags = $request->request->get('tags', []); | ||
304 | $isArchived = $request->request->get('archive'); | ||
305 | $isStarred = $request->request->get('starred'); | ||
306 | $content = $request->request->get('content'); | ||
307 | $language = $request->request->get('language'); | ||
308 | $picture = $request->request->get('preview_picture'); | ||
309 | $publishedAt = $request->request->get('published_at'); | ||
310 | $authors = $request->request->get('authors', ''); | ||
311 | 302 | ||
312 | $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId($url, $this->getUser()->getId()); | 303 | $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId( |
304 | $url, | ||
305 | $this->getUser()->getId() | ||
306 | ); | ||
313 | 307 | ||
314 | if (false === $entry) { | 308 | if (false === $entry) { |
315 | $entry = new Entry($this->getUser()); | 309 | $entry = new Entry($this->getUser()); |
316 | } | ||
317 | |||
318 | try { | ||
319 | $this->get('wallabag_core.content_proxy')->updateEntry( | ||
320 | $entry, | ||
321 | $url, | ||
322 | [ | ||
323 | 'title' => $title, | ||
324 | 'html' => $content, | ||
325 | 'url' => $url, | ||
326 | 'language' => $language, | ||
327 | 'date' => $publishedAt, | ||
328 | // faking the preview picture | ||
329 | 'open_graph' => [ | ||
330 | 'og_image' => $picture, | ||
331 | ], | ||
332 | 'authors' => explode(',', $authors), | ||
333 | ] | ||
334 | ); | ||
335 | } catch (\Exception $e) { | ||
336 | $this->get('logger')->error('Error while saving an entry', [ | ||
337 | 'exception' => $e, | ||
338 | 'entry' => $entry, | ||
339 | ]); | ||
340 | $entry->setUrl($url); | 310 | $entry->setUrl($url); |
341 | } | 311 | } |
342 | 312 | ||
343 | if (!empty($tags)) { | 313 | $this->upsertEntry($entry, $request); |
344 | $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags); | ||
345 | } | ||
346 | |||
347 | if (!is_null($isStarred)) { | ||
348 | $entry->setStarred((bool) $isStarred); | ||
349 | } | ||
350 | |||
351 | if (!is_null($isArchived)) { | ||
352 | $entry->setArchived((bool) $isArchived); | ||
353 | } | ||
354 | |||
355 | $em = $this->getDoctrine()->getManager(); | ||
356 | $em->persist($entry); | ||
357 | $em->flush(); | ||
358 | |||
359 | // entry saved, dispatch event about it! | ||
360 | $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); | ||
361 | 314 | ||
362 | return $this->sendResponse($entry); | 315 | return $this->sendResponse($entry); |
363 | } | 316 | } |
@@ -374,6 +327,11 @@ class EntryRestController extends WallabagRestController | |||
374 | * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."}, | 327 | * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."}, |
375 | * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="archived the entry."}, | 328 | * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="archived the entry."}, |
376 | * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="starred the entry."}, | 329 | * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="starred the entry."}, |
330 | * {"name"="content", "dataType"="string", "required"=false, "description"="Content of the entry"}, | ||
331 | * {"name"="language", "dataType"="string", "required"=false, "description"="Language of the entry"}, | ||
332 | * {"name"="preview_picture", "dataType"="string", "required"=false, "description"="Preview picture of the entry"}, | ||
333 | * {"name"="published_at", "dataType"="datetime|integer", "format"="YYYY-MM-DDTHH:II:SS+TZ or a timestamp", "required"=false, "description"="Published date of the entry"}, | ||
334 | * {"name"="authors", "dataType"="string", "format"="Name Firstname,author2,author3", "required"=false, "description"="Authors of the entry"}, | ||
377 | * } | 335 | * } |
378 | * ) | 336 | * ) |
379 | * | 337 | * |
@@ -384,29 +342,7 @@ class EntryRestController extends WallabagRestController | |||
384 | $this->validateAuthentication(); | 342 | $this->validateAuthentication(); |
385 | $this->validateUserAccess($entry->getUser()->getId()); | 343 | $this->validateUserAccess($entry->getUser()->getId()); |
386 | 344 | ||
387 | $title = $request->request->get('title'); | 345 | $this->upsertEntry($entry, $request, true); |
388 | $isArchived = $request->request->get('archive'); | ||
389 | $isStarred = $request->request->get('starred'); | ||
390 | |||
391 | if (!is_null($title)) { | ||
392 | $entry->setTitle($title); | ||
393 | } | ||
394 | |||
395 | if (!is_null($isArchived)) { | ||
396 | $entry->setArchived((bool) $isArchived); | ||
397 | } | ||
398 | |||
399 | if (!is_null($isStarred)) { | ||
400 | $entry->setStarred((bool) $isStarred); | ||
401 | } | ||
402 | |||
403 | $tags = $request->request->get('tags', ''); | ||
404 | if (!empty($tags)) { | ||
405 | $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags); | ||
406 | } | ||
407 | |||
408 | $em = $this->getDoctrine()->getManager(); | ||
409 | $em->flush(); | ||
410 | 346 | ||
411 | return $this->sendResponse($entry); | 347 | return $this->sendResponse($entry); |
412 | } | 348 | } |
@@ -673,4 +609,68 @@ class EntryRestController extends WallabagRestController | |||
673 | 609 | ||
674 | return (new JsonResponse())->setJson($json); | 610 | return (new JsonResponse())->setJson($json); |
675 | } | 611 | } |
612 | |||
613 | /** | ||
614 | * Update or Insert a new entry. | ||
615 | * | ||
616 | * @param Entry $entry | ||
617 | * @param Request $request | ||
618 | * @param bool $disableContentUpdate If we don't want the content to be update by fetching the url (used when patching instead of posting) | ||
619 | */ | ||
620 | private function upsertEntry(Entry $entry, Request $request, $disableContentUpdate = false) | ||
621 | { | ||
622 | $title = $request->request->get('title'); | ||
623 | $tags = $request->request->get('tags', []); | ||
624 | $isArchived = $request->request->get('archive'); | ||
625 | $isStarred = $request->request->get('starred'); | ||
626 | $content = $request->request->get('content'); | ||
627 | $language = $request->request->get('language'); | ||
628 | $picture = $request->request->get('preview_picture'); | ||
629 | $publishedAt = $request->request->get('published_at'); | ||
630 | $authors = $request->request->get('authors', ''); | ||
631 | |||
632 | try { | ||
633 | $this->get('wallabag_core.content_proxy')->updateEntry( | ||
634 | $entry, | ||
635 | $entry->getUrl(), | ||
636 | [ | ||
637 | 'title' => !empty($title) ? $title : $entry->getTitle(), | ||
638 | 'html' => !empty($content) ? $content : $entry->getContent(), | ||
639 | 'url' => $entry->getUrl(), | ||
640 | 'language' => !empty($language) ? $language : $entry->getLanguage(), | ||
641 | 'date' => !empty($publishedAt) ? $publishedAt : $entry->getPublishedAt(), | ||
642 | // faking the open graph preview picture | ||
643 | 'open_graph' => [ | ||
644 | 'og_image' => !empty($picture) ? $picture : $entry->getPreviewPicture(), | ||
645 | ], | ||
646 | 'authors' => is_string($authors) ? explode(',', $authors) : $entry->getPublishedBy(), | ||
647 | ], | ||
648 | $disableContentUpdate | ||
649 | ); | ||
650 | } catch (\Exception $e) { | ||
651 | $this->get('logger')->error('Error while saving an entry', [ | ||
652 | 'exception' => $e, | ||
653 | 'entry' => $entry, | ||
654 | ]); | ||
655 | } | ||
656 | |||
657 | if (!is_null($isArchived)) { | ||
658 | $entry->setArchived((bool) $isArchived); | ||
659 | } | ||
660 | |||
661 | if (!is_null($isStarred)) { | ||
662 | $entry->setStarred((bool) $isStarred); | ||
663 | } | ||
664 | |||
665 | if (!empty($tags)) { | ||
666 | $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags); | ||
667 | } | ||
668 | |||
669 | $em = $this->getDoctrine()->getManager(); | ||
670 | $em->persist($entry); | ||
671 | $em->flush(); | ||
672 | |||
673 | // entry saved, dispatch event about it! | ||
674 | $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); | ||
675 | } | ||
676 | } | 676 | } |
diff --git a/src/Wallabag/ApiBundle/Controller/UserRestController.php b/src/Wallabag/ApiBundle/Controller/UserRestController.php index 8f675b8d..7471f5f6 100644 --- a/src/Wallabag/ApiBundle/Controller/UserRestController.php +++ b/src/Wallabag/ApiBundle/Controller/UserRestController.php | |||
@@ -9,6 +9,7 @@ use Nelmio\ApiDocBundle\Annotation\ApiDoc; | |||
9 | use Symfony\Component\HttpFoundation\Request; | 9 | use Symfony\Component\HttpFoundation\Request; |
10 | use Symfony\Component\HttpFoundation\JsonResponse; | 10 | use Symfony\Component\HttpFoundation\JsonResponse; |
11 | use Wallabag\UserBundle\Entity\User; | 11 | use Wallabag\UserBundle\Entity\User; |
12 | use Wallabag\ApiBundle\Entity\Client; | ||
12 | 13 | ||
13 | class UserRestController extends WallabagRestController | 14 | class UserRestController extends WallabagRestController |
14 | { | 15 | { |
@@ -27,13 +28,14 @@ class UserRestController extends WallabagRestController | |||
27 | } | 28 | } |
28 | 29 | ||
29 | /** | 30 | /** |
30 | * Register an user. | 31 | * Register an user and create a client. |
31 | * | 32 | * |
32 | * @ApiDoc( | 33 | * @ApiDoc( |
33 | * requirements={ | 34 | * requirements={ |
34 | * {"name"="username", "dataType"="string", "required"=true, "description"="The user's username"}, | 35 | * {"name"="username", "dataType"="string", "required"=true, "description"="The user's username"}, |
35 | * {"name"="password", "dataType"="string", "required"=true, "description"="The user's password"}, | 36 | * {"name"="password", "dataType"="string", "required"=true, "description"="The user's password"}, |
36 | * {"name"="email", "dataType"="string", "required"=true, "description"="The user's email"} | 37 | * {"name"="email", "dataType"="string", "required"=true, "description"="The user's email"}, |
38 | * {"name"="client_name", "dataType"="string", "required"=true, "description"="The client name (to be used by your app)"} | ||
37 | * } | 39 | * } |
38 | * ) | 40 | * ) |
39 | * | 41 | * |
@@ -97,29 +99,38 @@ class UserRestController extends WallabagRestController | |||
97 | ->setStatusCode(JsonResponse::HTTP_BAD_REQUEST); | 99 | ->setStatusCode(JsonResponse::HTTP_BAD_REQUEST); |
98 | } | 100 | } |
99 | 101 | ||
102 | // create a default client | ||
103 | $client = new Client($user); | ||
104 | $client->setName($request->request->get('client_name', 'Default client')); | ||
105 | |||
106 | $this->getDoctrine()->getManager()->persist($client); | ||
107 | |||
108 | $user->addClient($client); | ||
109 | |||
100 | $userManager->updateUser($user); | 110 | $userManager->updateUser($user); |
101 | 111 | ||
102 | // dispatch a created event so the associated config will be created | 112 | // dispatch a created event so the associated config will be created |
103 | $event = new UserEvent($user, $request); | 113 | $event = new UserEvent($user, $request); |
104 | $this->get('event_dispatcher')->dispatch(FOSUserEvents::USER_CREATED, $event); | 114 | $this->get('event_dispatcher')->dispatch(FOSUserEvents::USER_CREATED, $event); |
105 | 115 | ||
106 | return $this->sendUser($user, JsonResponse::HTTP_CREATED); | 116 | return $this->sendUser($user, 'user_api_with_client', JsonResponse::HTTP_CREATED); |
107 | } | 117 | } |
108 | 118 | ||
109 | /** | 119 | /** |
110 | * Send user response. | 120 | * Send user response. |
111 | * | 121 | * |
112 | * @param User $user | 122 | * @param User $user |
113 | * @param int $status HTTP Status code to send | 123 | * @param string $group Used to define with serialized group might be used |
124 | * @param int $status HTTP Status code to send | ||
114 | * | 125 | * |
115 | * @return JsonResponse | 126 | * @return JsonResponse |
116 | */ | 127 | */ |
117 | private function sendUser(User $user, $status = JsonResponse::HTTP_OK) | 128 | private function sendUser(User $user, $group = 'user_api', $status = JsonResponse::HTTP_OK) |
118 | { | 129 | { |
119 | $json = $this->get('serializer')->serialize( | 130 | $json = $this->get('serializer')->serialize( |
120 | $user, | 131 | $user, |
121 | 'json', | 132 | 'json', |
122 | SerializationContext::create()->setGroups(['user_api']) | 133 | SerializationContext::create()->setGroups([$group]) |
123 | ); | 134 | ); |
124 | 135 | ||
125 | return (new JsonResponse()) | 136 | return (new JsonResponse()) |
diff --git a/src/Wallabag/ApiBundle/Entity/Client.php b/src/Wallabag/ApiBundle/Entity/Client.php index 9ed9f980..c15fd3fa 100644 --- a/src/Wallabag/ApiBundle/Entity/Client.php +++ b/src/Wallabag/ApiBundle/Entity/Client.php | |||
@@ -5,6 +5,9 @@ namespace Wallabag\ApiBundle\Entity; | |||
5 | use Doctrine\ORM\Mapping as ORM; | 5 | use Doctrine\ORM\Mapping as ORM; |
6 | use FOS\OAuthServerBundle\Entity\Client as BaseClient; | 6 | use FOS\OAuthServerBundle\Entity\Client as BaseClient; |
7 | use Wallabag\UserBundle\Entity\User; | 7 | use Wallabag\UserBundle\Entity\User; |
8 | use JMS\Serializer\Annotation\Groups; | ||
9 | use JMS\Serializer\Annotation\SerializedName; | ||
10 | use JMS\Serializer\Annotation\VirtualProperty; | ||
8 | 11 | ||
9 | /** | 12 | /** |
10 | * @ORM\Table("oauth2_clients") | 13 | * @ORM\Table("oauth2_clients") |
@@ -23,6 +26,8 @@ class Client extends BaseClient | |||
23 | * @var string | 26 | * @var string |
24 | * | 27 | * |
25 | * @ORM\Column(name="name", type="text", nullable=false) | 28 | * @ORM\Column(name="name", type="text", nullable=false) |
29 | * | ||
30 | * @Groups({"user_api_with_client"}) | ||
26 | */ | 31 | */ |
27 | protected $name; | 32 | protected $name; |
28 | 33 | ||
@@ -37,6 +42,14 @@ class Client extends BaseClient | |||
37 | protected $accessTokens; | 42 | protected $accessTokens; |
38 | 43 | ||
39 | /** | 44 | /** |
45 | * @var string | ||
46 | * | ||
47 | * @SerializedName("client_secret") | ||
48 | * @Groups({"user_api_with_client"}) | ||
49 | */ | ||
50 | protected $secret; | ||
51 | |||
52 | /** | ||
40 | * @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User", inversedBy="clients") | 53 | * @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User", inversedBy="clients") |
41 | */ | 54 | */ |
42 | private $user; | 55 | private $user; |
@@ -78,4 +91,14 @@ class Client extends BaseClient | |||
78 | { | 91 | { |
79 | return $this->user; | 92 | return $this->user; |
80 | } | 93 | } |
94 | |||
95 | /** | ||
96 | * @VirtualProperty | ||
97 | * @SerializedName("client_id") | ||
98 | * @Groups({"user_api_with_client"}) | ||
99 | */ | ||
100 | public function getClientId() | ||
101 | { | ||
102 | return $this->getId().'_'.$this->getRandomId(); | ||
103 | } | ||
81 | } | 104 | } |
diff --git a/src/Wallabag/CoreBundle/Command/ShowUserCommand.php b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php new file mode 100644 index 00000000..0eeaabc4 --- /dev/null +++ b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php | |||
@@ -0,0 +1,77 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Command; | ||
4 | |||
5 | use Doctrine\ORM\NoResultException; | ||
6 | use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; | ||
7 | use Symfony\Component\Console\Input\InputArgument; | ||
8 | use Symfony\Component\Console\Input\InputInterface; | ||
9 | use Symfony\Component\Console\Output\OutputInterface; | ||
10 | use Wallabag\UserBundle\Entity\User; | ||
11 | |||
12 | class ShowUserCommand extends ContainerAwareCommand | ||
13 | { | ||
14 | /** @var OutputInterface */ | ||
15 | protected $output; | ||
16 | |||
17 | protected function configure() | ||
18 | { | ||
19 | $this | ||
20 | ->setName('wallabag:user:show') | ||
21 | ->setDescription('Show user details') | ||
22 | ->setHelp('This command shows the details for an user') | ||
23 | ->addArgument( | ||
24 | 'username', | ||
25 | InputArgument::REQUIRED, | ||
26 | 'User to show details for' | ||
27 | ); | ||
28 | } | ||
29 | |||
30 | protected function execute(InputInterface $input, OutputInterface $output) | ||
31 | { | ||
32 | $this->output = $output; | ||
33 | |||
34 | $username = $input->getArgument('username'); | ||
35 | |||
36 | try { | ||
37 | $user = $this->getUser($username); | ||
38 | $this->showUser($user); | ||
39 | } catch (NoResultException $e) { | ||
40 | $output->writeln(sprintf('<error>User "%s" not found.</error>', $username)); | ||
41 | |||
42 | return 1; | ||
43 | } | ||
44 | |||
45 | return 0; | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * @param User $user | ||
50 | */ | ||
51 | private function showUser(User $user) | ||
52 | { | ||
53 | $this->output->writeln(sprintf('Username : %s', $user->getUsername())); | ||
54 | $this->output->writeln(sprintf('Email : %s', $user->getEmail())); | ||
55 | $this->output->writeln(sprintf('Display name : %s', $user->getName())); | ||
56 | $this->output->writeln(sprintf('Creation date : %s', $user->getCreatedAt()->format('Y-m-d H:i:s'))); | ||
57 | $this->output->writeln(sprintf('Last login : %s', $user->getLastLogin() !== null ? $user->getLastLogin()->format('Y-m-d H:i:s') : 'never')); | ||
58 | $this->output->writeln(sprintf('2FA activated: %s', $user->isTwoFactorAuthentication() ? 'yes' : 'no')); | ||
59 | } | ||
60 | |||
61 | /** | ||
62 | * Fetches a user from its username. | ||
63 | * | ||
64 | * @param string $username | ||
65 | * | ||
66 | * @return \Wallabag\UserBundle\Entity\User | ||
67 | */ | ||
68 | private function getUser($username) | ||
69 | { | ||
70 | return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username); | ||
71 | } | ||
72 | |||
73 | private function getDoctrine() | ||
74 | { | ||
75 | return $this->getContainer()->get('doctrine'); | ||
76 | } | ||
77 | } | ||
diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index bfaa1976..0c971863 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php | |||
@@ -7,6 +7,9 @@ use Psr\Log\LoggerInterface; | |||
7 | use Wallabag\CoreBundle\Entity\Entry; | 7 | use Wallabag\CoreBundle\Entity\Entry; |
8 | use Wallabag\CoreBundle\Tools\Utils; | 8 | use Wallabag\CoreBundle\Tools\Utils; |
9 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | 9 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; |
10 | use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint; | ||
11 | use Symfony\Component\Validator\Constraints\Url as UrlConstraint; | ||
12 | use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
10 | 13 | ||
11 | /** | 14 | /** |
12 | * This kind of proxy class take care of getting the content from an url | 15 | * This kind of proxy class take care of getting the content from an url |
@@ -16,15 +19,17 @@ class ContentProxy | |||
16 | { | 19 | { |
17 | protected $graby; | 20 | protected $graby; |
18 | protected $tagger; | 21 | protected $tagger; |
22 | protected $validator; | ||
19 | protected $logger; | 23 | protected $logger; |
20 | protected $mimeGuesser; | 24 | protected $mimeGuesser; |
21 | protected $fetchingErrorMessage; | 25 | protected $fetchingErrorMessage; |
22 | protected $eventDispatcher; | 26 | protected $eventDispatcher; |
23 | 27 | ||
24 | public function __construct(Graby $graby, RuleBasedTagger $tagger, LoggerInterface $logger, $fetchingErrorMessage) | 28 | public function __construct(Graby $graby, RuleBasedTagger $tagger, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage) |
25 | { | 29 | { |
26 | $this->graby = $graby; | 30 | $this->graby = $graby; |
27 | $this->tagger = $tagger; | 31 | $this->tagger = $tagger; |
32 | $this->validator = $validator; | ||
28 | $this->logger = $logger; | 33 | $this->logger = $logger; |
29 | $this->mimeGuesser = new MimeTypeExtensionGuesser(); | 34 | $this->mimeGuesser = new MimeTypeExtensionGuesser(); |
30 | $this->fetchingErrorMessage = $fetchingErrorMessage; | 35 | $this->fetchingErrorMessage = $fetchingErrorMessage; |
@@ -105,7 +110,7 @@ class ContentProxy | |||
105 | } | 110 | } |
106 | } | 111 | } |
107 | 112 | ||
108 | if (!empty($content['authors'])) { | 113 | if (!empty($content['authors']) && is_array($content['authors'])) { |
109 | $entry->setPublishedBy($content['authors']); | 114 | $entry->setPublishedBy($content['authors']); |
110 | } | 115 | } |
111 | 116 | ||
@@ -113,7 +118,24 @@ class ContentProxy | |||
113 | $entry->setHeaders($content['all_headers']); | 118 | $entry->setHeaders($content['all_headers']); |
114 | } | 119 | } |
115 | 120 | ||
116 | $entry->setLanguage(isset($content['language']) ? $content['language'] : ''); | 121 | $this->validateAndSetLanguage( |
122 | $entry, | ||
123 | isset($content['language']) ? $content['language'] : '' | ||
124 | ); | ||
125 | |||
126 | $this->validateAndSetPreviewPicture( | ||
127 | $entry, | ||
128 | isset($content['open_graph']['og_image']) ? $content['open_graph']['og_image'] : '' | ||
129 | ); | ||
130 | |||
131 | // if content is an image, define it as a preview too | ||
132 | if (!empty($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
133 | $this->validateAndSetPreviewPicture( | ||
134 | $entry, | ||
135 | $content['url'] | ||
136 | ); | ||
137 | } | ||
138 | |||
117 | $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); | 139 | $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); |
118 | $entry->setReadingTime(Utils::getReadingTime($html)); | 140 | $entry->setReadingTime(Utils::getReadingTime($html)); |
119 | 141 | ||
@@ -122,15 +144,6 @@ class ContentProxy | |||
122 | $entry->setDomainName($domainName); | 144 | $entry->setDomainName($domainName); |
123 | } | 145 | } |
124 | 146 | ||
125 | if (!empty($content['open_graph']['og_image'])) { | ||
126 | $entry->setPreviewPicture($content['open_graph']['og_image']); | ||
127 | } | ||
128 | |||
129 | // if content is an image define as a preview too | ||
130 | if (!empty($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
131 | $entry->setPreviewPicture($content['url']); | ||
132 | } | ||
133 | |||
134 | try { | 147 | try { |
135 | $this->tagger->tag($entry); | 148 | $this->tagger->tag($entry); |
136 | } catch (\Exception $e) { | 149 | } catch (\Exception $e) { |
@@ -152,4 +165,52 @@ class ContentProxy | |||
152 | { | 165 | { |
153 | return !empty($content['title']) && !empty($content['html']) && !empty($content['url']); | 166 | return !empty($content['title']) && !empty($content['html']) && !empty($content['url']); |
154 | } | 167 | } |
168 | |||
169 | /** | ||
170 | * Use a Symfony validator to ensure the language is well formatted. | ||
171 | * | ||
172 | * @param Entry $entry | ||
173 | * @param string $value Language to validate | ||
174 | */ | ||
175 | private function validateAndSetLanguage($entry, $value) | ||
176 | { | ||
177 | // some lang are defined as fr-FR, es-ES. | ||
178 | // replacing - by _ might increase language support | ||
179 | $value = str_replace('-', '_', $value); | ||
180 | |||
181 | $errors = $this->validator->validate( | ||
182 | $value, | ||
183 | (new LocaleConstraint()) | ||
184 | ); | ||
185 | |||
186 | if (0 === count($errors)) { | ||
187 | $entry->setLanguage($value); | ||
188 | |||
189 | return; | ||
190 | } | ||
191 | |||
192 | $this->logger->warning('Language validation failed. '.(string) $errors); | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Use a Symfony validator to ensure the preview picture is a real url. | ||
197 | * | ||
198 | * @param Entry $entry | ||
199 | * @param string $value URL to validate | ||
200 | */ | ||
201 | private function validateAndSetPreviewPicture($entry, $value) | ||
202 | { | ||
203 | $errors = $this->validator->validate( | ||
204 | $value, | ||
205 | (new UrlConstraint()) | ||
206 | ); | ||
207 | |||
208 | if (0 === count($errors)) { | ||
209 | $entry->setPreviewPicture($value); | ||
210 | |||
211 | return; | ||
212 | } | ||
213 | |||
214 | $this->logger->warning('PreviewPicture validation failed. '.(string) $errors); | ||
215 | } | ||
155 | } | 216 | } |
diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index 54e23a05..ed888cdb 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php | |||
@@ -5,6 +5,7 @@ namespace Wallabag\CoreBundle\Helper; | |||
5 | use Psr\Log\LoggerInterface; | 5 | use Psr\Log\LoggerInterface; |
6 | use Symfony\Component\DomCrawler\Crawler; | 6 | use Symfony\Component\DomCrawler\Crawler; |
7 | use GuzzleHttp\Client; | 7 | use GuzzleHttp\Client; |
8 | use GuzzleHttp\Message\Response; | ||
8 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | 9 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; |
9 | use Symfony\Component\Finder\Finder; | 10 | use Symfony\Component\Finder\Finder; |
10 | 11 | ||
@@ -116,13 +117,11 @@ class DownloadImages | |||
116 | return false; | 117 | return false; |
117 | } | 118 | } |
118 | 119 | ||
119 | $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); | 120 | $ext = $this->getExtensionFromResponse($res, $imagePath); |
120 | $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); | 121 | if (false === $res) { |
121 | if (!in_array($ext, ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
122 | $this->logger->error('DownloadImages: Processed image with not allowed extension. Skipping: '.$imagePath); | ||
123 | |||
124 | return false; | 122 | return false; |
125 | } | 123 | } |
124 | |||
126 | $hashImage = hash('crc32', $absolutePath); | 125 | $hashImage = hash('crc32', $absolutePath); |
127 | $localPath = $folderPath.'/'.$hashImage.'.'.$ext; | 126 | $localPath = $folderPath.'/'.$hashImage.'.'.$ext; |
128 | 127 | ||
@@ -237,4 +236,45 @@ class DownloadImages | |||
237 | 236 | ||
238 | return false; | 237 | return false; |
239 | } | 238 | } |
239 | |||
240 | /** | ||
241 | * Retrieve and validate the extension from the response of the url of the image. | ||
242 | * | ||
243 | * @param Response $res Guzzle Response | ||
244 | * @param string $imagePath Path from the src image from the content (used for log only) | ||
245 | * | ||
246 | * @return string|false Extension name or false if validation failed | ||
247 | */ | ||
248 | private function getExtensionFromResponse(Response $res, $imagePath) | ||
249 | { | ||
250 | $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); | ||
251 | $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); | ||
252 | |||
253 | // ok header doesn't have the extension, try a different way | ||
254 | if (empty($ext)) { | ||
255 | $types = [ | ||
256 | 'jpeg' => "\xFF\xD8\xFF", | ||
257 | 'gif' => 'GIF', | ||
258 | 'png' => "\x89\x50\x4e\x47\x0d\x0a", | ||
259 | ]; | ||
260 | $bytes = substr((string) $res->getBody(), 0, 8); | ||
261 | |||
262 | foreach ($types as $type => $header) { | ||
263 | if (0 === strpos($bytes, $header)) { | ||
264 | $ext = $type; | ||
265 | break; | ||
266 | } | ||
267 | } | ||
268 | |||
269 | $this->logger->debug('DownloadImages: Checking extension (alternative)', ['ext' => $ext]); | ||
270 | } | ||
271 | |||
272 | if (!in_array($ext, ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
273 | $this->logger->error('DownloadImages: Processed image with not allowed extension. Skipping: '.$imagePath); | ||
274 | |||
275 | return false; | ||
276 | } | ||
277 | |||
278 | return $ext; | ||
279 | } | ||
240 | } | 280 | } |
diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index 8b00700f..4be79547 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.yml +++ b/src/Wallabag/CoreBundle/Resources/config/services.yml | |||
@@ -90,6 +90,7 @@ services: | |||
90 | arguments: | 90 | arguments: |
91 | - "@wallabag_core.graby" | 91 | - "@wallabag_core.graby" |
92 | - "@wallabag_core.rule_based_tagger" | 92 | - "@wallabag_core.rule_based_tagger" |
93 | - "@validator" | ||
93 | - "@logger" | 94 | - "@logger" |
94 | - '%wallabag_core.fetching_error_message%' | 95 | - '%wallabag_core.fetching_error_message%' |
95 | 96 | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml index 32a8b4a8..c6a84209 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | # password_wrong_value: 'Wrong value for your current password' | 4 | # password_wrong_value: 'Wrong value for your current password' |
5 | # item_per_page_too_high: 'This will certainly kill the app' | 5 | # item_per_page_too_high: 'This will certainly kill the app' |
6 | # rss_limit_too_high: 'This will certainly kill the app' | 6 | # rss_limit_too_high: 'This will certainly kill the app' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml index 37b9888f..c74c00ca 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'Falscher Wert für dein aktuelles Kennwort' | 4 | password_wrong_value: 'Falscher Wert für dein aktuelles Kennwort' |
5 | item_per_page_too_high: 'Dies wird die Anwendung möglicherweise beenden' | 5 | item_per_page_too_high: 'Dies wird die Anwendung möglicherweise beenden' |
6 | rss_limit_too_high: 'Dies wird die Anwendung möglicherweise beenden' | 6 | rss_limit_too_high: 'Dies wird die Anwendung möglicherweise beenden' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml index 29217497..8cc117fe 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'Wrong value for your current password' | 4 | password_wrong_value: 'Wrong value for your current password' |
5 | item_per_page_too_high: 'This will certainly kill the app' | 5 | item_per_page_too_high: 'This will certainly kill the app' |
6 | rss_limit_too_high: 'This will certainly kill the app' | 6 | rss_limit_too_high: 'This will certainly kill the app' |
7 | quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml index 57ddaa5a..97a8edfa 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'Entrada equivocada para su contraseña actual' | 4 | password_wrong_value: 'Entrada equivocada para su contraseña actual' |
5 | item_per_page_too_high: 'Esto matará la aplicación' | 5 | item_per_page_too_high: 'Esto matará la aplicación' |
6 | rss_limit_too_high: 'Esto matará la aplicación' | 6 | rss_limit_too_high: 'Esto matará la aplicación' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml index e0536d18..ef677525 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'رمز فعلی را اشتباه وارد کردهاید' | 4 | password_wrong_value: 'رمز فعلی را اشتباه وارد کردهاید' |
5 | item_per_page_too_high: 'با این تعداد برنامه به فنا میرود' | 5 | item_per_page_too_high: 'با این تعداد برنامه به فنا میرود' |
6 | rss_limit_too_high: 'با این تعداد برنامه به فنا میرود' | 6 | rss_limit_too_high: 'با این تعداد برنامه به فنا میرود' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml index 64574709..f31b4ed2 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: "Votre mot de passe actuel est faux" | 4 | password_wrong_value: "Votre mot de passe actuel est faux" |
5 | item_per_page_too_high: "Ça ne va pas plaire à l’application" | 5 | item_per_page_too_high: "Ça ne va pas plaire à l’application" |
6 | rss_limit_too_high: "Ça ne va pas plaire à l’application" | 6 | rss_limit_too_high: "Ça ne va pas plaire à l’application" |
7 | quote_length_too_high: "La citation est trop longue. Elle doit avoir au maximum {{ limit }} caractères." | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml index d9beb54f..d949cc3b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'Valore inserito per la password corrente errato' | 4 | password_wrong_value: 'Valore inserito per la password corrente errato' |
5 | item_per_page_too_high: 'Questo valore è troppo alto' | 5 | item_per_page_too_high: 'Questo valore è troppo alto' |
6 | rss_limit_too_high: 'Questo valore è troppo alto' | 6 | rss_limit_too_high: 'Questo valore è troppo alto' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml index f92c2708..fb4aa592 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'Vòstre senhal actual es pas bon' | 4 | password_wrong_value: 'Vòstre senhal actual es pas bon' |
5 | item_per_page_too_high: "Aquò li agradarà pas a l'aplicacion" | 5 | item_per_page_too_high: "Aquò li agradarà pas a l'aplicacion" |
6 | rss_limit_too_high: "Aquò li agradarà pas a l'aplicacion" | 6 | rss_limit_too_high: "Aquò li agradarà pas a l'aplicacion" |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml index ffcd5e7f..58d19cc9 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'Twoje obecne hasło jest błędne' | 4 | password_wrong_value: 'Twoje obecne hasło jest błędne' |
5 | item_per_page_too_high: 'To może spowodować problemy z aplikacją' | 5 | item_per_page_too_high: 'To może spowodować problemy z aplikacją' |
6 | rss_limit_too_high: 'To może spowodować problemy z aplikacją' | 6 | rss_limit_too_high: 'To może spowodować problemy z aplikacją' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml index 4eddff10..a8c1f9de 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | password_wrong_value: 'A senha atual informada está errada' | 4 | password_wrong_value: 'A senha atual informada está errada' |
5 | item_per_page_too_high: 'Certamente isso pode matar a aplicação' | 5 | item_per_page_too_high: 'Certamente isso pode matar a aplicação' |
6 | rss_limit_too_high: 'Certamente isso pode matar a aplicação' | 6 | rss_limit_too_high: 'Certamente isso pode matar a aplicação' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml index 59a8cdd8..6840cf11 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | # password_wrong_value: 'Wrong value for your current password' | 4 | # password_wrong_value: 'Wrong value for your current password' |
5 | # item_per_page_too_high: 'This will certainly kill the app' | 5 | # item_per_page_too_high: 'This will certainly kill the app' |
6 | # rss_limit_too_high: 'This will certainly kill the app' | 6 | # rss_limit_too_high: 'This will certainly kill the app' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml index 01388771..e1e7317f 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml | |||
@@ -4,3 +4,4 @@ validator: | |||
4 | # password_wrong_value: 'Wrong value for your current password' | 4 | # password_wrong_value: 'Wrong value for your current password' |
5 | # item_per_page_too_high: 'This will certainly kill the app' | 5 | # item_per_page_too_high: 'This will certainly kill the app' |
6 | # rss_limit_too_high: 'This will certainly kill the app' | 6 | # rss_limit_too_high: 'This will certainly kill the app' |
7 | # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' | ||
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/common/Developer/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/common/Developer/index.html.twig index b3f0affb..528b055c 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/common/Developer/index.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/common/Developer/index.html.twig | |||
@@ -33,7 +33,7 @@ | |||
33 | <table class="striped"> | 33 | <table class="striped"> |
34 | <tr> | 34 | <tr> |
35 | <td>{{ 'developer.existing_clients.field_id'|trans }}</td> | 35 | <td>{{ 'developer.existing_clients.field_id'|trans }}</td> |
36 | <td><strong><code>{{ client.id }}_{{ client.randomId }}</code></strong></td> | 36 | <td><strong><code>{{ client.clientId }}</code></strong></td> |
37 | </tr> | 37 | </tr> |
38 | <tr> | 38 | <tr> |
39 | <td>{{ 'developer.existing_clients.field_secret'|trans }}</td> | 39 | <td>{{ 'developer.existing_clients.field_secret'|trans }}</td> |
diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php index ed6ce331..aba76ca7 100644 --- a/src/Wallabag/UserBundle/Entity/User.php +++ b/src/Wallabag/UserBundle/Entity/User.php | |||
@@ -6,6 +6,7 @@ use Doctrine\Common\Collections\ArrayCollection; | |||
6 | use Doctrine\ORM\Mapping as ORM; | 6 | use Doctrine\ORM\Mapping as ORM; |
7 | use JMS\Serializer\Annotation\Groups; | 7 | use JMS\Serializer\Annotation\Groups; |
8 | use JMS\Serializer\Annotation\XmlRoot; | 8 | use JMS\Serializer\Annotation\XmlRoot; |
9 | use JMS\Serializer\Annotation\Accessor; | ||
9 | use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface; | 10 | use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface; |
10 | use Scheb\TwoFactorBundle\Model\TrustedComputerInterface; | 11 | use Scheb\TwoFactorBundle\Model\TrustedComputerInterface; |
11 | use FOS\UserBundle\Model\User as BaseUser; | 12 | use FOS\UserBundle\Model\User as BaseUser; |
@@ -36,7 +37,7 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
36 | * @ORM\Id | 37 | * @ORM\Id |
37 | * @ORM\GeneratedValue(strategy="AUTO") | 38 | * @ORM\GeneratedValue(strategy="AUTO") |
38 | * | 39 | * |
39 | * @Groups({"user_api"}) | 40 | * @Groups({"user_api", "user_api_with_client"}) |
40 | */ | 41 | */ |
41 | protected $id; | 42 | protected $id; |
42 | 43 | ||
@@ -45,21 +46,21 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
45 | * | 46 | * |
46 | * @ORM\Column(name="name", type="text", nullable=true) | 47 | * @ORM\Column(name="name", type="text", nullable=true) |
47 | * | 48 | * |
48 | * @Groups({"user_api"}) | 49 | * @Groups({"user_api", "user_api_with_client"}) |
49 | */ | 50 | */ |
50 | protected $name; | 51 | protected $name; |
51 | 52 | ||
52 | /** | 53 | /** |
53 | * @var string | 54 | * @var string |
54 | * | 55 | * |
55 | * @Groups({"user_api"}) | 56 | * @Groups({"user_api", "user_api_with_client"}) |
56 | */ | 57 | */ |
57 | protected $username; | 58 | protected $username; |
58 | 59 | ||
59 | /** | 60 | /** |
60 | * @var string | 61 | * @var string |
61 | * | 62 | * |
62 | * @Groups({"user_api"}) | 63 | * @Groups({"user_api", "user_api_with_client"}) |
63 | */ | 64 | */ |
64 | protected $email; | 65 | protected $email; |
65 | 66 | ||
@@ -68,7 +69,7 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
68 | * | 69 | * |
69 | * @ORM\Column(name="created_at", type="datetime") | 70 | * @ORM\Column(name="created_at", type="datetime") |
70 | * | 71 | * |
71 | * @Groups({"user_api"}) | 72 | * @Groups({"user_api", "user_api_with_client"}) |
72 | */ | 73 | */ |
73 | protected $createdAt; | 74 | protected $createdAt; |
74 | 75 | ||
@@ -77,7 +78,7 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
77 | * | 78 | * |
78 | * @ORM\Column(name="updated_at", type="datetime") | 79 | * @ORM\Column(name="updated_at", type="datetime") |
79 | * | 80 | * |
80 | * @Groups({"user_api"}) | 81 | * @Groups({"user_api", "user_api_with_client"}) |
81 | */ | 82 | */ |
82 | protected $updatedAt; | 83 | protected $updatedAt; |
83 | 84 | ||
@@ -97,7 +98,8 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
97 | private $authCode; | 98 | private $authCode; |
98 | 99 | ||
99 | /** | 100 | /** |
100 | * @var bool Enabled yes/no | 101 | * @var bool |
102 | * | ||
101 | * @ORM\Column(type="boolean") | 103 | * @ORM\Column(type="boolean") |
102 | */ | 104 | */ |
103 | private $twoFactorAuthentication = false; | 105 | private $twoFactorAuthentication = false; |
@@ -108,10 +110,20 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
108 | private $trusted; | 110 | private $trusted; |
109 | 111 | ||
110 | /** | 112 | /** |
113 | * @var ArrayCollection | ||
114 | * | ||
111 | * @ORM\OneToMany(targetEntity="Wallabag\ApiBundle\Entity\Client", mappedBy="user", cascade={"remove"}) | 115 | * @ORM\OneToMany(targetEntity="Wallabag\ApiBundle\Entity\Client", mappedBy="user", cascade={"remove"}) |
112 | */ | 116 | */ |
113 | protected $clients; | 117 | protected $clients; |
114 | 118 | ||
119 | /** | ||
120 | * @see getFirstClient() below | ||
121 | * | ||
122 | * @Groups({"user_api_with_client"}) | ||
123 | * @Accessor(getter="getFirstClient") | ||
124 | */ | ||
125 | protected $default_client; | ||
126 | |||
115 | public function __construct() | 127 | public function __construct() |
116 | { | 128 | { |
117 | parent::__construct(); | 129 | parent::__construct(); |
@@ -288,4 +300,16 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
288 | { | 300 | { |
289 | return $this->clients; | 301 | return $this->clients; |
290 | } | 302 | } |
303 | |||
304 | /** | ||
305 | * Only used by the API when creating a new user it'll also return the first client (which was also created at the same time). | ||
306 | * | ||
307 | * @return Client | ||
308 | */ | ||
309 | public function getFirstClient() | ||
310 | { | ||
311 | if (!empty($this->clients)) { | ||
312 | return $this->clients->first(); | ||
313 | } | ||
314 | } | ||
291 | } | 315 | } |
diff --git a/src/Wallabag/UserBundle/EventListener/AuthenticationFailureListener.php b/src/Wallabag/UserBundle/EventListener/AuthenticationFailureListener.php new file mode 100644 index 00000000..10f13233 --- /dev/null +++ b/src/Wallabag/UserBundle/EventListener/AuthenticationFailureListener.php | |||
@@ -0,0 +1,40 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\UserBundle\EventListener; | ||
4 | |||
5 | use Psr\Log\LoggerInterface; | ||
6 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
7 | use Symfony\Component\HttpFoundation\RequestStack; | ||
8 | use Symfony\Component\Security\Core\AuthenticationEvents; | ||
9 | |||
10 | class AuthenticationFailureListener implements EventSubscriberInterface | ||
11 | { | ||
12 | private $requestStack; | ||
13 | private $logger; | ||
14 | |||
15 | public function __construct(RequestStack $requestStack, LoggerInterface $logger) | ||
16 | { | ||
17 | $this->requestStack = $requestStack; | ||
18 | $this->logger = $logger; | ||
19 | } | ||
20 | |||
21 | /** | ||
22 | * {@inheritdoc} | ||
23 | */ | ||
24 | public static function getSubscribedEvents() | ||
25 | { | ||
26 | return [ | ||
27 | AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', | ||
28 | ]; | ||
29 | } | ||
30 | |||
31 | /** | ||
32 | * On failure, add a custom error in log so server admin can configure fail2ban to block IP from people who try to login too much. | ||
33 | */ | ||
34 | public function onAuthenticationFailure() | ||
35 | { | ||
36 | $request = $this->requestStack->getMasterRequest(); | ||
37 | |||
38 | $this->logger->error('Authentication failure for user "'.$request->request->get('_username').'", from IP "'.$request->getClientIp().'", with UA: "'.$request->server->get('HTTP_USER_AGENT').'".'); | ||
39 | } | ||
40 | } | ||
diff --git a/src/Wallabag/UserBundle/Resources/config/services.yml b/src/Wallabag/UserBundle/Resources/config/services.yml index bfba6010..d3925de3 100644 --- a/src/Wallabag/UserBundle/Resources/config/services.yml +++ b/src/Wallabag/UserBundle/Resources/config/services.yml | |||
@@ -35,3 +35,11 @@ services: | |||
35 | - "%wallabag_core.list_mode%" | 35 | - "%wallabag_core.list_mode%" |
36 | tags: | 36 | tags: |
37 | - { name: kernel.event_subscriber } | 37 | - { name: kernel.event_subscriber } |
38 | |||
39 | wallabag_user.listener.authentication_failure_event_listener: | ||
40 | class: Wallabag\UserBundle\EventListener\AuthenticationFailureListener | ||
41 | arguments: | ||
42 | - "@request_stack" | ||
43 | - "@logger" | ||
44 | tags: | ||
45 | - { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure } | ||