aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/Wallabag
diff options
context:
space:
mode:
Diffstat (limited to 'src/Wallabag')
-rw-r--r--src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php49
-rw-r--r--src/Wallabag/AnnotationBundle/Entity/Annotation.php7
-rw-r--r--src/Wallabag/AnnotationBundle/Form/EditAnnotationType.php18
-rw-r--r--src/Wallabag/AnnotationBundle/Form/NewAnnotationType.php35
-rw-r--r--src/Wallabag/AnnotationBundle/Form/RangeType.php19
-rw-r--r--src/Wallabag/ApiBundle/Controller/EntryRestController.php150
-rw-r--r--src/Wallabag/ApiBundle/Controller/UserRestController.php25
-rw-r--r--src/Wallabag/ApiBundle/Entity/Client.php23
-rw-r--r--src/Wallabag/CoreBundle/Command/ShowUserCommand.php77
-rw-r--r--src/Wallabag/CoreBundle/Helper/ContentProxy.php85
-rw-r--r--src/Wallabag/CoreBundle/Helper/DownloadImages.php50
-rw-r--r--src/Wallabag/CoreBundle/Resources/config/services.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.da.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.de.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.en.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.es.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.it.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/common/Developer/index.html.twig2
-rw-r--r--src/Wallabag/UserBundle/Entity/User.php38
-rw-r--r--src/Wallabag/UserBundle/EventListener/AuthenticationFailureListener.php40
-rw-r--r--src/Wallabag/UserBundle/Resources/config/services.yml8
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;
7use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 8use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
9use Wallabag\AnnotationBundle\Entity\Annotation; 9use Wallabag\AnnotationBundle\Entity\Annotation;
10use Wallabag\AnnotationBundle\Form\EditAnnotationType;
11use Wallabag\AnnotationBundle\Form\NewAnnotationType;
10use Wallabag\CoreBundle\Entity\Entry; 12use Wallabag\CoreBundle\Entity\Entry;
11 13
12class WallabagAnnotationController extends FOSRestController 14class 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;
8use JMS\Serializer\Annotation\VirtualProperty; 8use JMS\Serializer\Annotation\VirtualProperty;
9use JMS\Serializer\Annotation\SerializedName; 9use JMS\Serializer\Annotation\SerializedName;
10use JMS\Serializer\Annotation\Groups; 10use JMS\Serializer\Annotation\Groups;
11use Symfony\Component\Validator\Constraints as Assert;
11use Wallabag\UserBundle\Entity\User; 12use Wallabag\UserBundle\Entity\User;
12use Wallabag\CoreBundle\Entity\Entry; 13use 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
3namespace Wallabag\AnnotationBundle\Form;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\FormBuilderInterface;
7
8class 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
3namespace Wallabag\AnnotationBundle\Form;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\Extension\Core\Type\CollectionType;
7use Symfony\Component\Form\FormBuilderInterface;
8use Symfony\Component\OptionsResolver\OptionsResolver;
9use Wallabag\AnnotationBundle\Entity\Annotation;
10
11class 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
3namespace Wallabag\AnnotationBundle\Form;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\FormBuilderInterface;
7
8class 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;
9use Symfony\Component\HttpFoundation\Request; 9use Symfony\Component\HttpFoundation\Request;
10use Symfony\Component\HttpFoundation\JsonResponse; 10use Symfony\Component\HttpFoundation\JsonResponse;
11use Wallabag\UserBundle\Entity\User; 11use Wallabag\UserBundle\Entity\User;
12use Wallabag\ApiBundle\Entity\Client;
12 13
13class UserRestController extends WallabagRestController 14class 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;
5use Doctrine\ORM\Mapping as ORM; 5use Doctrine\ORM\Mapping as ORM;
6use FOS\OAuthServerBundle\Entity\Client as BaseClient; 6use FOS\OAuthServerBundle\Entity\Client as BaseClient;
7use Wallabag\UserBundle\Entity\User; 7use Wallabag\UserBundle\Entity\User;
8use JMS\Serializer\Annotation\Groups;
9use JMS\Serializer\Annotation\SerializedName;
10use 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
3namespace Wallabag\CoreBundle\Command;
4
5use Doctrine\ORM\NoResultException;
6use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface;
10use Wallabag\UserBundle\Entity\User;
11
12class 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;
7use Wallabag\CoreBundle\Entity\Entry; 7use Wallabag\CoreBundle\Entity\Entry;
8use Wallabag\CoreBundle\Tools\Utils; 8use Wallabag\CoreBundle\Tools\Utils;
9use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; 9use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
10use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint;
11use Symfony\Component\Validator\Constraints\Url as UrlConstraint;
12use 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;
5use Psr\Log\LoggerInterface; 5use Psr\Log\LoggerInterface;
6use Symfony\Component\DomCrawler\Crawler; 6use Symfony\Component\DomCrawler\Crawler;
7use GuzzleHttp\Client; 7use GuzzleHttp\Client;
8use GuzzleHttp\Message\Response;
8use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; 9use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
9use Symfony\Component\Finder\Finder; 10use 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;
6use Doctrine\ORM\Mapping as ORM; 6use Doctrine\ORM\Mapping as ORM;
7use JMS\Serializer\Annotation\Groups; 7use JMS\Serializer\Annotation\Groups;
8use JMS\Serializer\Annotation\XmlRoot; 8use JMS\Serializer\Annotation\XmlRoot;
9use JMS\Serializer\Annotation\Accessor;
9use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface; 10use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
10use Scheb\TwoFactorBundle\Model\TrustedComputerInterface; 11use Scheb\TwoFactorBundle\Model\TrustedComputerInterface;
11use FOS\UserBundle\Model\User as BaseUser; 12use 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
3namespace Wallabag\UserBundle\EventListener;
4
5use Psr\Log\LoggerInterface;
6use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7use Symfony\Component\HttpFoundation\RequestStack;
8use Symfony\Component\Security\Core\AuthenticationEvents;
9
10class 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 }