aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php55
-rw-r--r--src/Wallabag/AnnotationBundle/Entity/Annotation.php30
-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/AnnotationBundle/Repository/AnnotationRepository.php51
-rw-r--r--src/Wallabag/ApiBundle/Controller/AnnotationRestController.php4
-rw-r--r--src/Wallabag/ApiBundle/Controller/DeveloperController.php4
-rw-r--r--src/Wallabag/ApiBundle/Controller/EntryRestController.php507
-rw-r--r--src/Wallabag/ApiBundle/Controller/TagRestController.php20
-rw-r--r--src/Wallabag/ApiBundle/Controller/UserRestController.php157
-rw-r--r--src/Wallabag/ApiBundle/Controller/WallabagRestController.php6
-rw-r--r--src/Wallabag/ApiBundle/Entity/Client.php23
-rw-r--r--src/Wallabag/ApiBundle/Form/Type/ClientType.php2
-rw-r--r--src/Wallabag/ApiBundle/Resources/config/routing_rest.yml5
-rw-r--r--src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php117
-rw-r--r--src/Wallabag/CoreBundle/Command/ExportCommand.php24
-rw-r--r--src/Wallabag/CoreBundle/Command/InstallCommand.php281
-rw-r--r--src/Wallabag/CoreBundle/Command/ListUserCommand.php61
-rw-r--r--src/Wallabag/CoreBundle/Command/ReloadEntryCommand.php90
-rw-r--r--src/Wallabag/CoreBundle/Command/ShowUserCommand.php75
-rw-r--r--src/Wallabag/CoreBundle/Command/TagAllCommand.php16
-rw-r--r--src/Wallabag/CoreBundle/Controller/ConfigController.php214
-rw-r--r--src/Wallabag/CoreBundle/Controller/EntryController.php270
-rw-r--r--src/Wallabag/CoreBundle/Controller/ExceptionController.php2
-rw-r--r--src/Wallabag/CoreBundle/Controller/ExportController.php20
-rw-r--r--src/Wallabag/CoreBundle/Controller/RssController.php116
-rw-r--r--src/Wallabag/CoreBundle/Controller/SiteCredentialController.php174
-rw-r--r--src/Wallabag/CoreBundle/Controller/StaticController.php7
-rw-r--r--src/Wallabag/CoreBundle/Controller/TagController.php42
-rw-r--r--src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php166
-rw-r--r--src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php34
-rw-r--r--src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php9
-rw-r--r--src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php7
-rw-r--r--src/Wallabag/CoreBundle/DependencyInjection/Configuration.php18
-rw-r--r--src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php6
-rw-r--r--src/Wallabag/CoreBundle/Entity/Entry.php206
-rw-r--r--src/Wallabag/CoreBundle/Entity/SiteCredential.php188
-rw-r--r--src/Wallabag/CoreBundle/Entity/Tag.php4
-rw-r--r--src/Wallabag/CoreBundle/Entity/TaggingRule.php4
-rw-r--r--src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php8
-rw-r--r--src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php7
-rw-r--r--src/Wallabag/CoreBundle/Event/Subscriber/TablePrefixSubscriber.php6
-rw-r--r--src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php2
-rw-r--r--src/Wallabag/CoreBundle/Form/Type/EditEntryType.php6
-rw-r--r--src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php28
-rw-r--r--src/Wallabag/CoreBundle/Form/Type/SiteCredentialType.php44
-rw-r--r--src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php100
-rw-r--r--src/Wallabag/CoreBundle/Helper/ContentProxy.php238
-rw-r--r--src/Wallabag/CoreBundle/Helper/CryptoProxy.php86
-rw-r--r--src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php2
-rw-r--r--src/Wallabag/CoreBundle/Helper/DownloadImages.php97
-rw-r--r--src/Wallabag/CoreBundle/Helper/EntityTimestampsTrait.php24
-rw-r--r--src/Wallabag/CoreBundle/Helper/EntriesExport.php107
-rw-r--r--src/Wallabag/CoreBundle/Helper/HttpClientFactory.php31
-rw-r--r--src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php11
-rw-r--r--src/Wallabag/CoreBundle/Helper/Redirect.php10
-rw-r--r--src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php10
-rw-r--r--src/Wallabag/CoreBundle/Helper/TagsAssigner.php75
-rw-r--r--src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php2
-rw-r--r--src/Wallabag/CoreBundle/Operator/Doctrine/NotMatches.php25
-rw-r--r--src/Wallabag/CoreBundle/Operator/PHP/Matches.php2
-rw-r--r--src/Wallabag/CoreBundle/Operator/PHP/NotMatches.php21
-rw-r--r--src/Wallabag/CoreBundle/Repository/EntryRepository.php157
-rw-r--r--src/Wallabag/CoreBundle/Repository/SiteCredentialRepository.php47
-rw-r--r--src/Wallabag/CoreBundle/Repository/TagRepository.php42
-rw-r--r--src/Wallabag/CoreBundle/Resources/config/services.yml49
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.da.yml53
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.de.yml167
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.en.yml53
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.es.yml45
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml47
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml185
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.it.yml65
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml199
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml51
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml51
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml53
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml564
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml31
-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.yml3
-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.ru.yml6
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig16
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entries.html.twig34
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entry.html.twig29
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/edit.html.twig60
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/index.html.twig42
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/new.html.twig53
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/Tag/tags.html.twig7
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/layout.html.twig9
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/common/Developer/index.html.twig2
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_rss_link.html.twig6
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig2
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/entries.xml.twig6
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/share.html.twig38
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig48
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig2
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig40
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig4
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/edit.html.twig5
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig37
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig103
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/edit.html.twig60
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/index.html.twig42
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/new.html.twig53
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Tag/tags.html.twig3
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig15
-rw-r--r--src/Wallabag/CoreBundle/Twig/WallabagExtension.php12
-rw-r--r--src/Wallabag/ImportBundle/Command/ImportCommand.php26
-rw-r--r--src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php12
-rw-r--r--src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php10
-rw-r--r--src/Wallabag/ImportBundle/Controller/BrowserController.php43
-rw-r--r--src/Wallabag/ImportBundle/Controller/ChromeController.php16
-rw-r--r--src/Wallabag/ImportBundle/Controller/FirefoxController.php16
-rw-r--r--src/Wallabag/ImportBundle/Controller/ImportController.php6
-rw-r--r--src/Wallabag/ImportBundle/Controller/InstapaperController.php20
-rw-r--r--src/Wallabag/ImportBundle/Controller/PinboardController.php20
-rw-r--r--src/Wallabag/ImportBundle/Controller/PocketController.php46
-rw-r--r--src/Wallabag/ImportBundle/Controller/ReadabilityController.php20
-rw-r--r--src/Wallabag/ImportBundle/Controller/WallabagController.php48
-rw-r--r--src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php16
-rw-r--r--src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php16
-rw-r--r--src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php6
-rw-r--r--src/Wallabag/ImportBundle/Form/Type/UploadImportType.php6
-rw-r--r--src/Wallabag/ImportBundle/Import/AbstractImport.php81
-rw-r--r--src/Wallabag/ImportBundle/Import/BrowserImport.php154
-rw-r--r--src/Wallabag/ImportBundle/Import/ChromeImport.php2
-rw-r--r--src/Wallabag/ImportBundle/Import/FirefoxImport.php2
-rw-r--r--src/Wallabag/ImportBundle/Import/ImportCompilerPass.php2
-rw-r--r--src/Wallabag/ImportBundle/Import/InstapaperImport.php25
-rw-r--r--src/Wallabag/ImportBundle/Import/PinboardImport.php4
-rw-r--r--src/Wallabag/ImportBundle/Import/PocketImport.php20
-rw-r--r--src/Wallabag/ImportBundle/Import/ReadabilityImport.php2
-rw-r--r--src/Wallabag/ImportBundle/Import/WallabagImport.php4
-rw-r--r--src/Wallabag/ImportBundle/Import/WallabagV1Import.php22
-rw-r--r--src/Wallabag/ImportBundle/Import/WallabagV2Import.php4
-rw-r--r--src/Wallabag/ImportBundle/Redis/Producer.php2
-rw-r--r--src/Wallabag/ImportBundle/Resources/config/services.yml10
-rw-r--r--src/Wallabag/ImportBundle/WallabagImportBundle.php2
-rw-r--r--src/Wallabag/UserBundle/Controller/ManageController.php89
-rw-r--r--src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php6
-rw-r--r--src/Wallabag/UserBundle/Entity/User.php99
-rw-r--r--src/Wallabag/UserBundle/EventListener/AuthenticationFailureListener.php40
-rw-r--r--src/Wallabag/UserBundle/EventListener/CreateConfigListener.php3
-rw-r--r--src/Wallabag/UserBundle/EventListener/PasswordResettingListener.php2
-rw-r--r--src/Wallabag/UserBundle/Form/SearchUserType.php29
-rw-r--r--src/Wallabag/UserBundle/Form/UserType.php10
-rw-r--r--src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php2
-rw-r--r--src/Wallabag/UserBundle/Repository/UserRepository.php28
-rw-r--r--src/Wallabag/UserBundle/Resources/config/services.yml10
-rw-r--r--src/Wallabag/UserBundle/Resources/translations/wallabag_user.oc.yml6
-rw-r--r--src/Wallabag/UserBundle/Resources/translations/wallabag_user.ru.yml11
-rw-r--r--src/Wallabag/UserBundle/Resources/views/Manage/index.html.twig77
164 files changed, 5748 insertions, 1963 deletions
diff --git a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
index c13a034f..f3090e65 100644
--- a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
+++ b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
@@ -3,10 +3,12 @@
3namespace Wallabag\AnnotationBundle\Controller; 3namespace Wallabag\AnnotationBundle\Controller;
4 4
5use FOS\RestBundle\Controller\FOSRestController; 5use FOS\RestBundle\Controller\FOSRestController;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
6use Symfony\Component\HttpFoundation\JsonResponse; 7use Symfony\Component\HttpFoundation\JsonResponse;
7use Symfony\Component\HttpFoundation\Request; 8use Symfony\Component\HttpFoundation\Request;
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
@@ -29,7 +31,7 @@ class WallabagAnnotationController extends FOSRestController
29 $total = count($annotationRows); 31 $total = count($annotationRows);
30 $annotations = ['total' => $total, 'rows' => $annotationRows]; 32 $annotations = ['total' => $total, 'rows' => $annotationRows];
31 33
32 $json = $this->get('serializer')->serialize($annotations, 'json'); 34 $json = $this->get('jms_serializer')->serialize($annotations, 'json');
33 35
34 return (new JsonResponse())->setJson($json); 36 return (new JsonResponse())->setJson($json);
35 } 37 }
@@ -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('jms_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('jms_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 /**
@@ -115,7 +124,7 @@ class WallabagAnnotationController extends FOSRestController
115 $em->remove($annotation); 124 $em->remove($annotation);
116 $em->flush(); 125 $em->flush();
117 126
118 $json = $this->get('serializer')->serialize($annotation, 'json'); 127 $json = $this->get('jms_serializer')->serialize($annotation, 'json');
119 128
120 return (new JsonResponse())->setJson($json); 129 return (new JsonResponse())->setJson($json);
121 } 130 }
diff --git a/src/Wallabag/AnnotationBundle/Entity/Annotation.php b/src/Wallabag/AnnotationBundle/Entity/Annotation.php
index 0838f5aa..a180d504 100644
--- a/src/Wallabag/AnnotationBundle/Entity/Annotation.php
+++ b/src/Wallabag/AnnotationBundle/Entity/Annotation.php
@@ -3,13 +3,15 @@
3namespace Wallabag\AnnotationBundle\Entity; 3namespace Wallabag\AnnotationBundle\Entity;
4 4
5use Doctrine\ORM\Mapping as ORM; 5use Doctrine\ORM\Mapping as ORM;
6use JMS\Serializer\Annotation\ExclusionPolicy;
7use JMS\Serializer\Annotation\Exclude; 6use JMS\Serializer\Annotation\Exclude;
8use JMS\Serializer\Annotation\VirtualProperty; 7use JMS\Serializer\Annotation\ExclusionPolicy;
9use JMS\Serializer\Annotation\SerializedName;
10use JMS\Serializer\Annotation\Groups; 8use JMS\Serializer\Annotation\Groups;
11use Wallabag\UserBundle\Entity\User; 9use JMS\Serializer\Annotation\SerializedName;
10use JMS\Serializer\Annotation\VirtualProperty;
11use Symfony\Component\Validator\Constraints as Assert;
12use Wallabag\CoreBundle\Entity\Entry; 12use Wallabag\CoreBundle\Entity\Entry;
13use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
14use Wallabag\UserBundle\Entity\User;
13 15
14/** 16/**
15 * Annotation. 17 * Annotation.
@@ -21,6 +23,8 @@ use Wallabag\CoreBundle\Entity\Entry;
21 */ 23 */
22class Annotation 24class Annotation
23{ 25{
26 use EntityTimestampsTrait;
27
24 /** 28 /**
25 * @var int 29 * @var int
26 * 30 *
@@ -56,7 +60,11 @@ class Annotation
56 /** 60 /**
57 * @var string 61 * @var string
58 * 62 *
59 * @ORM\Column(name="quote", type="string") 63 * @Assert\Length(
64 * max = 10000,
65 * maxMessage = "validator.quote_length_too_high"
66 * )
67 * @ORM\Column(name="quote", type="text")
60 * 68 *
61 * @Groups({"entries_for_user", "export_all"}) 69 * @Groups({"entries_for_user", "export_all"})
62 */ 70 */
@@ -129,18 +137,6 @@ class Annotation
129 } 137 }
130 138
131 /** 139 /**
132 * @ORM\PrePersist
133 * @ORM\PreUpdate
134 */
135 public function timestamps()
136 {
137 if (is_null($this->createdAt)) {
138 $this->createdAt = new \DateTime();
139 }
140 $this->updatedAt = new \DateTime();
141 }
142
143 /**
144 * Get created. 140 * Get created.
145 * 141 *
146 * @return \DateTime 142 * @return \DateTime
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/AnnotationBundle/Repository/AnnotationRepository.php b/src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php
index 8d3f07ee..b44f7e64 100644
--- a/src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php
+++ b/src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php
@@ -3,6 +3,8 @@
3namespace Wallabag\AnnotationBundle\Repository; 3namespace Wallabag\AnnotationBundle\Repository;
4 4
5use Doctrine\ORM\EntityRepository; 5use Doctrine\ORM\EntityRepository;
6use Doctrine\ORM\QueryBuilder;
7use Wallabag\AnnotationBundle\Entity\Annotation;
6 8
7/** 9/**
8 * AnnotationRepository. 10 * AnnotationRepository.
@@ -10,22 +12,6 @@ use Doctrine\ORM\EntityRepository;
10class AnnotationRepository extends EntityRepository 12class AnnotationRepository extends EntityRepository
11{ 13{
12 /** 14 /**
13 * Return a query builder to used by other getBuilderFor* method.
14 *
15 * @param int $userId
16 *
17 * @return QueryBuilder
18 */
19 private function getBuilderByUser($userId)
20 {
21 return $this->createQueryBuilder('a')
22 ->leftJoin('a.user', 'u')
23 ->andWhere('u.id = :userId')->setParameter('userId', $userId)
24 ->orderBy('a.id', 'desc')
25 ;
26 }
27
28 /**
29 * Retrieves all annotations for a user. 15 * Retrieves all annotations for a user.
30 * 16 *
31 * @param int $userId 17 * @param int $userId
@@ -122,4 +108,37 @@ class AnnotationRepository extends EntityRepository
122 ->setParameter('userId', $userId) 108 ->setParameter('userId', $userId)
123 ->execute(); 109 ->execute();
124 } 110 }
111
112 /**
113 * Find all annotations related to archived entries.
114 *
115 * @param $userId
116 *
117 * @return mixed
118 */
119 public function findAllArchivedEntriesByUser($userId)
120 {
121 return $this->createQueryBuilder('a')
122 ->leftJoin('a.entry', 'e')
123 ->where('a.user = :userid')->setParameter(':userid', $userId)
124 ->andWhere('e.isArchived = true')
125 ->getQuery()
126 ->getResult();
127 }
128
129 /**
130 * Return a query builder to used by other getBuilderFor* method.
131 *
132 * @param int $userId
133 *
134 * @return QueryBuilder
135 */
136 private function getBuilderByUser($userId)
137 {
138 return $this->createQueryBuilder('a')
139 ->leftJoin('a.user', 'u')
140 ->andWhere('u.id = :userId')->setParameter('userId', $userId)
141 ->orderBy('a.id', 'desc')
142 ;
143 }
125} 144}
diff --git a/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php b/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php
index 2dd26c07..28d55ba9 100644
--- a/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php
+++ b/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php
@@ -4,10 +4,10 @@ namespace Wallabag\ApiBundle\Controller;
4 4
5use Nelmio\ApiDocBundle\Annotation\ApiDoc; 5use Nelmio\ApiDocBundle\Annotation\ApiDoc;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 6use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpFoundation\JsonResponse; 7use Symfony\Component\HttpFoundation\JsonResponse;
9use Wallabag\CoreBundle\Entity\Entry; 8use Symfony\Component\HttpFoundation\Request;
10use Wallabag\AnnotationBundle\Entity\Annotation; 9use Wallabag\AnnotationBundle\Entity\Annotation;
10use Wallabag\CoreBundle\Entity\Entry;
11 11
12class AnnotationRestController extends WallabagRestController 12class AnnotationRestController extends WallabagRestController
13{ 13{
diff --git a/src/Wallabag/ApiBundle/Controller/DeveloperController.php b/src/Wallabag/ApiBundle/Controller/DeveloperController.php
index 9cb1b626..c7178017 100644
--- a/src/Wallabag/ApiBundle/Controller/DeveloperController.php
+++ b/src/Wallabag/ApiBundle/Controller/DeveloperController.php
@@ -3,8 +3,8 @@
3namespace Wallabag\ApiBundle\Controller; 3namespace Wallabag\ApiBundle\Controller;
4 4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Component\HttpFoundation\Request;
7use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\Request;
8use Wallabag\ApiBundle\Entity\Client; 8use Wallabag\ApiBundle\Entity\Client;
9use Wallabag\ApiBundle\Form\Type\ClientType; 9use Wallabag\ApiBundle\Form\Type\ClientType;
10 10
@@ -75,7 +75,7 @@ class DeveloperController extends Controller
75 */ 75 */
76 public function deleteClientAction(Client $client) 76 public function deleteClientAction(Client $client)
77 { 77 {
78 if (null === $this->getUser() || $client->getUser()->getId() != $this->getUser()->getId()) { 78 if (null === $this->getUser() || $client->getUser()->getId() !== $this->getUser()->getId()) {
79 throw $this->createAccessDeniedException('You can not access this client.'); 79 throw $this->createAccessDeniedException('You can not access this client.');
80 } 80 }
81 81
diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php
index 54c1747c..6f161a08 100644
--- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php
+++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php
@@ -4,22 +4,30 @@ namespace Wallabag\ApiBundle\Controller;
4 4
5use Hateoas\Configuration\Route; 5use Hateoas\Configuration\Route;
6use Hateoas\Representation\Factory\PagerfantaFactory; 6use Hateoas\Representation\Factory\PagerfantaFactory;
7use JMS\Serializer\SerializationContext;
7use Nelmio\ApiDocBundle\Annotation\ApiDoc; 8use Nelmio\ApiDocBundle\Annotation\ApiDoc;
8use Symfony\Component\HttpFoundation\Request;
9use Symfony\Component\HttpFoundation\JsonResponse; 9use Symfony\Component\HttpFoundation\JsonResponse;
10use Symfony\Component\HttpFoundation\Request;
11use Symfony\Component\HttpFoundation\Response;
12use Symfony\Component\HttpKernel\Exception\HttpException;
10use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 13use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
11use Wallabag\CoreBundle\Entity\Entry; 14use Wallabag\CoreBundle\Entity\Entry;
12use Wallabag\CoreBundle\Entity\Tag; 15use Wallabag\CoreBundle\Entity\Tag;
13use Wallabag\CoreBundle\Event\EntrySavedEvent;
14use Wallabag\CoreBundle\Event\EntryDeletedEvent; 16use Wallabag\CoreBundle\Event\EntryDeletedEvent;
17use Wallabag\CoreBundle\Event\EntrySavedEvent;
15 18
16class EntryRestController extends WallabagRestController 19class EntryRestController extends WallabagRestController
17{ 20{
18 /** 21 /**
19 * Check if an entry exist by url. 22 * Check if an entry exist by url.
23 * Return ID if entry(ies) exist (and if you give the return_id parameter).
24 * Otherwise it returns false.
25 *
26 * @todo Remove that `return_id` in the next major release
20 * 27 *
21 * @ApiDoc( 28 * @ApiDoc(
22 * parameters={ 29 * parameters={
30 * {"name"="return_id", "dataType"="string", "required"=false, "format"="1 or 0", "description"="Set 1 if you want to retrieve ID in case entry(ies) exists, 0 by default"},
23 * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="Url to check if it exists"}, 31 * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="Url to check if it exists"},
24 * {"name"="urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Urls (as an array) to check if it exists"} 32 * {"name"="urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Urls (as an array) to check if it exists"}
25 * } 33 * }
@@ -31,6 +39,7 @@ class EntryRestController extends WallabagRestController
31 { 39 {
32 $this->validateAuthentication(); 40 $this->validateAuthentication();
33 41
42 $returnId = (null === $request->query->get('return_id')) ? false : (bool) $request->query->get('return_id');
34 $urls = $request->query->get('urls', []); 43 $urls = $request->query->get('urls', []);
35 44
36 // handle multiple urls first 45 // handle multiple urls first
@@ -41,30 +50,26 @@ class EntryRestController extends WallabagRestController
41 ->getRepository('WallabagCoreBundle:Entry') 50 ->getRepository('WallabagCoreBundle:Entry')
42 ->findByUrlAndUserId($url, $this->getUser()->getId()); 51 ->findByUrlAndUserId($url, $this->getUser()->getId());
43 52
44 $results[$url] = false === $res ? false : true; 53 $results[$url] = $this->returnExistInformation($res, $returnId);
45 } 54 }
46 55
47 $json = $this->get('serializer')->serialize($results, 'json'); 56 return $this->sendResponse($results);
48
49 return (new JsonResponse())->setJson($json);
50 } 57 }
51 58
52 // let's see if it is a simple url? 59 // let's see if it is a simple url?
53 $url = $request->query->get('url', ''); 60 $url = $request->query->get('url', '');
54 61
55 if (empty($url)) { 62 if (empty($url)) {
56 throw $this->createAccessDeniedException('URL is empty?, logged user id: '.$this->getUser()->getId()); 63 throw $this->createAccessDeniedException('URL is empty?, logged user id: ' . $this->getUser()->getId());
57 } 64 }
58 65
59 $res = $this->getDoctrine() 66 $res = $this->getDoctrine()
60 ->getRepository('WallabagCoreBundle:Entry') 67 ->getRepository('WallabagCoreBundle:Entry')
61 ->findByUrlAndUserId($url, $this->getUser()->getId()); 68 ->findByUrlAndUserId($url, $this->getUser()->getId());
62 69
63 $exists = false === $res ? false : true; 70 $exists = $this->returnExistInformation($res, $returnId);
64
65 $json = $this->get('serializer')->serialize(['exists' => $exists], 'json');
66 71
67 return (new JsonResponse())->setJson($json); 72 return $this->sendResponse(['exists' => $exists]);
68 } 73 }
69 74
70 /** 75 /**
@@ -80,6 +85,7 @@ class EntryRestController extends WallabagRestController
80 * {"name"="perPage", "dataType"="integer", "required"=false, "format"="default'30'", "description"="results per page."}, 85 * {"name"="perPage", "dataType"="integer", "required"=false, "format"="default'30'", "description"="results per page."},
81 * {"name"="tags", "dataType"="string", "required"=false, "format"="api,rest", "description"="a list of tags url encoded. Will returns entries that matches ALL tags."}, 86 * {"name"="tags", "dataType"="string", "required"=false, "format"="api,rest", "description"="a list of tags url encoded. Will returns entries that matches ALL tags."},
82 * {"name"="since", "dataType"="integer", "required"=false, "format"="default '0'", "description"="The timestamp since when you want entries updated."}, 87 * {"name"="since", "dataType"="integer", "required"=false, "format"="default '0'", "description"="The timestamp since when you want entries updated."},
88 * {"name"="public", "dataType"="integer", "required"=false, "format"="1 or 0, all entries by default", "description"="filter by entries with a public link"},
83 * } 89 * }
84 * ) 90 * )
85 * 91 *
@@ -91,17 +97,25 @@ class EntryRestController extends WallabagRestController
91 97
92 $isArchived = (null === $request->query->get('archive')) ? null : (bool) $request->query->get('archive'); 98 $isArchived = (null === $request->query->get('archive')) ? null : (bool) $request->query->get('archive');
93 $isStarred = (null === $request->query->get('starred')) ? null : (bool) $request->query->get('starred'); 99 $isStarred = (null === $request->query->get('starred')) ? null : (bool) $request->query->get('starred');
100 $isPublic = (null === $request->query->get('public')) ? null : (bool) $request->query->get('public');
94 $sort = $request->query->get('sort', 'created'); 101 $sort = $request->query->get('sort', 'created');
95 $order = $request->query->get('order', 'desc'); 102 $order = $request->query->get('order', 'desc');
96 $page = (int) $request->query->get('page', 1); 103 $page = (int) $request->query->get('page', 1);
97 $perPage = (int) $request->query->get('perPage', 30); 104 $perPage = (int) $request->query->get('perPage', 30);
98 $tags = $request->query->get('tags', ''); 105 $tags = is_array($request->query->get('tags')) ? '' : (string) $request->query->get('tags', '');
99 $since = $request->query->get('since', 0); 106 $since = $request->query->get('since', 0);
100 107
101 /** @var \Pagerfanta\Pagerfanta $pager */ 108 /** @var \Pagerfanta\Pagerfanta $pager */
102 $pager = $this->getDoctrine() 109 $pager = $this->get('wallabag_core.entry_repository')->findEntries(
103 ->getRepository('WallabagCoreBundle:Entry') 110 $this->getUser()->getId(),
104 ->findEntries($this->getUser()->getId(), $isArchived, $isStarred, $sort, $order, $since, $tags); 111 $isArchived,
112 $isStarred,
113 $isPublic,
114 $sort,
115 $order,
116 $since,
117 $tags
118 );
105 119
106 $pager->setMaxPerPage($perPage); 120 $pager->setMaxPerPage($perPage);
107 $pager->setCurrentPage($page); 121 $pager->setCurrentPage($page);
@@ -114,6 +128,7 @@ class EntryRestController extends WallabagRestController
114 [ 128 [
115 'archive' => $isArchived, 129 'archive' => $isArchived,
116 'starred' => $isStarred, 130 'starred' => $isStarred,
131 'public' => $isPublic,
117 'sort' => $sort, 132 'sort' => $sort,
118 'order' => $order, 133 'order' => $order,
119 'page' => $page, 134 'page' => $page,
@@ -125,9 +140,7 @@ class EntryRestController extends WallabagRestController
125 ) 140 )
126 ); 141 );
127 142
128 $json = $this->get('serializer')->serialize($paginatedCollection, 'json'); 143 return $this->sendResponse($paginatedCollection);
129
130 return (new JsonResponse())->setJson($json);
131 } 144 }
132 145
133 /** 146 /**
@@ -146,9 +159,7 @@ class EntryRestController extends WallabagRestController
146 $this->validateAuthentication(); 159 $this->validateAuthentication();
147 $this->validateUserAccess($entry->getUser()->getId()); 160 $this->validateUserAccess($entry->getUser()->getId());
148 161
149 $json = $this->get('serializer')->serialize($entry, 'json'); 162 return $this->sendResponse($entry);
150
151 return (new JsonResponse())->setJson($json);
152 } 163 }
153 164
154 /** 165 /**
@@ -170,19 +181,134 @@ class EntryRestController extends WallabagRestController
170 return $this->get('wallabag_core.helper.entries_export') 181 return $this->get('wallabag_core.helper.entries_export')
171 ->setEntries($entry) 182 ->setEntries($entry)
172 ->updateTitle('entry') 183 ->updateTitle('entry')
184 ->updateAuthor('entry')
173 ->exportAs($request->attributes->get('_format')); 185 ->exportAs($request->attributes->get('_format'));
174 } 186 }
175 187
176 /** 188 /**
189 * Handles an entries list and delete URL.
190 *
191 * @ApiDoc(
192 * parameters={
193 * {"name"="urls", "dataType"="string", "required"=true, "format"="A JSON array of urls [{'url': 'http://...'}, {'url': 'http://...'}]", "description"="Urls (as an array) to delete."}
194 * }
195 * )
196 *
197 * @return JsonResponse
198 */
199 public function deleteEntriesListAction(Request $request)
200 {
201 $this->validateAuthentication();
202
203 $urls = json_decode($request->query->get('urls', []));
204
205 if (empty($urls)) {
206 return $this->sendResponse([]);
207 }
208
209 $results = [];
210
211 // handle multiple urls
212 foreach ($urls as $key => $url) {
213 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
214 $url,
215 $this->getUser()->getId()
216 );
217
218 $results[$key]['url'] = $url;
219
220 if (false !== $entry) {
221 $em = $this->getDoctrine()->getManager();
222 $em->remove($entry);
223 $em->flush();
224
225 // entry deleted, dispatch event about it!
226 $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry));
227 }
228
229 $results[$key]['entry'] = $entry instanceof Entry ? true : false;
230 }
231
232 return $this->sendResponse($results);
233 }
234
235 /**
236 * Handles an entries list and create URL.
237 *
238 * @ApiDoc(
239 * parameters={
240 * {"name"="urls", "dataType"="string", "required"=true, "format"="A JSON array of urls [{'url': 'http://...'}, {'url': 'http://...'}]", "description"="Urls (as an array) to create."}
241 * }
242 * )
243 *
244 * @throws HttpException When limit is reached
245 *
246 * @return JsonResponse
247 */
248 public function postEntriesListAction(Request $request)
249 {
250 $this->validateAuthentication();
251
252 $urls = json_decode($request->query->get('urls', []));
253
254 $limit = $this->container->getParameter('wallabag_core.api_limit_mass_actions');
255
256 if (count($urls) > $limit) {
257 throw new HttpException(400, 'API limit reached');
258 }
259
260 $results = [];
261 if (empty($urls)) {
262 return $this->sendResponse($results);
263 }
264
265 // handle multiple urls
266 foreach ($urls as $key => $url) {
267 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
268 $url,
269 $this->getUser()->getId()
270 );
271
272 $results[$key]['url'] = $url;
273
274 if (false === $entry) {
275 $entry = new Entry($this->getUser());
276
277 $this->get('wallabag_core.content_proxy')->updateEntry($entry, $url);
278 }
279
280 $em = $this->getDoctrine()->getManager();
281 $em->persist($entry);
282 $em->flush();
283
284 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false;
285
286 // entry saved, dispatch event about it!
287 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
288 }
289
290 return $this->sendResponse($results);
291 }
292
293 /**
177 * Create an entry. 294 * Create an entry.
178 * 295 *
296 * If you want to provide the HTML content (which means wallabag won't fetch it from the url), you must provide `content`, `title` & `url` fields **non-empty**.
297 * Otherwise, content will be fetched as normal from the url and values will be overwritten.
298 *
179 * @ApiDoc( 299 * @ApiDoc(
180 * parameters={ 300 * parameters={
181 * {"name"="url", "dataType"="string", "required"=true, "format"="http://www.test.com/article.html", "description"="Url for the entry."}, 301 * {"name"="url", "dataType"="string", "required"=true, "format"="http://www.test.com/article.html", "description"="Url for the entry."},
182 * {"name"="title", "dataType"="string", "required"=false, "description"="Optional, we'll get the title from the page."}, 302 * {"name"="title", "dataType"="string", "required"=false, "description"="Optional, we'll get the title from the page."},
183 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."}, 303 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
184 * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already starred"},
185 * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already archived"}, 304 * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already archived"},
305 * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already starred"},
306 * {"name"="content", "dataType"="string", "required"=false, "description"="Content of the entry"},
307 * {"name"="language", "dataType"="string", "required"=false, "description"="Language of the entry"},
308 * {"name"="preview_picture", "dataType"="string", "required"=false, "description"="Preview picture of the entry"},
309 * {"name"="published_at", "dataType"="datetime|integer", "format"="YYYY-MM-DDTHH:II:SS+TZ or a timestamp", "required"=false, "description"="Published date of the entry"},
310 * {"name"="authors", "dataType"="string", "format"="Name Firstname,author2,author3", "required"=false, "description"="Authors of the entry"},
311 * {"name"="public", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="will generate a public link for the entry"},
186 * } 312 * }
187 * ) 313 * )
188 * 314 *
@@ -193,43 +319,61 @@ class EntryRestController extends WallabagRestController
193 $this->validateAuthentication(); 319 $this->validateAuthentication();
194 320
195 $url = $request->request->get('url'); 321 $url = $request->request->get('url');
196 $title = $request->request->get('title');
197 $isArchived = $request->request->get('archive');
198 $isStarred = $request->request->get('starred');
199 322
200 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId($url, $this->getUser()->getId()); 323 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
324 $url,
325 $this->getUser()->getId()
326 );
201 327
202 if (false === $entry) { 328 if (false === $entry) {
203 $entry = new Entry($this->getUser()); 329 $entry = new Entry($this->getUser());
204 try { 330 $entry->setUrl($url);
205 $entry = $this->get('wallabag_core.content_proxy')->updateEntry(
206 $entry,
207 $url
208 );
209 } catch (\Exception $e) {
210 $this->get('logger')->error('Error while saving an entry', [
211 'exception' => $e,
212 'entry' => $entry,
213 ]);
214 $entry->setUrl($url);
215 }
216 } 331 }
217 332
218 if (!is_null($title)) { 333 $data = $this->retrieveValueFromRequest($request);
219 $entry->setTitle($title); 334
335 try {
336 $this->get('wallabag_core.content_proxy')->updateEntry(
337 $entry,
338 $entry->getUrl(),
339 [
340 'title' => !empty($data['title']) ? $data['title'] : $entry->getTitle(),
341 'html' => !empty($data['content']) ? $data['content'] : $entry->getContent(),
342 'url' => $entry->getUrl(),
343 'language' => !empty($data['language']) ? $data['language'] : $entry->getLanguage(),
344 'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt(),
345 // faking the open graph preview picture
346 'open_graph' => [
347 'og_image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(),
348 ],
349 'authors' => is_string($data['authors']) ? explode(',', $data['authors']) : $entry->getPublishedBy(),
350 ]
351 );
352 } catch (\Exception $e) {
353 $this->get('logger')->error('Error while saving an entry', [
354 'exception' => $e,
355 'entry' => $entry,
356 ]);
220 } 357 }
221 358
222 $tags = $request->request->get('tags', ''); 359 if (null !== $data['isArchived']) {
223 if (!empty($tags)) { 360 $entry->setArchived((bool) $data['isArchived']);
224 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags); 361 }
362
363 if (null !== $data['isStarred']) {
364 $entry->updateStar((bool) $data['isStarred']);
225 } 365 }
226 366
227 if (!is_null($isStarred)) { 367 if (!empty($data['tags'])) {
228 $entry->setStarred((bool) $isStarred); 368 $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $data['tags']);
229 } 369 }
230 370
231 if (!is_null($isArchived)) { 371 if (null !== $data['isPublic']) {
232 $entry->setArchived((bool) $isArchived); 372 if (true === (bool) $data['isPublic'] && null === $entry->getUid()) {
373 $entry->generateUid();
374 } elseif (false === (bool) $data['isPublic']) {
375 $entry->cleanUid();
376 }
233 } 377 }
234 378
235 $em = $this->getDoctrine()->getManager(); 379 $em = $this->getDoctrine()->getManager();
@@ -239,9 +383,7 @@ class EntryRestController extends WallabagRestController
239 // entry saved, dispatch event about it! 383 // entry saved, dispatch event about it!
240 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); 384 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
241 385
242 $json = $this->get('serializer')->serialize($entry, 'json'); 386 return $this->sendResponse($entry);
243
244 return (new JsonResponse())->setJson($json);
245 } 387 }
246 388
247 /** 389 /**
@@ -256,6 +398,12 @@ class EntryRestController extends WallabagRestController
256 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."}, 398 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
257 * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="archived the entry."}, 399 * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="archived the entry."},
258 * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="starred the entry."}, 400 * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="starred the entry."},
401 * {"name"="content", "dataType"="string", "required"=false, "description"="Content of the entry"},
402 * {"name"="language", "dataType"="string", "required"=false, "description"="Language of the entry"},
403 * {"name"="preview_picture", "dataType"="string", "required"=false, "description"="Preview picture of the entry"},
404 * {"name"="published_at", "dataType"="datetime|integer", "format"="YYYY-MM-DDTHH:II:SS+TZ or a timestamp", "required"=false, "description"="Published date of the entry"},
405 * {"name"="authors", "dataType"="string", "format"="Name Firstname,author2,author3", "required"=false, "description"="Authors of the entry"},
406 * {"name"="public", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="will generate a public link for the entry"},
259 * } 407 * }
260 * ) 408 * )
261 * 409 *
@@ -266,33 +414,80 @@ class EntryRestController extends WallabagRestController
266 $this->validateAuthentication(); 414 $this->validateAuthentication();
267 $this->validateUserAccess($entry->getUser()->getId()); 415 $this->validateUserAccess($entry->getUser()->getId());
268 416
269 $title = $request->request->get('title'); 417 $contentProxy = $this->get('wallabag_core.content_proxy');
270 $isArchived = $request->request->get('archive');
271 $isStarred = $request->request->get('starred');
272 418
273 if (!is_null($title)) { 419 $data = $this->retrieveValueFromRequest($request);
274 $entry->setTitle($title); 420
421 // this is a special case where user want to manually update the entry content
422 // the ContentProxy will only cleanup the html
423 // and also we force to not re-fetch the content in case of error
424 if (!empty($data['content'])) {
425 try {
426 $contentProxy->updateEntry(
427 $entry,
428 $entry->getUrl(),
429 [
430 'html' => $data['content'],
431 ],
432 true
433 );
434 } catch (\Exception $e) {
435 $this->get('logger')->error('Error while saving an entry', [
436 'exception' => $e,
437 'entry' => $entry,
438 ]);
439 }
275 } 440 }
276 441
277 if (!is_null($isArchived)) { 442 if (!empty($data['title'])) {
278 $entry->setArchived((bool) $isArchived); 443 $entry->setTitle($data['title']);
279 } 444 }
280 445
281 if (!is_null($isStarred)) { 446 if (!empty($data['language'])) {
282 $entry->setStarred((bool) $isStarred); 447 $contentProxy->updateLanguage($entry, $data['language']);
283 } 448 }
284 449
285 $tags = $request->request->get('tags', ''); 450 if (!empty($data['authors']) && is_string($data['authors'])) {
286 if (!empty($tags)) { 451 $entry->setPublishedBy(explode(',', $data['authors']));
287 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags); 452 }
453
454 if (!empty($data['picture'])) {
455 $contentProxy->updatePreviewPicture($entry, $data['picture']);
456 }
457
458 if (!empty($data['publishedAt'])) {
459 $contentProxy->updatePublishedAt($entry, $data['publishedAt']);
460 }
461
462 if (null !== $data['isArchived']) {
463 $entry->setArchived((bool) $data['isArchived']);
464 }
465
466 if (null !== $data['isStarred']) {
467 $entry->updateStar((bool) $data['isStarred']);
468 }
469
470 if (!empty($data['tags'])) {
471 $entry->removeAllTags();
472 $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $data['tags']);
473 }
474
475 if (null !== $data['isPublic']) {
476 if (true === (bool) $data['isPublic'] && null === $entry->getUid()) {
477 $entry->generateUid();
478 } elseif (false === (bool) $data['isPublic']) {
479 $entry->cleanUid();
480 }
288 } 481 }
289 482
290 $em = $this->getDoctrine()->getManager(); 483 $em = $this->getDoctrine()->getManager();
484 $em->persist($entry);
291 $em->flush(); 485 $em->flush();
292 486
293 $json = $this->get('serializer')->serialize($entry, 'json'); 487 // entry saved, dispatch event about it!
488 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
294 489
295 return (new JsonResponse())->setJson($json); 490 return $this->sendResponse($entry);
296 } 491 }
297 492
298 /** 493 /**
@@ -313,7 +508,7 @@ class EntryRestController extends WallabagRestController
313 $this->validateUserAccess($entry->getUser()->getId()); 508 $this->validateUserAccess($entry->getUser()->getId());
314 509
315 try { 510 try {
316 $entry = $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl()); 511 $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl());
317 } catch (\Exception $e) { 512 } catch (\Exception $e) {
318 $this->get('logger')->error('Error while saving an entry', [ 513 $this->get('logger')->error('Error while saving an entry', [
319 'exception' => $e, 514 'exception' => $e,
@@ -335,9 +530,7 @@ class EntryRestController extends WallabagRestController
335 // entry saved, dispatch event about it! 530 // entry saved, dispatch event about it!
336 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); 531 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
337 532
338 $json = $this->get('serializer')->serialize($entry, 'json'); 533 return $this->sendResponse($entry);
339
340 return (new JsonResponse())->setJson($json);
341 } 534 }
342 535
343 /** 536 /**
@@ -363,9 +556,7 @@ class EntryRestController extends WallabagRestController
363 // entry deleted, dispatch event about it! 556 // entry deleted, dispatch event about it!
364 $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry)); 557 $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry));
365 558
366 $json = $this->get('serializer')->serialize($entry, 'json'); 559 return $this->sendResponse($entry);
367
368 return (new JsonResponse())->setJson($json);
369 } 560 }
370 561
371 /** 562 /**
@@ -384,9 +575,7 @@ class EntryRestController extends WallabagRestController
384 $this->validateAuthentication(); 575 $this->validateAuthentication();
385 $this->validateUserAccess($entry->getUser()->getId()); 576 $this->validateUserAccess($entry->getUser()->getId());
386 577
387 $json = $this->get('serializer')->serialize($entry->getTags(), 'json'); 578 return $this->sendResponse($entry->getTags());
388
389 return (new JsonResponse())->setJson($json);
390 } 579 }
391 580
392 /** 581 /**
@@ -410,16 +599,14 @@ class EntryRestController extends WallabagRestController
410 599
411 $tags = $request->request->get('tags', ''); 600 $tags = $request->request->get('tags', '');
412 if (!empty($tags)) { 601 if (!empty($tags)) {
413 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags); 602 $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags);
414 } 603 }
415 604
416 $em = $this->getDoctrine()->getManager(); 605 $em = $this->getDoctrine()->getManager();
417 $em->persist($entry); 606 $em->persist($entry);
418 $em->flush(); 607 $em->flush();
419 608
420 $json = $this->get('serializer')->serialize($entry, 'json'); 609 return $this->sendResponse($entry);
421
422 return (new JsonResponse())->setJson($json);
423 } 610 }
424 611
425 /** 612 /**
@@ -444,8 +631,170 @@ class EntryRestController extends WallabagRestController
444 $em->persist($entry); 631 $em->persist($entry);
445 $em->flush(); 632 $em->flush();
446 633
447 $json = $this->get('serializer')->serialize($entry, 'json'); 634 return $this->sendResponse($entry);
635 }
636
637 /**
638 * Handles an entries list delete tags from them.
639 *
640 * @ApiDoc(
641 * parameters={
642 * {"name"="list", "dataType"="string", "required"=true, "format"="A JSON array of urls [{'url': 'http://...','tags': 'tag1, tag2'}, {'url': 'http://...','tags': 'tag1, tag2'}]", "description"="Urls (as an array) to handle."}
643 * }
644 * )
645 *
646 * @return JsonResponse
647 */
648 public function deleteEntriesTagsListAction(Request $request)
649 {
650 $this->validateAuthentication();
651
652 $list = json_decode($request->query->get('list', []));
653
654 if (empty($list)) {
655 return $this->sendResponse([]);
656 }
657
658 // handle multiple urls
659 $results = [];
660
661 foreach ($list as $key => $element) {
662 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
663 $element->url,
664 $this->getUser()->getId()
665 );
666
667 $results[$key]['url'] = $element->url;
668 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false;
669
670 $tags = $element->tags;
671
672 if (false !== $entry && !(empty($tags))) {
673 $tags = explode(',', $tags);
674 foreach ($tags as $label) {
675 $label = trim($label);
676
677 $tag = $this->getDoctrine()
678 ->getRepository('WallabagCoreBundle:Tag')
679 ->findOneByLabel($label);
680
681 if (false !== $tag) {
682 $entry->removeTag($tag);
683 }
684 }
685
686 $em = $this->getDoctrine()->getManager();
687 $em->persist($entry);
688 $em->flush();
689 }
690 }
691
692 return $this->sendResponse($results);
693 }
694
695 /**
696 * Handles an entries list and add tags to them.
697 *
698 * @ApiDoc(
699 * parameters={
700 * {"name"="list", "dataType"="string", "required"=true, "format"="A JSON array of urls [{'url': 'http://...','tags': 'tag1, tag2'}, {'url': 'http://...','tags': 'tag1, tag2'}]", "description"="Urls (as an array) to handle."}
701 * }
702 * )
703 *
704 * @return JsonResponse
705 */
706 public function postEntriesTagsListAction(Request $request)
707 {
708 $this->validateAuthentication();
709
710 $list = json_decode($request->query->get('list', []));
711
712 if (empty($list)) {
713 return $this->sendResponse([]);
714 }
715
716 $results = [];
717
718 // handle multiple urls
719 foreach ($list as $key => $element) {
720 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
721 $element->url,
722 $this->getUser()->getId()
723 );
724
725 $results[$key]['url'] = $element->url;
726 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false;
727
728 $tags = $element->tags;
729
730 if (false !== $entry && !(empty($tags))) {
731 $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags);
732
733 $em = $this->getDoctrine()->getManager();
734 $em->persist($entry);
735 $em->flush();
736 }
737 }
738
739 return $this->sendResponse($results);
740 }
741
742 /**
743 * Shortcut to send data serialized in json.
744 *
745 * @param mixed $data
746 *
747 * @return JsonResponse
748 */
749 private function sendResponse($data)
750 {
751 // https://github.com/schmittjoh/JMSSerializerBundle/issues/293
752 $context = new SerializationContext();
753 $context->setSerializeNull(true);
754
755 $json = $this->get('jms_serializer')->serialize($data, 'json', $context);
448 756
449 return (new JsonResponse())->setJson($json); 757 return (new JsonResponse())->setJson($json);
450 } 758 }
759
760 /**
761 * Retrieve value from the request.
762 * Used for POST & PATCH on a an entry.
763 *
764 * @param Request $request
765 *
766 * @return array
767 */
768 private function retrieveValueFromRequest(Request $request)
769 {
770 return [
771 'title' => $request->request->get('title'),
772 'tags' => $request->request->get('tags', []),
773 'isArchived' => $request->request->get('archive'),
774 'isStarred' => $request->request->get('starred'),
775 'isPublic' => $request->request->get('public'),
776 'content' => $request->request->get('content'),
777 'language' => $request->request->get('language'),
778 'picture' => $request->request->get('preview_picture'),
779 'publishedAt' => $request->request->get('published_at'),
780 'authors' => $request->request->get('authors', ''),
781 ];
782 }
783
784 /**
785 * Return information about the entry if it exist and depending on the id or not.
786 *
787 * @param Entry|null $entry
788 * @param bool $returnId
789 *
790 * @return bool|int
791 */
792 private function returnExistInformation($entry, $returnId)
793 {
794 if ($returnId) {
795 return $entry instanceof Entry ? $entry->getId() : null;
796 }
797
798 return $entry instanceof Entry;
799 }
451} 800}
diff --git a/src/Wallabag/ApiBundle/Controller/TagRestController.php b/src/Wallabag/ApiBundle/Controller/TagRestController.php
index bc6d4e64..9d333fe4 100644
--- a/src/Wallabag/ApiBundle/Controller/TagRestController.php
+++ b/src/Wallabag/ApiBundle/Controller/TagRestController.php
@@ -3,8 +3,8 @@
3namespace Wallabag\ApiBundle\Controller; 3namespace Wallabag\ApiBundle\Controller;
4 4
5use Nelmio\ApiDocBundle\Annotation\ApiDoc; 5use Nelmio\ApiDocBundle\Annotation\ApiDoc;
6use Symfony\Component\HttpFoundation\Request;
7use Symfony\Component\HttpFoundation\JsonResponse; 6use Symfony\Component\HttpFoundation\JsonResponse;
7use Symfony\Component\HttpFoundation\Request;
8use Wallabag\CoreBundle\Entity\Entry; 8use Wallabag\CoreBundle\Entity\Entry;
9use Wallabag\CoreBundle\Entity\Tag; 9use Wallabag\CoreBundle\Entity\Tag;
10 10
@@ -25,13 +25,13 @@ class TagRestController extends WallabagRestController
25 ->getRepository('WallabagCoreBundle:Tag') 25 ->getRepository('WallabagCoreBundle:Tag')
26 ->findAllTags($this->getUser()->getId()); 26 ->findAllTags($this->getUser()->getId());
27 27
28 $json = $this->get('serializer')->serialize($tags, 'json'); 28 $json = $this->get('jms_serializer')->serialize($tags, 'json');
29 29
30 return (new JsonResponse())->setJson($json); 30 return (new JsonResponse())->setJson($json);
31 } 31 }
32 32
33 /** 33 /**
34 * Permanently remove one tag from **every** entry. 34 * Permanently remove one tag from **every** entry by passing the Tag label.
35 * 35 *
36 * @ApiDoc( 36 * @ApiDoc(
37 * requirements={ 37 * requirements={
@@ -44,7 +44,7 @@ class TagRestController extends WallabagRestController
44 public function deleteTagLabelAction(Request $request) 44 public function deleteTagLabelAction(Request $request)
45 { 45 {
46 $this->validateAuthentication(); 46 $this->validateAuthentication();
47 $label = $request->request->get('tag', ''); 47 $label = $request->get('tag', '');
48 48
49 $tag = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findOneByLabel($label); 49 $tag = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findOneByLabel($label);
50 50
@@ -58,7 +58,7 @@ class TagRestController extends WallabagRestController
58 58
59 $this->cleanOrphanTag($tag); 59 $this->cleanOrphanTag($tag);
60 60
61 $json = $this->get('serializer')->serialize($tag, 'json'); 61 $json = $this->get('jms_serializer')->serialize($tag, 'json');
62 62
63 return (new JsonResponse())->setJson($json); 63 return (new JsonResponse())->setJson($json);
64 } 64 }
@@ -78,7 +78,7 @@ class TagRestController extends WallabagRestController
78 { 78 {
79 $this->validateAuthentication(); 79 $this->validateAuthentication();
80 80
81 $tagsLabels = $request->request->get('tags', ''); 81 $tagsLabels = $request->get('tags', '');
82 82
83 $tags = []; 83 $tags = [];
84 84
@@ -100,13 +100,13 @@ class TagRestController extends WallabagRestController
100 100
101 $this->cleanOrphanTag($tags); 101 $this->cleanOrphanTag($tags);
102 102
103 $json = $this->get('serializer')->serialize($tags, 'json'); 103 $json = $this->get('jms_serializer')->serialize($tags, 'json');
104 104
105 return (new JsonResponse())->setJson($json); 105 return (new JsonResponse())->setJson($json);
106 } 106 }
107 107
108 /** 108 /**
109 * Permanently remove one tag from **every** entry. 109 * Permanently remove one tag from **every** entry by passing the Tag ID.
110 * 110 *
111 * @ApiDoc( 111 * @ApiDoc(
112 * requirements={ 112 * requirements={
@@ -126,7 +126,7 @@ class TagRestController extends WallabagRestController
126 126
127 $this->cleanOrphanTag($tag); 127 $this->cleanOrphanTag($tag);
128 128
129 $json = $this->get('serializer')->serialize($tag, 'json'); 129 $json = $this->get('jms_serializer')->serialize($tag, 'json');
130 130
131 return (new JsonResponse())->setJson($json); 131 return (new JsonResponse())->setJson($json);
132 } 132 }
@@ -145,7 +145,7 @@ class TagRestController extends WallabagRestController
145 $em = $this->getDoctrine()->getManager(); 145 $em = $this->getDoctrine()->getManager();
146 146
147 foreach ($tags as $tag) { 147 foreach ($tags as $tag) {
148 if (count($tag->getEntries()) === 0) { 148 if (0 === count($tag->getEntries())) {
149 $em->remove($tag); 149 $em->remove($tag);
150 } 150 }
151 } 151 }
diff --git a/src/Wallabag/ApiBundle/Controller/UserRestController.php b/src/Wallabag/ApiBundle/Controller/UserRestController.php
new file mode 100644
index 00000000..a1378fc5
--- /dev/null
+++ b/src/Wallabag/ApiBundle/Controller/UserRestController.php
@@ -0,0 +1,157 @@
1<?php
2
3namespace Wallabag\ApiBundle\Controller;
4
5use FOS\UserBundle\Event\UserEvent;
6use FOS\UserBundle\FOSUserEvents;
7use JMS\Serializer\SerializationContext;
8use Nelmio\ApiDocBundle\Annotation\ApiDoc;
9use Symfony\Component\HttpFoundation\JsonResponse;
10use Symfony\Component\HttpFoundation\Request;
11use Wallabag\ApiBundle\Entity\Client;
12use Wallabag\UserBundle\Entity\User;
13
14class UserRestController extends WallabagRestController
15{
16 /**
17 * Retrieve current logged in user informations.
18 *
19 * @ApiDoc()
20 *
21 * @return JsonResponse
22 */
23 public function getUserAction()
24 {
25 $this->validateAuthentication();
26
27 return $this->sendUser($this->getUser());
28 }
29
30 /**
31 * Register an user and create a client.
32 *
33 * @ApiDoc(
34 * requirements={
35 * {"name"="username", "dataType"="string", "required"=true, "description"="The user's username"},
36 * {"name"="password", "dataType"="string", "required"=true, "description"="The user's password"},
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)"}
39 * }
40 * )
41 *
42 * @todo Make this method (or the whole API) accessible only through https
43 *
44 * @return JsonResponse
45 */
46 public function putUserAction(Request $request)
47 {
48 if (!$this->getParameter('fosuser_registration') || !$this->get('craue_config')->get('api_user_registration')) {
49 $json = $this->get('jms_serializer')->serialize(['error' => "Server doesn't allow registrations"], 'json');
50
51 return (new JsonResponse())
52 ->setJson($json)
53 ->setStatusCode(JsonResponse::HTTP_FORBIDDEN);
54 }
55
56 $userManager = $this->get('fos_user.user_manager');
57 $user = $userManager->createUser();
58 // user will be disabled BY DEFAULT to avoid spamming account to be enabled
59 $user->setEnabled(false);
60
61 $form = $this->createForm('Wallabag\UserBundle\Form\NewUserType', $user, [
62 'csrf_protection' => false,
63 ]);
64
65 // simulate form submission
66 $form->submit([
67 'username' => $request->request->get('username'),
68 'plainPassword' => [
69 'first' => $request->request->get('password'),
70 'second' => $request->request->get('password'),
71 ],
72 'email' => $request->request->get('email'),
73 ]);
74
75 if ($form->isSubmitted() && false === $form->isValid()) {
76 $view = $this->view($form, 400);
77 $view->setFormat('json');
78
79 // handle errors in a more beautiful way than the default view
80 $data = json_decode($this->handleView($view)->getContent(), true)['children'];
81 $errors = [];
82
83 if (isset($data['username']['errors'])) {
84 $errors['username'] = $this->translateErrors($data['username']['errors']);
85 }
86
87 if (isset($data['email']['errors'])) {
88 $errors['email'] = $this->translateErrors($data['email']['errors']);
89 }
90
91 if (isset($data['plainPassword']['children']['first']['errors'])) {
92 $errors['password'] = $this->translateErrors($data['plainPassword']['children']['first']['errors']);
93 }
94
95 $json = $this->get('jms_serializer')->serialize(['error' => $errors], 'json');
96
97 return (new JsonResponse())
98 ->setJson($json)
99 ->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
100 }
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
110 $userManager->updateUser($user);
111
112 // dispatch a created event so the associated config will be created
113 $event = new UserEvent($user, $request);
114 $this->get('event_dispatcher')->dispatch(FOSUserEvents::USER_CREATED, $event);
115
116 return $this->sendUser($user, 'user_api_with_client', JsonResponse::HTTP_CREATED);
117 }
118
119 /**
120 * Send user response.
121 *
122 * @param User $user
123 * @param string $group Used to define with serialized group might be used
124 * @param int $status HTTP Status code to send
125 *
126 * @return JsonResponse
127 */
128 private function sendUser(User $user, $group = 'user_api', $status = JsonResponse::HTTP_OK)
129 {
130 $json = $this->get('jms_serializer')->serialize(
131 $user,
132 'json',
133 SerializationContext::create()->setGroups([$group])
134 );
135
136 return (new JsonResponse())
137 ->setJson($json)
138 ->setStatusCode($status);
139 }
140
141 /**
142 * Translate errors message.
143 *
144 * @param array $errors
145 *
146 * @return array
147 */
148 private function translateErrors($errors)
149 {
150 $translatedErrors = [];
151 foreach ($errors as $error) {
152 $translatedErrors[] = $this->get('translator')->trans($error);
153 }
154
155 return $translatedErrors;
156 }
157}
diff --git a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php
index b1e08ca4..7d8cfbba 100644
--- a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php
+++ b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php
@@ -19,7 +19,7 @@ class WallabagRestController extends FOSRestController
19 public function getVersionAction() 19 public function getVersionAction()
20 { 20 {
21 $version = $this->container->getParameter('wallabag_core.version'); 21 $version = $this->container->getParameter('wallabag_core.version');
22 $json = $this->get('serializer')->serialize($version, 'json'); 22 $json = $this->get('jms_serializer')->serialize($version, 'json');
23 23
24 return (new JsonResponse())->setJson($json); 24 return (new JsonResponse())->setJson($json);
25 } 25 }
@@ -40,8 +40,8 @@ class WallabagRestController extends FOSRestController
40 protected function validateUserAccess($requestUserId) 40 protected function validateUserAccess($requestUserId)
41 { 41 {
42 $user = $this->get('security.token_storage')->getToken()->getUser(); 42 $user = $this->get('security.token_storage')->getToken()->getUser();
43 if ($requestUserId != $user->getId()) { 43 if ($requestUserId !== $user->getId()) {
44 throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$requestUserId.', logged user id: '.$user->getId()); 44 throw $this->createAccessDeniedException('Access forbidden. Entry user id: ' . $requestUserId . ', logged user id: ' . $user->getId());
45 } 45 }
46 } 46 }
47} 47}
diff --git a/src/Wallabag/ApiBundle/Entity/Client.php b/src/Wallabag/ApiBundle/Entity/Client.php
index 9ed9f980..e6f98f98 100644
--- a/src/Wallabag/ApiBundle/Entity/Client.php
+++ b/src/Wallabag/ApiBundle/Entity/Client.php
@@ -4,6 +4,9 @@ namespace Wallabag\ApiBundle\Entity;
4 4
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 JMS\Serializer\Annotation\Groups;
8use JMS\Serializer\Annotation\SerializedName;
9use JMS\Serializer\Annotation\VirtualProperty;
7use Wallabag\UserBundle\Entity\User; 10use Wallabag\UserBundle\Entity\User;
8 11
9/** 12/**
@@ -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/ApiBundle/Form/Type/ClientType.php b/src/Wallabag/ApiBundle/Form/Type/ClientType.php
index eaea4feb..fc22538f 100644
--- a/src/Wallabag/ApiBundle/Form/Type/ClientType.php
+++ b/src/Wallabag/ApiBundle/Form/Type/ClientType.php
@@ -5,8 +5,8 @@ namespace Wallabag\ApiBundle\Form\Type;
5use Symfony\Component\Form\AbstractType; 5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\CallbackTransformer; 6use Symfony\Component\Form\CallbackTransformer;
7use Symfony\Component\Form\Extension\Core\Type\SubmitType; 7use Symfony\Component\Form\Extension\Core\Type\SubmitType;
8use Symfony\Component\Form\Extension\Core\Type\UrlType;
9use Symfony\Component\Form\Extension\Core\Type\TextType; 8use Symfony\Component\Form\Extension\Core\Type\TextType;
9use Symfony\Component\Form\Extension\Core\Type\UrlType;
10use Symfony\Component\Form\FormBuilderInterface; 10use Symfony\Component\Form\FormBuilderInterface;
11use Symfony\Component\OptionsResolver\OptionsResolver; 11use Symfony\Component\OptionsResolver\OptionsResolver;
12 12
diff --git a/src/Wallabag/ApiBundle/Resources/config/routing_rest.yml b/src/Wallabag/ApiBundle/Resources/config/routing_rest.yml
index 57d37f4b..c0283e71 100644
--- a/src/Wallabag/ApiBundle/Resources/config/routing_rest.yml
+++ b/src/Wallabag/ApiBundle/Resources/config/routing_rest.yml
@@ -17,3 +17,8 @@ misc:
17 type: rest 17 type: rest
18 resource: "WallabagApiBundle:WallabagRest" 18 resource: "WallabagApiBundle:WallabagRest"
19 name_prefix: api_ 19 name_prefix: api_
20
21user:
22 type: rest
23 resource: "WallabagApiBundle:UserRest"
24 name_prefix: api_
diff --git a/src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php b/src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php
new file mode 100644
index 00000000..b58909db
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php
@@ -0,0 +1,117 @@
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 Symfony\Component\Console\Style\SymfonyStyle;
11use Wallabag\CoreBundle\Entity\Entry;
12use Wallabag\UserBundle\Entity\User;
13
14class CleanDuplicatesCommand extends ContainerAwareCommand
15{
16 /** @var SymfonyStyle */
17 protected $io;
18
19 protected $duplicates = 0;
20
21 protected function configure()
22 {
23 $this
24 ->setName('wallabag:clean-duplicates')
25 ->setDescription('Cleans the database for duplicates')
26 ->setHelp('This command helps you to clean your articles list in case of duplicates')
27 ->addArgument(
28 'username',
29 InputArgument::OPTIONAL,
30 'User to clean'
31 );
32 }
33
34 protected function execute(InputInterface $input, OutputInterface $output)
35 {
36 $this->io = new SymfonyStyle($input, $output);
37
38 $username = $input->getArgument('username');
39
40 if ($username) {
41 try {
42 $user = $this->getUser($username);
43 $this->cleanDuplicates($user);
44 } catch (NoResultException $e) {
45 $this->io->error(sprintf('User "%s" not found.', $username));
46
47 return 1;
48 }
49
50 $this->io->success('Finished cleaning.');
51 } else {
52 $users = $this->getContainer()->get('wallabag_user.user_repository')->findAll();
53
54 $this->io->text(sprintf('Cleaning through <info>%d</info> user accounts', count($users)));
55
56 foreach ($users as $user) {
57 $this->io->text(sprintf('Processing user <info>%s</info>', $user->getUsername()));
58 $this->cleanDuplicates($user);
59 }
60 $this->io->success(sprintf('Finished cleaning. %d duplicates found in total', $this->duplicates));
61 }
62
63 return 0;
64 }
65
66 /**
67 * @param User $user
68 */
69 private function cleanDuplicates(User $user)
70 {
71 $em = $this->getContainer()->get('doctrine.orm.entity_manager');
72 $repo = $this->getContainer()->get('wallabag_core.entry_repository');
73
74 $entries = $repo->findAllEntriesIdAndUrlByUserId($user->getId());
75
76 $duplicatesCount = 0;
77 $urls = [];
78 foreach ($entries as $entry) {
79 $url = $this->similarUrl($entry['url']);
80
81 /* @var $entry Entry */
82 if (in_array($url, $urls, true)) {
83 ++$duplicatesCount;
84
85 $em->remove($repo->find($entry['id']));
86 $em->flush(); // Flushing at the end of the loop would require the instance not being online
87 } else {
88 $urls[] = $entry['url'];
89 }
90 }
91
92 $this->duplicates += $duplicatesCount;
93
94 $this->io->text(sprintf('Cleaned <info>%d</info> duplicates for user <info>%s</info>', $duplicatesCount, $user->getUserName()));
95 }
96
97 private function similarUrl($url)
98 {
99 if (in_array(substr($url, -1), ['/', '#'], true)) { // get rid of "/" and "#" and the end of urls
100 return substr($url, 0, strlen($url));
101 }
102
103 return $url;
104 }
105
106 /**
107 * Fetches a user from its username.
108 *
109 * @param string $username
110 *
111 * @return \Wallabag\UserBundle\Entity\User
112 */
113 private function getUser($username)
114 {
115 return $this->getContainer()->get('wallabag_user.user_repository')->findOneByUserName($username);
116 }
117}
diff --git a/src/Wallabag/CoreBundle/Command/ExportCommand.php b/src/Wallabag/CoreBundle/Command/ExportCommand.php
index e3d3b399..75e9ad91 100644
--- a/src/Wallabag/CoreBundle/Command/ExportCommand.php
+++ b/src/Wallabag/CoreBundle/Command/ExportCommand.php
@@ -7,6 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
7use Symfony\Component\Console\Input\InputArgument; 7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface; 8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface; 9use Symfony\Component\Console\Output\OutputInterface;
10use Symfony\Component\Console\Style\SymfonyStyle;
10 11
11class ExportCommand extends ContainerAwareCommand 12class ExportCommand extends ContainerAwareCommand
12{ 13{
@@ -31,47 +32,44 @@ class ExportCommand extends ContainerAwareCommand
31 32
32 protected function execute(InputInterface $input, OutputInterface $output) 33 protected function execute(InputInterface $input, OutputInterface $output)
33 { 34 {
35 $io = new SymfonyStyle($input, $output);
36
34 try { 37 try {
35 $user = $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($input->getArgument('username')); 38 $user = $this->getContainer()->get('wallabag_user.user_repository')->findOneByUserName($input->getArgument('username'));
36 } catch (NoResultException $e) { 39 } catch (NoResultException $e) {
37 $output->writeln(sprintf('<error>User "%s" not found.</error>', $input->getArgument('username'))); 40 $io->error(sprintf('User "%s" not found.', $input->getArgument('username')));
38 41
39 return 1; 42 return 1;
40 } 43 }
41 44
42 $entries = $this->getDoctrine() 45 $entries = $this->getContainer()->get('wallabag_core.entry_repository')
43 ->getRepository('WallabagCoreBundle:Entry')
44 ->getBuilderForAllByUser($user->getId()) 46 ->getBuilderForAllByUser($user->getId())
45 ->getQuery() 47 ->getQuery()
46 ->getResult(); 48 ->getResult();
47 49
48 $output->write(sprintf('Exporting %d entrie(s) for user « <comment>%s</comment> »... ', count($entries), $user->getUserName())); 50 $io->text(sprintf('Exporting <info>%d</info> entrie(s) for user <info>%s</info>...', count($entries), $user->getUserName()));
49 51
50 $filePath = $input->getArgument('filepath'); 52 $filePath = $input->getArgument('filepath');
51 53
52 if (!$filePath) { 54 if (!$filePath) {
53 $filePath = $this->getContainer()->getParameter('kernel.root_dir').'/../'.sprintf('%s-export.json', $user->getUsername()); 55 $filePath = $this->getContainer()->getParameter('kernel.project_dir') . '/' . sprintf('%s-export.json', $user->getUsername());
54 } 56 }
55 57
56 try { 58 try {
57 $data = $this->getContainer()->get('wallabag_core.helper.entries_export') 59 $data = $this->getContainer()->get('wallabag_core.helper.entries_export')
58 ->setEntries($entries) 60 ->setEntries($entries)
59 ->updateTitle('All') 61 ->updateTitle('All')
62 ->updateAuthor('All')
60 ->exportJsonData(); 63 ->exportJsonData();
61 file_put_contents($filePath, $data); 64 file_put_contents($filePath, $data);
62 } catch (\InvalidArgumentException $e) { 65 } catch (\InvalidArgumentException $e) {
63 $output->writeln(sprintf('<error>Error: "%s"</error>', $e->getMessage())); 66 $io->error(sprintf('Error: "%s"', $e->getMessage()));
64 67
65 return 1; 68 return 1;
66 } 69 }
67 70
68 $output->writeln('<info>Done.</info>'); 71 $io->success('Done.');
69 72
70 return 0; 73 return 0;
71 } 74 }
72
73 private function getDoctrine()
74 {
75 return $this->getContainer()->get('doctrine');
76 }
77} 75}
diff --git a/src/Wallabag/CoreBundle/Command/InstallCommand.php b/src/Wallabag/CoreBundle/Command/InstallCommand.php
index f0738b91..877dbfa2 100644
--- a/src/Wallabag/CoreBundle/Command/InstallCommand.php
+++ b/src/Wallabag/CoreBundle/Command/InstallCommand.php
@@ -2,19 +2,17 @@
2 2
3namespace Wallabag\CoreBundle\Command; 3namespace Wallabag\CoreBundle\Command;
4 4
5use Craue\ConfigBundle\Entity\Setting;
5use FOS\UserBundle\Event\UserEvent; 6use FOS\UserBundle\Event\UserEvent;
6use FOS\UserBundle\FOSUserEvents; 7use FOS\UserBundle\FOSUserEvents;
7use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; 8use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
8use Symfony\Component\Console\Helper\Table;
9use Symfony\Component\Console\Input\ArrayInput; 9use Symfony\Component\Console\Input\ArrayInput;
10use Symfony\Component\Console\Input\InputInterface; 10use Symfony\Component\Console\Input\InputInterface;
11use Symfony\Component\Console\Input\InputOption; 11use Symfony\Component\Console\Input\InputOption;
12use Symfony\Component\Console\Output\BufferedOutput; 12use Symfony\Component\Console\Output\BufferedOutput;
13use Symfony\Component\Console\Output\OutputInterface; 13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Question\ConfirmationQuestion;
15use Symfony\Component\Console\Question\Question; 14use Symfony\Component\Console\Question\Question;
16use Wallabag\CoreBundle\Entity\Config; 15use Symfony\Component\Console\Style\SymfonyStyle;
17use Craue\ConfigBundle\Entity\Setting;
18 16
19class InstallCommand extends ContainerAwareCommand 17class InstallCommand extends ContainerAwareCommand
20{ 18{
@@ -24,9 +22,9 @@ class InstallCommand extends ContainerAwareCommand
24 protected $defaultInput; 22 protected $defaultInput;
25 23
26 /** 24 /**
27 * @var OutputInterface 25 * @var SymfonyStyle
28 */ 26 */
29 protected $defaultOutput; 27 protected $io;
30 28
31 /** 29 /**
32 * @var array 30 * @var array
@@ -53,25 +51,27 @@ class InstallCommand extends ContainerAwareCommand
53 protected function execute(InputInterface $input, OutputInterface $output) 51 protected function execute(InputInterface $input, OutputInterface $output)
54 { 52 {
55 $this->defaultInput = $input; 53 $this->defaultInput = $input;
56 $this->defaultOutput = $output;
57 54
58 $output->writeln('<info>Installing wallabag...</info>'); 55 $this->io = new SymfonyStyle($input, $output);
59 $output->writeln(''); 56
57 $this->io->title('Wallabag installer');
60 58
61 $this 59 $this
62 ->checkRequirements() 60 ->checkRequirements()
63 ->setupDatabase() 61 ->setupDatabase()
64 ->setupAdmin() 62 ->setupAdmin()
65 ->setupConfig() 63 ->setupConfig()
64 ->runMigrations()
66 ; 65 ;
67 66
68 $output->writeln('<info>wallabag has been successfully installed.</info>'); 67 $this->io->success('Wallabag has been successfully installed.');
69 $output->writeln('<comment>Just execute `php bin/console server:run --env=prod` for using wallabag: http://localhost:8000</comment>'); 68 $this->io->note('Just execute `php bin/console server:run --env=prod` for using wallabag: http://localhost:8000');
70 } 69 }
71 70
72 protected function checkRequirements() 71 protected function checkRequirements()
73 { 72 {
74 $this->defaultOutput->writeln('<info><comment>Step 1 of 4.</comment> Checking system requirements.</info>'); 73 $this->io->section('Step 1 of 5: Checking system requirements.');
74
75 $doctrineManager = $this->getContainer()->get('doctrine')->getManager(); 75 $doctrineManager = $this->getContainer()->get('doctrine')->getManager();
76 76
77 $rows = []; 77 $rows = [];
@@ -85,7 +85,7 @@ class InstallCommand extends ContainerAwareCommand
85 if (!extension_loaded($this->getContainer()->getParameter('database_driver'))) { 85 if (!extension_loaded($this->getContainer()->getParameter('database_driver'))) {
86 $fulfilled = false; 86 $fulfilled = false;
87 $status = '<error>ERROR!</error>'; 87 $status = '<error>ERROR!</error>';
88 $help = 'Database driver "'.$this->getContainer()->getParameter('database_driver').'" is not installed.'; 88 $help = 'Database driver "' . $this->getContainer()->getParameter('database_driver') . '" is not installed.';
89 } 89 }
90 90
91 $rows[] = [sprintf($label, $this->getContainer()->getParameter('database_driver')), $status, $help]; 91 $rows[] = [sprintf($label, $this->getContainer()->getParameter('database_driver')), $status, $help];
@@ -100,10 +100,10 @@ class InstallCommand extends ContainerAwareCommand
100 $conn->connect(); 100 $conn->connect();
101 } catch (\Exception $e) { 101 } catch (\Exception $e) {
102 if (false === strpos($e->getMessage(), 'Unknown database') 102 if (false === strpos($e->getMessage(), 'Unknown database')
103 && false === strpos($e->getMessage(), 'database "'.$this->getContainer()->getParameter('database_name').'" does not exist')) { 103 && false === strpos($e->getMessage(), 'database "' . $this->getContainer()->getParameter('database_name') . '" does not exist')) {
104 $fulfilled = false; 104 $fulfilled = false;
105 $status = '<error>ERROR!</error>'; 105 $status = '<error>ERROR!</error>';
106 $help = 'Can\'t connect to the database: '.$e->getMessage(); 106 $help = 'Can\'t connect to the database: ' . $e->getMessage();
107 } 107 }
108 } 108 }
109 109
@@ -122,7 +122,7 @@ class InstallCommand extends ContainerAwareCommand
122 if (false === version_compare($version, $minimalVersion, '>')) { 122 if (false === version_compare($version, $minimalVersion, '>')) {
123 $fulfilled = false; 123 $fulfilled = false;
124 $status = '<error>ERROR!</error>'; 124 $status = '<error>ERROR!</error>';
125 $help = 'Your MySQL version ('.$version.') is too old, consider upgrading ('.$minimalVersion.'+).'; 125 $help = 'Your MySQL version (' . $version . ') is too old, consider upgrading (' . $minimalVersion . '+).';
126 } 126 }
127 } 127 }
128 128
@@ -136,50 +136,44 @@ class InstallCommand extends ContainerAwareCommand
136 if (isset($matches[1]) & version_compare($matches[1], '9.2.0', '<')) { 136 if (isset($matches[1]) & version_compare($matches[1], '9.2.0', '<')) {
137 $fulfilled = false; 137 $fulfilled = false;
138 $status = '<error>ERROR!</error>'; 138 $status = '<error>ERROR!</error>';
139 $help = 'PostgreSQL should be greater than 9.1 (actual version: '.$matches[1].')'; 139 $help = 'PostgreSQL should be greater than 9.1 (actual version: ' . $matches[1] . ')';
140 } 140 }
141 } 141 }
142 142
143 $rows[] = [$label, $status, $help]; 143 $rows[] = [$label, $status, $help];
144 144
145 foreach ($this->functionExists as $functionRequired) { 145 foreach ($this->functionExists as $functionRequired) {
146 $label = '<comment>'.$functionRequired.'</comment>'; 146 $label = '<comment>' . $functionRequired . '</comment>';
147 $status = '<info>OK!</info>'; 147 $status = '<info>OK!</info>';
148 $help = ''; 148 $help = '';
149 149
150 if (!function_exists($functionRequired)) { 150 if (!function_exists($functionRequired)) {
151 $fulfilled = false; 151 $fulfilled = false;
152 $status = '<error>ERROR!</error>'; 152 $status = '<error>ERROR!</error>';
153 $help = 'You need the '.$functionRequired.' function activated'; 153 $help = 'You need the ' . $functionRequired . ' function activated';
154 } 154 }
155 155
156 $rows[] = [$label, $status, $help]; 156 $rows[] = [$label, $status, $help];
157 } 157 }
158 158
159 $table = new Table($this->defaultOutput); 159 $this->io->table(['Checked', 'Status', 'Recommendation'], $rows);
160 $table
161 ->setHeaders(['Checked', 'Status', 'Recommendation'])
162 ->setRows($rows)
163 ->render();
164 160
165 if (!$fulfilled) { 161 if (!$fulfilled) {
166 throw new \RuntimeException('Some system requirements are not fulfilled. Please check output messages and fix them.'); 162 throw new \RuntimeException('Some system requirements are not fulfilled. Please check output messages and fix them.');
167 } 163 }
168 164
169 $this->defaultOutput->writeln('<info>Success! Your system can run wallabag properly.</info>'); 165 $this->io->success('Success! Your system can run wallabag properly.');
170
171 $this->defaultOutput->writeln('');
172 166
173 return $this; 167 return $this;
174 } 168 }
175 169
176 protected function setupDatabase() 170 protected function setupDatabase()
177 { 171 {
178 $this->defaultOutput->writeln('<info><comment>Step 2 of 4.</comment> Setting up database.</info>'); 172 $this->io->section('Step 2 of 5: Setting up database.');
179 173
180 // user want to reset everything? Don't care about what is already here 174 // user want to reset everything? Don't care about what is already here
181 if (true === $this->defaultInput->getOption('reset')) { 175 if (true === $this->defaultInput->getOption('reset')) {
182 $this->defaultOutput->writeln('Droping database, creating database and schema, clearing the cache'); 176 $this->io->text('Dropping database, creating database and schema, clearing the cache');
183 177
184 $this 178 $this
185 ->runCommand('doctrine:database:drop', ['--force' => true]) 179 ->runCommand('doctrine:database:drop', ['--force' => true])
@@ -188,13 +182,13 @@ class InstallCommand extends ContainerAwareCommand
188 ->runCommand('cache:clear') 182 ->runCommand('cache:clear')
189 ; 183 ;
190 184
191 $this->defaultOutput->writeln(''); 185 $this->io->newLine();
192 186
193 return $this; 187 return $this;
194 } 188 }
195 189
196 if (!$this->isDatabasePresent()) { 190 if (!$this->isDatabasePresent()) {
197 $this->defaultOutput->writeln('Creating database and schema, clearing the cache'); 191 $this->io->text('Creating database and schema, clearing the cache');
198 192
199 $this 193 $this
200 ->runCommand('doctrine:database:create') 194 ->runCommand('doctrine:database:create')
@@ -202,16 +196,13 @@ class InstallCommand extends ContainerAwareCommand
202 ->runCommand('cache:clear') 196 ->runCommand('cache:clear')
203 ; 197 ;
204 198
205 $this->defaultOutput->writeln(''); 199 $this->io->newLine();
206 200
207 return $this; 201 return $this;
208 } 202 }
209 203
210 $questionHelper = $this->getHelper('question'); 204 if ($this->io->confirm('It appears that your database already exists. Would you like to reset it?', false)) {
211 $question = new ConfirmationQuestion('It appears that your database already exists. Would you like to reset it? (y/N)', false); 205 $this->io->text('Dropping database, creating database and schema...');
212
213 if ($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)) {
214 $this->defaultOutput->writeln('Droping database, creating database and schema');
215 206
216 $this 207 $this
217 ->runCommand('doctrine:database:drop', ['--force' => true]) 208 ->runCommand('doctrine:database:drop', ['--force' => true])
@@ -219,9 +210,8 @@ class InstallCommand extends ContainerAwareCommand
219 ->runCommand('doctrine:schema:create') 210 ->runCommand('doctrine:schema:create')
220 ; 211 ;
221 } elseif ($this->isSchemaPresent()) { 212 } elseif ($this->isSchemaPresent()) {
222 $question = new ConfirmationQuestion('Seems like your database contains schema. Do you want to reset it? (y/N)', false); 213 if ($this->io->confirm('Seems like your database contains schema. Do you want to reset it?', false)) {
223 if ($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)) { 214 $this->io->text('Dropping schema and creating schema...');
224 $this->defaultOutput->writeln('Droping schema and creating schema');
225 215
226 $this 216 $this
227 ->runCommand('doctrine:schema:drop', ['--force' => true]) 217 ->runCommand('doctrine:schema:drop', ['--force' => true])
@@ -229,29 +219,27 @@ class InstallCommand extends ContainerAwareCommand
229 ; 219 ;
230 } 220 }
231 } else { 221 } else {
232 $this->defaultOutput->writeln('Creating schema'); 222 $this->io->text('Creating schema...');
233 223
234 $this 224 $this
235 ->runCommand('doctrine:schema:create') 225 ->runCommand('doctrine:schema:create')
236 ; 226 ;
237 } 227 }
238 228
239 $this->defaultOutput->writeln('Clearing the cache'); 229 $this->io->text('Clearing the cache...');
240 $this->runCommand('cache:clear'); 230 $this->runCommand('cache:clear');
241 231
242 $this->defaultOutput->writeln(''); 232 $this->io->newLine();
233 $this->io->text('<info>Database successfully setup.</info>');
243 234
244 return $this; 235 return $this;
245 } 236 }
246 237
247 protected function setupAdmin() 238 protected function setupAdmin()
248 { 239 {
249 $this->defaultOutput->writeln('<info><comment>Step 3 of 4.</comment> Administration setup.</info>'); 240 $this->io->section('Step 3 of 5: Administration setup.');
250
251 $questionHelper = $this->getHelperSet()->get('question');
252 $question = new ConfirmationQuestion('Would you like to create a new admin user (recommended) ? (Y/n)', true);
253 241
254 if (!$questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)) { 242 if (!$this->io->confirm('Would you like to create a new admin user (recommended)?', true)) {
255 return $this; 243 return $this;
256 } 244 }
257 245
@@ -260,14 +248,13 @@ class InstallCommand extends ContainerAwareCommand
260 $userManager = $this->getContainer()->get('fos_user.user_manager'); 248 $userManager = $this->getContainer()->get('fos_user.user_manager');
261 $user = $userManager->createUser(); 249 $user = $userManager->createUser();
262 250
263 $question = new Question('Username (default: wallabag) :', 'wallabag'); 251 $user->setUsername($this->io->ask('Username', 'wallabag'));
264 $user->setUsername($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question));
265 252
266 $question = new Question('Password (default: wallabag) :', 'wallabag'); 253 $question = new Question('Password', 'wallabag');
267 $user->setPlainPassword($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)); 254 $question->setHidden(true);
255 $user->setPlainPassword($this->io->askQuestion($question));
268 256
269 $question = new Question('Email:', ''); 257 $user->setEmail($this->io->ask('Email', ''));
270 $user->setEmail($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question));
271 258
272 $user->setEnabled(true); 259 $user->setEnabled(true);
273 $user->addRole('ROLE_SUPER_ADMIN'); 260 $user->addRole('ROLE_SUPER_ADMIN');
@@ -278,168 +265,20 @@ class InstallCommand extends ContainerAwareCommand
278 $event = new UserEvent($user); 265 $event = new UserEvent($user);
279 $this->getContainer()->get('event_dispatcher')->dispatch(FOSUserEvents::USER_CREATED, $event); 266 $this->getContainer()->get('event_dispatcher')->dispatch(FOSUserEvents::USER_CREATED, $event);
280 267
281 $this->defaultOutput->writeln(''); 268 $this->io->text('<info>Administration successfully setup.</info>');
282 269
283 return $this; 270 return $this;
284 } 271 }
285 272
286 protected function setupConfig() 273 protected function setupConfig()
287 { 274 {
288 $this->defaultOutput->writeln('<info><comment>Step 4 of 4.</comment> Config setup.</info>'); 275 $this->io->section('Step 4 of 5: Config setup.');
289 $em = $this->getContainer()->get('doctrine.orm.entity_manager'); 276 $em = $this->getContainer()->get('doctrine.orm.entity_manager');
290 277
291 // cleanup before insert new stuff 278 // cleanup before insert new stuff
292 $em->createQuery('DELETE FROM CraueConfigBundle:Setting')->execute(); 279 $em->createQuery('DELETE FROM CraueConfigBundle:Setting')->execute();
293 280
294 $settings = [ 281 foreach ($this->getContainer()->getParameter('wallabag_core.default_internal_settings') as $setting) {
295 [
296 'name' => 'share_public',
297 'value' => '1',
298 'section' => 'entry',
299 ],
300 [
301 'name' => 'carrot',
302 'value' => '1',
303 'section' => 'entry',
304 ],
305 [
306 'name' => 'share_diaspora',
307 'value' => '1',
308 'section' => 'entry',
309 ],
310 [
311 'name' => 'diaspora_url',
312 'value' => 'http://diasporapod.com',
313 'section' => 'entry',
314 ],
315 [
316 'name' => 'share_unmark',
317 'value' => '1',
318 'section' => 'entry',
319 ],
320 [
321 'name' => 'unmark_url',
322 'value' => 'https://unmark.it',
323 'section' => 'entry',
324 ],
325 [
326 'name' => 'share_shaarli',
327 'value' => '1',
328 'section' => 'entry',
329 ],
330 [
331 'name' => 'shaarli_url',
332 'value' => 'http://myshaarli.com',
333 'section' => 'entry',
334 ],
335 [
336 'name' => 'share_mail',
337 'value' => '1',
338 'section' => 'entry',
339 ],
340 [
341 'name' => 'share_twitter',
342 'value' => '1',
343 'section' => 'entry',
344 ],
345 [
346 'name' => 'export_epub',
347 'value' => '1',
348 'section' => 'export',
349 ],
350 [
351 'name' => 'export_mobi',
352 'value' => '1',
353 'section' => 'export',
354 ],
355 [
356 'name' => 'export_pdf',
357 'value' => '1',
358 'section' => 'export',
359 ],
360 [
361 'name' => 'export_csv',
362 'value' => '1',
363 'section' => 'export',
364 ],
365 [
366 'name' => 'export_json',
367 'value' => '1',
368 'section' => 'export',
369 ],
370 [
371 'name' => 'export_txt',
372 'value' => '1',
373 'section' => 'export',
374 ],
375 [
376 'name' => 'export_xml',
377 'value' => '1',
378 'section' => 'export',
379 ],
380 [
381 'name' => 'import_with_redis',
382 'value' => '0',
383 'section' => 'import',
384 ],
385 [
386 'name' => 'import_with_rabbitmq',
387 'value' => '0',
388 'section' => 'import',
389 ],
390 [
391 'name' => 'show_printlink',
392 'value' => '1',
393 'section' => 'entry',
394 ],
395 [
396 'name' => 'wallabag_support_url',
397 'value' => 'https://www.wallabag.org/pages/support.html',
398 'section' => 'misc',
399 ],
400 [
401 'name' => 'wallabag_url',
402 'value' => '',
403 'section' => 'misc',
404 ],
405 [
406 'name' => 'piwik_enabled',
407 'value' => '0',
408 'section' => 'analytics',
409 ],
410 [
411 'name' => 'piwik_host',
412 'value' => 'v2.wallabag.org',
413 'section' => 'analytics',
414 ],
415 [
416 'name' => 'piwik_site_id',
417 'value' => '1',
418 'section' => 'analytics',
419 ],
420 [
421 'name' => 'demo_mode_enabled',
422 'value' => '0',
423 'section' => 'misc',
424 ],
425 [
426 'name' => 'demo_mode_username',
427 'value' => 'wallabag',
428 'section' => 'misc',
429 ],
430 [
431 'name' => 'download_images_enabled',
432 'value' => '0',
433 'section' => 'misc',
434 ],
435 [
436 'name' => 'restricted_access',
437 'value' => '0',
438 'section' => 'entry',
439 ],
440 ];
441
442 foreach ($settings as $setting) {
443 $newSetting = new Setting(); 282 $newSetting = new Setting();
444 $newSetting->setName($setting['name']); 283 $newSetting->setName($setting['name']);
445 $newSetting->setValue($setting['value']); 284 $newSetting->setValue($setting['value']);
@@ -449,7 +288,19 @@ class InstallCommand extends ContainerAwareCommand
449 288
450 $em->flush(); 289 $em->flush();
451 290
452 $this->defaultOutput->writeln(''); 291 $this->io->text('<info>Config successfully setup.</info>');
292
293 return $this;
294 }
295
296 protected function runMigrations()
297 {
298 $this->io->section('Step 5 of 5: Run migrations.');
299
300 $this
301 ->runCommand('doctrine:migrations:migrate', ['--no-interaction' => true]);
302
303 $this->io->text('<info>Migrations successfully executed.</info>');
453 304
454 return $this; 305 return $this;
455 } 306 }
@@ -480,20 +331,18 @@ class InstallCommand extends ContainerAwareCommand
480 $output = new BufferedOutput(); 331 $output = new BufferedOutput();
481 $exitCode = $this->getApplication()->run(new ArrayInput($parameters), $output); 332 $exitCode = $this->getApplication()->run(new ArrayInput($parameters), $output);
482 333
334 // PDO does not always close the connection after Doctrine commands.
335 // See https://github.com/symfony/symfony/issues/11750.
336 $this->getContainer()->get('doctrine')->getManager()->getConnection()->close();
337
483 if (0 !== $exitCode) { 338 if (0 !== $exitCode) {
484 $this->getApplication()->setAutoExit(true); 339 $this->getApplication()->setAutoExit(true);
485 340
486 $this->defaultOutput->writeln(''); 341 throw new \RuntimeException(
487 $this->defaultOutput->writeln('<error>The command "'.$command.'" generates some errors: </error>'); 342 'The command "' . $command . "\" generates some errors: \n\n"
488 $this->defaultOutput->writeln($output->fetch()); 343 . $output->fetch());
489
490 die();
491 } 344 }
492 345
493 // PDO does not always close the connection after Doctrine commands.
494 // See https://github.com/symfony/symfony/issues/11750.
495 $this->getContainer()->get('doctrine')->getManager()->getConnection()->close();
496
497 return $this; 346 return $this;
498 } 347 }
499 348
@@ -535,7 +384,7 @@ class InstallCommand extends ContainerAwareCommand
535 } 384 }
536 385
537 try { 386 try {
538 return in_array($databaseName, $schemaManager->listDatabases()); 387 return in_array($databaseName, $schemaManager->listDatabases(), true);
539 } catch (\Doctrine\DBAL\Exception\DriverException $e) { 388 } catch (\Doctrine\DBAL\Exception\DriverException $e) {
540 // it means we weren't able to get database list, assume the database doesn't exist 389 // it means we weren't able to get database list, assume the database doesn't exist
541 390
diff --git a/src/Wallabag/CoreBundle/Command/ListUserCommand.php b/src/Wallabag/CoreBundle/Command/ListUserCommand.php
new file mode 100644
index 00000000..68e515da
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Command/ListUserCommand.php
@@ -0,0 +1,61 @@
1<?php
2
3namespace Wallabag\CoreBundle\Command;
4
5use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
6use Symfony\Component\Console\Input\InputArgument;
7use Symfony\Component\Console\Input\InputInterface;
8use Symfony\Component\Console\Input\InputOption;
9use Symfony\Component\Console\Output\OutputInterface;
10use Symfony\Component\Console\Style\SymfonyStyle;
11
12class ListUserCommand extends ContainerAwareCommand
13{
14 protected function configure()
15 {
16 $this
17 ->setName('wallabag:user:list')
18 ->setDescription('List all users')
19 ->setHelp('This command list all existing users')
20 ->addArgument('search', InputArgument::OPTIONAL, 'Filter list by given search term')
21 ->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Max number of displayed users', 100)
22 ;
23 }
24
25 protected function execute(InputInterface $input, OutputInterface $output)
26 {
27 $io = new SymfonyStyle($input, $output);
28
29 $users = $this->getContainer()->get('wallabag_user.user_repository')
30 ->getQueryBuilderForSearch($input->getArgument('search'))
31 ->setMaxResults($input->getOption('limit'))
32 ->getQuery()
33 ->getResult();
34
35 $nbUsers = $this->getContainer()->get('wallabag_user.user_repository')
36 ->getSumUsers();
37
38 $rows = [];
39 foreach ($users as $user) {
40 $rows[] = [
41 $user->getUsername(),
42 $user->getEmail(),
43 $user->isEnabled() ? 'yes' : 'no',
44 $user->hasRole('ROLE_SUPER_ADMIN') || $user->hasRole('ROLE_ADMIN') ? 'yes' : 'no',
45 ];
46 }
47
48 $io->table(['username', 'email', 'is enabled?', 'is admin?'], $rows);
49
50 $io->success(
51 sprintf(
52 '%s/%s%s user(s) displayed.',
53 count($users),
54 $nbUsers,
55 null === $input->getArgument('search') ? '' : ' (filtered)'
56 )
57 );
58
59 return 0;
60 }
61}
diff --git a/src/Wallabag/CoreBundle/Command/ReloadEntryCommand.php b/src/Wallabag/CoreBundle/Command/ReloadEntryCommand.php
new file mode 100644
index 00000000..91998841
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Command/ReloadEntryCommand.php
@@ -0,0 +1,90 @@
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 Symfony\Component\Console\Style\SymfonyStyle;
11use Wallabag\CoreBundle\Event\EntrySavedEvent;
12
13class ReloadEntryCommand extends ContainerAwareCommand
14{
15 protected function configure()
16 {
17 $this
18 ->setName('wallabag:entry:reload')
19 ->setDescription('Reload entries')
20 ->setHelp('This command reload entries')
21 ->addArgument('username', InputArgument::OPTIONAL, 'Reload entries only for the given user')
22 ;
23 }
24
25 protected function execute(InputInterface $input, OutputInterface $output)
26 {
27 $io = new SymfonyStyle($input, $output);
28
29 $userId = null;
30 if ($username = $input->getArgument('username')) {
31 try {
32 $userId = $this->getContainer()
33 ->get('wallabag_user.user_repository')
34 ->findOneByUserName($username)
35 ->getId();
36 } catch (NoResultException $e) {
37 $io->error(sprintf('User "%s" not found.', $username));
38
39 return 1;
40 }
41 }
42
43 $entryRepository = $this->getContainer()->get('wallabag_core.entry_repository');
44 $entryIds = $entryRepository->findAllEntriesIdByUserId($userId);
45
46 $nbEntries = count($entryIds);
47 if (!$nbEntries) {
48 $io->success('No entry to reload.');
49
50 return 0;
51 }
52
53 $io->note(
54 sprintf(
55 "You're going to reload %s entries. Depending on the number of entry to reload, this could be a very long process.",
56 $nbEntries
57 )
58 );
59
60 if (!$io->confirm('Are you sure you want to proceed?')) {
61 return 0;
62 }
63
64 $progressBar = $io->createProgressBar($nbEntries);
65
66 $contentProxy = $this->getContainer()->get('wallabag_core.content_proxy');
67 $em = $this->getContainer()->get('doctrine')->getManager();
68 $dispatcher = $this->getContainer()->get('event_dispatcher');
69
70 $progressBar->start();
71 foreach ($entryIds as $entryId) {
72 $entry = $entryRepository->find($entryId);
73
74 $contentProxy->updateEntry($entry, $entry->getUrl());
75 $em->persist($entry);
76 $em->flush();
77
78 $dispatcher->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
79 $progressBar->advance();
80
81 $em->detach($entry);
82 }
83 $progressBar->finish();
84
85 $io->newLine(2);
86 $io->success('Done.');
87
88 return 0;
89 }
90}
diff --git a/src/Wallabag/CoreBundle/Command/ShowUserCommand.php b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php
new file mode 100644
index 00000000..a0184267
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php
@@ -0,0 +1,75 @@
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 Symfony\Component\Console\Style\SymfonyStyle;
11use Wallabag\UserBundle\Entity\User;
12
13class ShowUserCommand extends ContainerAwareCommand
14{
15 /** @var SymfonyStyle */
16 protected $io;
17
18 protected function configure()
19 {
20 $this
21 ->setName('wallabag:user:show')
22 ->setDescription('Show user details')
23 ->setHelp('This command shows the details for an user')
24 ->addArgument(
25 'username',
26 InputArgument::REQUIRED,
27 'User to show details for'
28 );
29 }
30
31 protected function execute(InputInterface $input, OutputInterface $output)
32 {
33 $this->io = new SymfonyStyle($input, $output);
34
35 $username = $input->getArgument('username');
36
37 try {
38 $user = $this->getUser($username);
39 $this->showUser($user);
40 } catch (NoResultException $e) {
41 $this->io->error(sprintf('User "%s" not found.', $username));
42
43 return 1;
44 }
45
46 return 0;
47 }
48
49 /**
50 * @param User $user
51 */
52 private function showUser(User $user)
53 {
54 $this->io->listing([
55 sprintf('Username: %s', $user->getUsername()),
56 sprintf('Email: %s', $user->getEmail()),
57 sprintf('Display name: %s', $user->getName()),
58 sprintf('Creation date: %s', $user->getCreatedAt()->format('Y-m-d H:i:s')),
59 sprintf('Last login: %s', null !== $user->getLastLogin() ? $user->getLastLogin()->format('Y-m-d H:i:s') : 'never'),
60 sprintf('2FA activated: %s', $user->isTwoFactorAuthentication() ? 'yes' : 'no'),
61 ]);
62 }
63
64 /**
65 * Fetches a user from its username.
66 *
67 * @param string $username
68 *
69 * @return \Wallabag\UserBundle\Entity\User
70 */
71 private function getUser($username)
72 {
73 return $this->getContainer()->get('wallabag_user.user_repository')->findOneByUserName($username);
74 }
75}
diff --git a/src/Wallabag/CoreBundle/Command/TagAllCommand.php b/src/Wallabag/CoreBundle/Command/TagAllCommand.php
index 3f9bb04d..4afac2d4 100644
--- a/src/Wallabag/CoreBundle/Command/TagAllCommand.php
+++ b/src/Wallabag/CoreBundle/Command/TagAllCommand.php
@@ -7,6 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
7use Symfony\Component\Console\Input\InputArgument; 7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface; 8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface; 9use Symfony\Component\Console\Output\OutputInterface;
10use Symfony\Component\Console\Style\SymfonyStyle;
10 11
11class TagAllCommand extends ContainerAwareCommand 12class TagAllCommand extends ContainerAwareCommand
12{ 13{
@@ -25,21 +26,22 @@ class TagAllCommand extends ContainerAwareCommand
25 26
26 protected function execute(InputInterface $input, OutputInterface $output) 27 protected function execute(InputInterface $input, OutputInterface $output)
27 { 28 {
29 $io = new SymfonyStyle($input, $output);
30
28 try { 31 try {
29 $user = $this->getUser($input->getArgument('username')); 32 $user = $this->getUser($input->getArgument('username'));
30 } catch (NoResultException $e) { 33 } catch (NoResultException $e) {
31 $output->writeln(sprintf('<error>User "%s" not found.</error>', $input->getArgument('username'))); 34 $io->error(sprintf('User "%s" not found.', $input->getArgument('username')));
32 35
33 return 1; 36 return 1;
34 } 37 }
35 $tagger = $this->getContainer()->get('wallabag_core.rule_based_tagger'); 38 $tagger = $this->getContainer()->get('wallabag_core.rule_based_tagger');
36 39
37 $output->write(sprintf('Tagging entries for user « <comment>%s</comment> »... ', $user->getUserName())); 40 $io->text(sprintf('Tagging entries for user <info>%s</info>...', $user->getUserName()));
38 41
39 $entries = $tagger->tagAllForUser($user); 42 $entries = $tagger->tagAllForUser($user);
40 43
41 $output->writeln('<info>Done.</info>'); 44 $io->text('Persist entries... ');
42 $output->write(sprintf('Persist entries ... ', $user->getUserName()));
43 45
44 $em = $this->getDoctrine()->getManager(); 46 $em = $this->getDoctrine()->getManager();
45 foreach ($entries as $entry) { 47 foreach ($entries as $entry) {
@@ -47,7 +49,9 @@ class TagAllCommand extends ContainerAwareCommand
47 } 49 }
48 $em->flush(); 50 $em->flush();
49 51
50 $output->writeln('<info>Done.</info>'); 52 $io->success('Done.');
53
54 return 0;
51 } 55 }
52 56
53 /** 57 /**
@@ -59,7 +63,7 @@ class TagAllCommand extends ContainerAwareCommand
59 */ 63 */
60 private function getUser($username) 64 private function getUser($username)
61 { 65 {
62 return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username); 66 return $this->getContainer()->get('wallabag_user.user_repository')->findOneByUserName($username);
63 } 67 }
64 68
65 private function getDoctrine() 69 private function getDoctrine()
diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php
index 907bf78e..a89bb780 100644
--- a/src/Wallabag/CoreBundle/Controller/ConfigController.php
+++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php
@@ -10,8 +10,8 @@ use Symfony\Component\HttpFoundation\Request;
10use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; 10use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
11use Wallabag\CoreBundle\Entity\Config; 11use Wallabag\CoreBundle\Entity\Config;
12use Wallabag\CoreBundle\Entity\TaggingRule; 12use Wallabag\CoreBundle\Entity\TaggingRule;
13use Wallabag\CoreBundle\Form\Type\ConfigType;
14use Wallabag\CoreBundle\Form\Type\ChangePasswordType; 13use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
14use Wallabag\CoreBundle\Form\Type\ConfigType;
15use Wallabag\CoreBundle\Form\Type\RssType; 15use Wallabag\CoreBundle\Form\Type\RssType;
16use Wallabag\CoreBundle\Form\Type\TaggingRuleType; 16use Wallabag\CoreBundle\Form\Type\TaggingRuleType;
17use Wallabag\CoreBundle\Form\Type\UserInformationType; 17use Wallabag\CoreBundle\Form\Type\UserInformationType;
@@ -54,7 +54,7 @@ class ConfigController extends Controller
54 } 54 }
55 55
56 // handle changing password 56 // handle changing password
57 $pwdForm = $this->createForm(ChangePasswordType::class, null, ['action' => $this->generateUrl('config').'#set4']); 57 $pwdForm = $this->createForm(ChangePasswordType::class, null, ['action' => $this->generateUrl('config') . '#set4']);
58 $pwdForm->handleRequest($request); 58 $pwdForm->handleRequest($request);
59 59
60 if ($pwdForm->isSubmitted() && $pwdForm->isValid()) { 60 if ($pwdForm->isSubmitted() && $pwdForm->isValid()) {
@@ -69,13 +69,13 @@ class ConfigController extends Controller
69 69
70 $this->get('session')->getFlashBag()->add('notice', $message); 70 $this->get('session')->getFlashBag()->add('notice', $message);
71 71
72 return $this->redirect($this->generateUrl('config').'#set4'); 72 return $this->redirect($this->generateUrl('config') . '#set4');
73 } 73 }
74 74
75 // handle changing user information 75 // handle changing user information
76 $userForm = $this->createForm(UserInformationType::class, $user, [ 76 $userForm = $this->createForm(UserInformationType::class, $user, [
77 'validation_groups' => ['Profile'], 77 'validation_groups' => ['Profile'],
78 'action' => $this->generateUrl('config').'#set3', 78 'action' => $this->generateUrl('config') . '#set3',
79 ]); 79 ]);
80 $userForm->handleRequest($request); 80 $userForm->handleRequest($request);
81 81
@@ -87,11 +87,11 @@ class ConfigController extends Controller
87 'flashes.config.notice.user_updated' 87 'flashes.config.notice.user_updated'
88 ); 88 );
89 89
90 return $this->redirect($this->generateUrl('config').'#set3'); 90 return $this->redirect($this->generateUrl('config') . '#set3');
91 } 91 }
92 92
93 // handle rss information 93 // handle rss information
94 $rssForm = $this->createForm(RssType::class, $config, ['action' => $this->generateUrl('config').'#set2']); 94 $rssForm = $this->createForm(RssType::class, $config, ['action' => $this->generateUrl('config') . '#set2']);
95 $rssForm->handleRequest($request); 95 $rssForm->handleRequest($request);
96 96
97 if ($rssForm->isSubmitted() && $rssForm->isValid()) { 97 if ($rssForm->isSubmitted() && $rssForm->isValid()) {
@@ -103,12 +103,12 @@ class ConfigController extends Controller
103 'flashes.config.notice.rss_updated' 103 'flashes.config.notice.rss_updated'
104 ); 104 );
105 105
106 return $this->redirect($this->generateUrl('config').'#set2'); 106 return $this->redirect($this->generateUrl('config') . '#set2');
107 } 107 }
108 108
109 // handle tagging rule 109 // handle tagging rule
110 $taggingRule = new TaggingRule(); 110 $taggingRule = new TaggingRule();
111 $action = $this->generateUrl('config').'#set5'; 111 $action = $this->generateUrl('config') . '#set5';
112 112
113 if ($request->query->has('tagging-rule')) { 113 if ($request->query->has('tagging-rule')) {
114 $taggingRule = $this->getDoctrine() 114 $taggingRule = $this->getDoctrine()
@@ -119,7 +119,7 @@ class ConfigController extends Controller
119 return $this->redirect($action); 119 return $this->redirect($action);
120 } 120 }
121 121
122 $action = $this->generateUrl('config').'?tagging-rule='.$taggingRule->getId().'#set5'; 122 $action = $this->generateUrl('config') . '?tagging-rule=' . $taggingRule->getId() . '#set5';
123 } 123 }
124 124
125 $newTaggingRule = $this->createForm(TaggingRuleType::class, $taggingRule, ['action' => $action]); 125 $newTaggingRule = $this->createForm(TaggingRuleType::class, $taggingRule, ['action' => $action]);
@@ -135,7 +135,7 @@ class ConfigController extends Controller
135 'flashes.config.notice.tagging_rules_updated' 135 'flashes.config.notice.tagging_rules_updated'
136 ); 136 );
137 137
138 return $this->redirect($this->generateUrl('config').'#set5'); 138 return $this->redirect($this->generateUrl('config') . '#set5');
139 } 139 }
140 140
141 return $this->render('WallabagCoreBundle:Config:index.html.twig', [ 141 return $this->render('WallabagCoreBundle:Config:index.html.twig', [
@@ -151,9 +151,8 @@ class ConfigController extends Controller
151 'token' => $config->getRssToken(), 151 'token' => $config->getRssToken(),
152 ], 152 ],
153 'twofactor_auth' => $this->getParameter('twofactor_auth'), 153 'twofactor_auth' => $this->getParameter('twofactor_auth'),
154 'wallabag_url' => $this->get('craue_config')->get('wallabag_url'), 154 'wallabag_url' => $this->getParameter('domain_name'),
155 'enabled_users' => $this->getDoctrine() 155 'enabled_users' => $this->get('wallabag_user.user_repository')
156 ->getRepository('WallabagUserBundle:User')
157 ->getSumEnabledUsers(), 156 ->getSumEnabledUsers(),
158 ]); 157 ]);
159 } 158 }
@@ -183,7 +182,7 @@ class ConfigController extends Controller
183 'flashes.config.notice.rss_token_updated' 182 'flashes.config.notice.rss_token_updated'
184 ); 183 );
185 184
186 return $this->redirect($this->generateUrl('config').'#set2'); 185 return $this->redirect($this->generateUrl('config') . '#set2');
187 } 186 }
188 187
189 /** 188 /**
@@ -208,7 +207,7 @@ class ConfigController extends Controller
208 'flashes.config.notice.tagging_rules_deleted' 207 'flashes.config.notice.tagging_rules_deleted'
209 ); 208 );
210 209
211 return $this->redirect($this->generateUrl('config').'#set5'); 210 return $this->redirect($this->generateUrl('config') . '#set5');
212 } 211 }
213 212
214 /** 213 /**
@@ -224,7 +223,7 @@ class ConfigController extends Controller
224 { 223 {
225 $this->validateRuleAction($rule); 224 $this->validateRuleAction($rule);
226 225
227 return $this->redirect($this->generateUrl('config').'?tagging-rule='.$rule->getId().'#set5'); 226 return $this->redirect($this->generateUrl('config') . '?tagging-rule=' . $rule->getId() . '#set5');
228 } 227 }
229 228
230 /** 229 /**
@@ -242,56 +241,114 @@ class ConfigController extends Controller
242 ->getRepository('WallabagAnnotationBundle:Annotation') 241 ->getRepository('WallabagAnnotationBundle:Annotation')
243 ->removeAllByUserId($this->getUser()->getId()); 242 ->removeAllByUserId($this->getUser()->getId());
244 break; 243 break;
245
246 case 'tags': 244 case 'tags':
247 $this->removeAllTagsByUserId($this->getUser()->getId()); 245 $this->removeAllTagsByUserId($this->getUser()->getId());
248 break; 246 break;
249
250 case 'entries': 247 case 'entries':
251 // SQLite doesn't care about cascading remove, so we need to manually remove associated stuf 248 // SQLite doesn't care about cascading remove, so we need to manually remove associated stuff
252 // otherwise they won't be removed ... 249 // otherwise they won't be removed ...
253 if ($this->get('doctrine')->getConnection()->getDriver() instanceof \Doctrine\DBAL\Driver\PDOSqlite\Driver) { 250 if ($this->get('doctrine')->getConnection()->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform) {
254 $this->getDoctrine()->getRepository('WallabagAnnotationBundle:Annotation')->removeAllByUserId($this->getUser()->getId()); 251 $this->getDoctrine()->getRepository('WallabagAnnotationBundle:Annotation')->removeAllByUserId($this->getUser()->getId());
255 } 252 }
256 253
257 // manually remove tags to avoid orphan tag 254 // manually remove tags to avoid orphan tag
258 $this->removeAllTagsByUserId($this->getUser()->getId()); 255 $this->removeAllTagsByUserId($this->getUser()->getId());
259 256
260 $this->getDoctrine() 257 $this->get('wallabag_core.entry_repository')->removeAllByUserId($this->getUser()->getId());
261 ->getRepository('WallabagCoreBundle:Entry') 258 break;
262 ->removeAllByUserId($this->getUser()->getId()); 259 case 'archived':
260 if ($this->get('doctrine')->getConnection()->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform) {
261 $this->removeAnnotationsForArchivedByUserId($this->getUser()->getId());
262 }
263
264 // manually remove tags to avoid orphan tag
265 $this->removeTagsForArchivedByUserId($this->getUser()->getId());
266
267 $this->get('wallabag_core.entry_repository')->removeArchivedByUserId($this->getUser()->getId());
268 break;
263 } 269 }
264 270
265 $this->get('session')->getFlashBag()->add( 271 $this->get('session')->getFlashBag()->add(
266 'notice', 272 'notice',
267 'flashes.config.notice.'.$type.'_reset' 273 'flashes.config.notice.' . $type . '_reset'
268 ); 274 );
269 275
270 return $this->redirect($this->generateUrl('config').'#set3'); 276 return $this->redirect($this->generateUrl('config') . '#set3');
271 } 277 }
272 278
273 /** 279 /**
274 * Remove all tags for a given user and cleanup orphan tags. 280 * Delete account for current user.
275 * 281 *
276 * @param int $userId 282 * @Route("/account/delete", name="delete_account")
283 *
284 * @param Request $request
285 *
286 * @throws AccessDeniedHttpException
287 *
288 * @return \Symfony\Component\HttpFoundation\RedirectResponse
277 */ 289 */
278 private function removeAllTagsByUserId($userId) 290 public function deleteAccountAction(Request $request)
291 {
292 $enabledUsers = $this->get('wallabag_user.user_repository')
293 ->getSumEnabledUsers();
294
295 if ($enabledUsers <= 1) {
296 throw new AccessDeniedHttpException();
297 }
298
299 $user = $this->getUser();
300
301 // logout current user
302 $this->get('security.token_storage')->setToken(null);
303 $request->getSession()->invalidate();
304
305 $em = $this->get('fos_user.user_manager');
306 $em->deleteUser($user);
307
308 return $this->redirect($this->generateUrl('fos_user_security_login'));
309 }
310
311 /**
312 * Switch view mode for current user.
313 *
314 * @Route("/config/view-mode", name="switch_view_mode")
315 *
316 * @param Request $request
317 *
318 * @return \Symfony\Component\HttpFoundation\RedirectResponse
319 */
320 public function changeViewModeAction(Request $request)
279 { 321 {
280 $tags = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findAllTags($userId); 322 $user = $this->getUser();
323 $user->getConfig()->setListMode(!$user->getConfig()->getListMode());
324
325 $em = $this->getDoctrine()->getManager();
326 $em->persist($user);
327 $em->flush();
281 328
329 return $this->redirect($request->headers->get('referer'));
330 }
331
332 /**
333 * Remove all tags for given tags and a given user and cleanup orphan tags.
334 *
335 * @param array $tags
336 * @param int $userId
337 */
338 private function removeAllTagsByStatusAndUserId($tags, $userId)
339 {
282 if (empty($tags)) { 340 if (empty($tags)) {
283 return; 341 return;
284 } 342 }
285 343
286 $this->getDoctrine() 344 $this->get('wallabag_core.entry_repository')
287 ->getRepository('WallabagCoreBundle:Entry')
288 ->removeTags($userId, $tags); 345 ->removeTags($userId, $tags);
289 346
290 // cleanup orphan tags 347 // cleanup orphan tags
291 $em = $this->getDoctrine()->getManager(); 348 $em = $this->getDoctrine()->getManager();
292 349
293 foreach ($tags as $tag) { 350 foreach ($tags as $tag) {
294 if (count($tag->getEntries()) === 0) { 351 if (0 === count($tag->getEntries())) {
295 $em->remove($tag); 352 $em->remove($tag);
296 } 353 }
297 } 354 }
@@ -300,13 +357,50 @@ class ConfigController extends Controller
300 } 357 }
301 358
302 /** 359 /**
360 * Remove all tags for a given user and cleanup orphan tags.
361 *
362 * @param int $userId
363 */
364 private function removeAllTagsByUserId($userId)
365 {
366 $tags = $this->get('wallabag_core.tag_repository')->findAllTags($userId);
367 $this->removeAllTagsByStatusAndUserId($tags, $userId);
368 }
369
370 /**
371 * Remove all tags for a given user and cleanup orphan tags.
372 *
373 * @param int $userId
374 */
375 private function removeTagsForArchivedByUserId($userId)
376 {
377 $tags = $this->get('wallabag_core.tag_repository')->findForArchivedArticlesByUser($userId);
378 $this->removeAllTagsByStatusAndUserId($tags, $userId);
379 }
380
381 private function removeAnnotationsForArchivedByUserId($userId)
382 {
383 $em = $this->getDoctrine()->getManager();
384
385 $archivedEntriesAnnotations = $this->getDoctrine()
386 ->getRepository('WallabagAnnotationBundle:Annotation')
387 ->findAllArchivedEntriesByUser($userId);
388
389 foreach ($archivedEntriesAnnotations as $archivedEntriesAnnotation) {
390 $em->remove($archivedEntriesAnnotation);
391 }
392
393 $em->flush();
394 }
395
396 /**
303 * Validate that a rule can be edited/deleted by the current user. 397 * Validate that a rule can be edited/deleted by the current user.
304 * 398 *
305 * @param TaggingRule $rule 399 * @param TaggingRule $rule
306 */ 400 */
307 private function validateRuleAction(TaggingRule $rule) 401 private function validateRuleAction(TaggingRule $rule)
308 { 402 {
309 if ($this->getUser()->getId() != $rule->getConfig()->getUser()->getId()) { 403 if ($this->getUser()->getId() !== $rule->getConfig()->getUser()->getId()) {
310 throw $this->createAccessDeniedException('You can not access this tagging rule.'); 404 throw $this->createAccessDeniedException('You can not access this tagging rule.');
311 } 405 }
312 } 406 }
@@ -330,58 +424,4 @@ class ConfigController extends Controller
330 424
331 return $config; 425 return $config;
332 } 426 }
333
334 /**
335 * Delete account for current user.
336 *
337 * @Route("/account/delete", name="delete_account")
338 *
339 * @param Request $request
340 *
341 * @throws AccessDeniedHttpException
342 *
343 * @return \Symfony\Component\HttpFoundation\RedirectResponse
344 */
345 public function deleteAccountAction(Request $request)
346 {
347 $enabledUsers = $this->getDoctrine()
348 ->getRepository('WallabagUserBundle:User')
349 ->getSumEnabledUsers();
350
351 if ($enabledUsers <= 1) {
352 throw new AccessDeniedHttpException();
353 }
354
355 $user = $this->getUser();
356
357 // logout current user
358 $this->get('security.token_storage')->setToken(null);
359 $request->getSession()->invalidate();
360
361 $em = $this->get('fos_user.user_manager');
362 $em->deleteUser($user);
363
364 return $this->redirect($this->generateUrl('fos_user_security_login'));
365 }
366
367 /**
368 * Switch view mode for current user.
369 *
370 * @Route("/config/view-mode", name="switch_view_mode")
371 *
372 * @param Request $request
373 *
374 * @return \Symfony\Component\HttpFoundation\RedirectResponse
375 */
376 public function changeViewModeAction(Request $request)
377 {
378 $user = $this->getUser();
379 $user->getConfig()->setListMode(!$user->getConfig()->getListMode());
380
381 $em = $this->getDoctrine()->getManager();
382 $em->persist($user);
383 $em->flush();
384
385 return $this->redirect($request->headers->get('referer'));
386 }
387} 427}
diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php
index f7398e69..840dc254 100644
--- a/src/Wallabag/CoreBundle/Controller/EntryController.php
+++ b/src/Wallabag/CoreBundle/Controller/EntryController.php
@@ -4,17 +4,17 @@ namespace Wallabag\CoreBundle\Controller;
4 4
5use Pagerfanta\Adapter\DoctrineORMAdapter; 5use Pagerfanta\Adapter\DoctrineORMAdapter;
6use Pagerfanta\Exception\OutOfRangeCurrentPageException; 6use Pagerfanta\Exception\OutOfRangeCurrentPageException;
7use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
7use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 8use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
8use Symfony\Bundle\FrameworkBundle\Controller\Controller; 9use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9use Symfony\Component\HttpFoundation\Request; 10use Symfony\Component\HttpFoundation\Request;
10use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 11use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
11use Wallabag\CoreBundle\Entity\Entry; 12use Wallabag\CoreBundle\Entity\Entry;
12use Wallabag\CoreBundle\Form\Type\EntryFilterType; 13use Wallabag\CoreBundle\Event\EntryDeletedEvent;
14use Wallabag\CoreBundle\Event\EntrySavedEvent;
13use Wallabag\CoreBundle\Form\Type\EditEntryType; 15use Wallabag\CoreBundle\Form\Type\EditEntryType;
16use Wallabag\CoreBundle\Form\Type\EntryFilterType;
14use Wallabag\CoreBundle\Form\Type\NewEntryType; 17use Wallabag\CoreBundle\Form\Type\NewEntryType;
15use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
16use Wallabag\CoreBundle\Event\EntrySavedEvent;
17use Wallabag\CoreBundle\Event\EntryDeletedEvent;
18use Wallabag\CoreBundle\Form\Type\SearchEntryType; 18use Wallabag\CoreBundle\Form\Type\SearchEntryType;
19 19
20class EntryController extends Controller 20class EntryController extends Controller
@@ -52,38 +52,6 @@ class EntryController extends Controller
52 } 52 }
53 53
54 /** 54 /**
55 * Fetch content and update entry.
56 * In case it fails, entry will return to avod loosing the data.
57 *
58 * @param Entry $entry
59 * @param string $prefixMessage Should be the translation key: entry_saved or entry_reloaded
60 *
61 * @return Entry
62 */
63 private function updateEntry(Entry $entry, $prefixMessage = 'entry_saved')
64 {
65 // put default title in case of fetching content failed
66 $entry->setTitle('No title found');
67
68 $message = 'flashes.entry.notice.'.$prefixMessage;
69
70 try {
71 $entry = $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl());
72 } catch (\Exception $e) {
73 $this->get('logger')->error('Error while saving an entry', [
74 'exception' => $e,
75 'entry' => $entry,
76 ]);
77
78 $message = 'flashes.entry.notice.'.$prefixMessage.'_failed';
79 }
80
81 $this->get('session')->getFlashBag()->add('notice', $message);
82
83 return $entry;
84 }
85
86 /**
87 * @param Request $request 55 * @param Request $request
88 * 56 *
89 * @Route("/new-entry", name="new_entry") 57 * @Route("/new-entry", name="new_entry")
@@ -227,7 +195,7 @@ class EntryController extends Controller
227 public function showUnreadAction(Request $request, $page) 195 public function showUnreadAction(Request $request, $page)
228 { 196 {
229 // load the quickstart if no entry in database 197 // load the quickstart if no entry in database
230 if ($page == 1 && $this->get('wallabag_core.entry_repository')->countAllEntriesByUsername($this->getUser()->getId()) == 0) { 198 if (1 === (int) $page && 0 === $this->get('wallabag_core.entry_repository')->countAllEntriesByUser($this->getUser()->getId())) {
231 return $this->redirect($this->generateUrl('quickstart')); 199 return $this->redirect($this->generateUrl('quickstart'));
232 } 200 }
233 201
@@ -265,84 +233,6 @@ class EntryController extends Controller
265 } 233 }
266 234
267 /** 235 /**
268 * Global method to retrieve entries depending on the given type
269 * It returns the response to be send.
270 *
271 * @param string $type Entries type: unread, starred or archive
272 * @param Request $request
273 * @param int $page
274 *
275 * @return \Symfony\Component\HttpFoundation\Response
276 */
277 private function showEntries($type, Request $request, $page)
278 {
279 $repository = $this->get('wallabag_core.entry_repository');
280 $searchTerm = (isset($request->get('search_entry')['term']) ? $request->get('search_entry')['term'] : '');
281 $currentRoute = (!is_null($request->query->get('currentRoute')) ? $request->query->get('currentRoute') : '');
282
283 switch ($type) {
284 case 'search':
285 $qb = $repository->getBuilderForSearchByUser($this->getUser()->getId(), $searchTerm, $currentRoute);
286
287 break;
288 case 'untagged':
289 $qb = $repository->getBuilderForUntaggedByUser($this->getUser()->getId());
290
291 break;
292 case 'starred':
293 $qb = $repository->getBuilderForStarredByUser($this->getUser()->getId());
294 break;
295
296 case 'archive':
297 $qb = $repository->getBuilderForArchiveByUser($this->getUser()->getId());
298 break;
299
300 case 'unread':
301 $qb = $repository->getBuilderForUnreadByUser($this->getUser()->getId());
302 break;
303
304 case 'all':
305 $qb = $repository->getBuilderForAllByUser($this->getUser()->getId());
306 break;
307
308 default:
309 throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
310 }
311
312 $form = $this->createForm(EntryFilterType::class);
313
314 if ($request->query->has($form->getName())) {
315 // manually bind values from the request
316 $form->submit($request->query->get($form->getName()));
317
318 // build the query from the given form object
319 $this->get('lexik_form_filter.query_builder_updater')->addFilterConditions($form, $qb);
320 }
321
322 $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
323
324 $entries = $this->get('wallabag_core.helper.prepare_pager_for_entries')
325 ->prepare($pagerAdapter, $page);
326
327 try {
328 $entries->setCurrentPage($page);
329 } catch (OutOfRangeCurrentPageException $e) {
330 if ($page > 1) {
331 return $this->redirect($this->generateUrl($type, ['page' => $entries->getNbPages()]), 302);
332 }
333 }
334
335 return $this->render(
336 'WallabagCoreBundle:Entry:entries.html.twig', [
337 'form' => $form->createView(),
338 'entries' => $entries,
339 'currentPage' => $page,
340 'searchTerm' => $searchTerm,
341 ]
342 );
343 }
344
345 /**
346 * Shows entry content. 236 * Shows entry content.
347 * 237 *
348 * @param Entry $entry 238 * @param Entry $entry
@@ -443,6 +333,7 @@ class EntryController extends Controller
443 $this->checkUserAction($entry); 333 $this->checkUserAction($entry);
444 334
445 $entry->toggleStar(); 335 $entry->toggleStar();
336 $entry->updateStar($entry->isStarred());
446 $this->getDoctrine()->getManager()->flush(); 337 $this->getDoctrine()->getManager()->flush();
447 338
448 $message = 'flashes.entry.notice.entry_unstarred'; 339 $message = 'flashes.entry.notice.entry_unstarred';
@@ -495,7 +386,7 @@ class EntryController extends Controller
495 386
496 // don't redirect user to the deleted entry (check that the referer doesn't end with the same url) 387 // don't redirect user to the deleted entry (check that the referer doesn't end with the same url)
497 $referer = $request->headers->get('referer'); 388 $referer = $request->headers->get('referer');
498 $to = (1 !== preg_match('#'.$url.'$#i', $referer) ? $referer : null); 389 $to = (1 !== preg_match('#' . $url . '$#i', $referer) ? $referer : null);
499 390
500 $redirectUrl = $this->get('wallabag_core.helper.redirect')->to($to); 391 $redirectUrl = $this->get('wallabag_core.helper.redirect')->to($to);
501 392
@@ -503,30 +394,6 @@ class EntryController extends Controller
503 } 394 }
504 395
505 /** 396 /**
506 * Check if the logged user can manage the given entry.
507 *
508 * @param Entry $entry
509 */
510 private function checkUserAction(Entry $entry)
511 {
512 if (null === $this->getUser() || $this->getUser()->getId() != $entry->getUser()->getId()) {
513 throw $this->createAccessDeniedException('You can not access this entry.');
514 }
515 }
516
517 /**
518 * Check for existing entry, if it exists, redirect to it with a message.
519 *
520 * @param Entry $entry
521 *
522 * @return Entry|bool
523 */
524 private function checkIfEntryAlreadyExists(Entry $entry)
525 {
526 return $this->get('wallabag_core.entry_repository')->findByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
527 }
528
529 /**
530 * Get public URL for entry (and generate it if necessary). 397 * Get public URL for entry (and generate it if necessary).
531 * 398 *
532 * @param Entry $entry 399 * @param Entry $entry
@@ -612,4 +479,127 @@ class EntryController extends Controller
612 { 479 {
613 return $this->showEntries('untagged', $request, $page); 480 return $this->showEntries('untagged', $request, $page);
614 } 481 }
482
483 /**
484 * Fetch content and update entry.
485 * In case it fails, $entry->getContent will return an error message.
486 *
487 * @param Entry $entry
488 * @param string $prefixMessage Should be the translation key: entry_saved or entry_reloaded
489 */
490 private function updateEntry(Entry $entry, $prefixMessage = 'entry_saved')
491 {
492 $message = 'flashes.entry.notice.' . $prefixMessage;
493
494 try {
495 $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl());
496 } catch (\Exception $e) {
497 $this->get('logger')->error('Error while saving an entry', [
498 'exception' => $e,
499 'entry' => $entry,
500 ]);
501
502 $message = 'flashes.entry.notice.' . $prefixMessage . '_failed';
503 }
504
505 $this->get('session')->getFlashBag()->add('notice', $message);
506 }
507
508 /**
509 * Global method to retrieve entries depending on the given type
510 * It returns the response to be send.
511 *
512 * @param string $type Entries type: unread, starred or archive
513 * @param Request $request
514 * @param int $page
515 *
516 * @return \Symfony\Component\HttpFoundation\Response
517 */
518 private function showEntries($type, Request $request, $page)
519 {
520 $repository = $this->get('wallabag_core.entry_repository');
521 $searchTerm = (isset($request->get('search_entry')['term']) ? $request->get('search_entry')['term'] : '');
522 $currentRoute = (null !== $request->query->get('currentRoute') ? $request->query->get('currentRoute') : '');
523
524 switch ($type) {
525 case 'search':
526 $qb = $repository->getBuilderForSearchByUser($this->getUser()->getId(), $searchTerm, $currentRoute);
527
528 break;
529 case 'untagged':
530 $qb = $repository->getBuilderForUntaggedByUser($this->getUser()->getId());
531
532 break;
533 case 'starred':
534 $qb = $repository->getBuilderForStarredByUser($this->getUser()->getId());
535 break;
536 case 'archive':
537 $qb = $repository->getBuilderForArchiveByUser($this->getUser()->getId());
538 break;
539 case 'unread':
540 $qb = $repository->getBuilderForUnreadByUser($this->getUser()->getId());
541 break;
542 case 'all':
543 $qb = $repository->getBuilderForAllByUser($this->getUser()->getId());
544 break;
545 default:
546 throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
547 }
548
549 $form = $this->createForm(EntryFilterType::class);
550
551 if ($request->query->has($form->getName())) {
552 // manually bind values from the request
553 $form->submit($request->query->get($form->getName()));
554
555 // build the query from the given form object
556 $this->get('lexik_form_filter.query_builder_updater')->addFilterConditions($form, $qb);
557 }
558
559 $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
560
561 $entries = $this->get('wallabag_core.helper.prepare_pager_for_entries')->prepare($pagerAdapter);
562
563 try {
564 $entries->setCurrentPage($page);
565 } catch (OutOfRangeCurrentPageException $e) {
566 if ($page > 1) {
567 return $this->redirect($this->generateUrl($type, ['page' => $entries->getNbPages()]), 302);
568 }
569 }
570
571 return $this->render(
572 'WallabagCoreBundle:Entry:entries.html.twig', [
573 'form' => $form->createView(),
574 'entries' => $entries,
575 'currentPage' => $page,
576 'searchTerm' => $searchTerm,
577 'isFiltered' => $form->isSubmitted(),
578 ]
579 );
580 }
581
582 /**
583 * Check if the logged user can manage the given entry.
584 *
585 * @param Entry $entry
586 */
587 private function checkUserAction(Entry $entry)
588 {
589 if (null === $this->getUser() || $this->getUser()->getId() !== $entry->getUser()->getId()) {
590 throw $this->createAccessDeniedException('You can not access this entry.');
591 }
592 }
593
594 /**
595 * Check for existing entry, if it exists, redirect to it with a message.
596 *
597 * @param Entry $entry
598 *
599 * @return Entry|bool
600 */
601 private function checkIfEntryAlreadyExists(Entry $entry)
602 {
603 return $this->get('wallabag_core.entry_repository')->findByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
604 }
615} 605}
diff --git a/src/Wallabag/CoreBundle/Controller/ExceptionController.php b/src/Wallabag/CoreBundle/Controller/ExceptionController.php
index abfa9c2f..461309ea 100644
--- a/src/Wallabag/CoreBundle/Controller/ExceptionController.php
+++ b/src/Wallabag/CoreBundle/Controller/ExceptionController.php
@@ -14,7 +14,7 @@ class ExceptionController extends BaseExceptionController
14 protected function findTemplate(Request $request, $format, $code, $showException) 14 protected function findTemplate(Request $request, $format, $code, $showException)
15 { 15 {
16 $name = $showException ? 'exception' : 'error'; 16 $name = $showException ? 'exception' : 'error';
17 if ($showException && 'html' == $format) { 17 if ($showException && 'html' === $format) {
18 $name = 'exception_full'; 18 $name = 'exception_full';
19 } 19 }
20 20
diff --git a/src/Wallabag/CoreBundle/Controller/ExportController.php b/src/Wallabag/CoreBundle/Controller/ExportController.php
index abc3336a..7ca89239 100644
--- a/src/Wallabag/CoreBundle/Controller/ExportController.php
+++ b/src/Wallabag/CoreBundle/Controller/ExportController.php
@@ -7,7 +7,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 8use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
9use Wallabag\CoreBundle\Entity\Entry; 9use Wallabag\CoreBundle\Entity\Entry;
10use Wallabag\CoreBundle\Entity\Tag;
11 10
12/** 11/**
13 * The try/catch can be removed once all formats will be implemented. 12 * The try/catch can be removed once all formats will be implemented.
@@ -34,6 +33,7 @@ class ExportController extends Controller
34 return $this->get('wallabag_core.helper.entries_export') 33 return $this->get('wallabag_core.helper.entries_export')
35 ->setEntries($entry) 34 ->setEntries($entry)
36 ->updateTitle('entry') 35 ->updateTitle('entry')
36 ->updateAuthor('entry')
37 ->exportAs($format); 37 ->exportAs($format);
38 } catch (\InvalidArgumentException $e) { 38 } catch (\InvalidArgumentException $e) {
39 throw new NotFoundHttpException($e->getMessage()); 39 throw new NotFoundHttpException($e->getMessage());
@@ -56,17 +56,18 @@ class ExportController extends Controller
56 public function downloadEntriesAction(Request $request, $format, $category) 56 public function downloadEntriesAction(Request $request, $format, $category)
57 { 57 {
58 $method = ucfirst($category); 58 $method = ucfirst($category);
59 $methodBuilder = 'getBuilderFor'.$method.'ByUser'; 59 $methodBuilder = 'getBuilderFor' . $method . 'ByUser';
60 $repository = $this->get('wallabag_core.entry_repository');
60 61
61 if ($category == 'tag_entries') { 62 if ('tag_entries' === $category) {
62 $tag = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findOneBySlug($request->query->get('tag')); 63 $tag = $this->get('wallabag_core.tag_repository')->findOneBySlug($request->query->get('tag'));
63 64
64 $entries = $this->getDoctrine() 65 $entries = $repository->findAllByTagId(
65 ->getRepository('WallabagCoreBundle:Entry') 66 $this->getUser()->getId(),
66 ->findAllByTagId($this->getUser()->getId(), $tag->getId()); 67 $tag->getId()
68 );
67 } else { 69 } else {
68 $entries = $this->getDoctrine() 70 $entries = $repository
69 ->getRepository('WallabagCoreBundle:Entry')
70 ->$methodBuilder($this->getUser()->getId()) 71 ->$methodBuilder($this->getUser()->getId())
71 ->getQuery() 72 ->getQuery()
72 ->getResult(); 73 ->getResult();
@@ -76,6 +77,7 @@ class ExportController extends Controller
76 return $this->get('wallabag_core.helper.entries_export') 77 return $this->get('wallabag_core.helper.entries_export')
77 ->setEntries($entries) 78 ->setEntries($entries)
78 ->updateTitle($method) 79 ->updateTitle($method)
80 ->updateAuthor($method)
79 ->exportAs($format); 81 ->exportAs($format);
80 } catch (\InvalidArgumentException $e) { 82 } catch (\InvalidArgumentException $e) {
81 throw new NotFoundHttpException($e->getMessage()); 83 throw new NotFoundHttpException($e->getMessage());
diff --git a/src/Wallabag/CoreBundle/Controller/RssController.php b/src/Wallabag/CoreBundle/Controller/RssController.php
index 92f18707..e84044b1 100644
--- a/src/Wallabag/CoreBundle/Controller/RssController.php
+++ b/src/Wallabag/CoreBundle/Controller/RssController.php
@@ -2,16 +2,19 @@
2 2
3namespace Wallabag\CoreBundle\Controller; 3namespace Wallabag\CoreBundle\Controller;
4 4
5use Pagerfanta\Adapter\ArrayAdapter;
5use Pagerfanta\Adapter\DoctrineORMAdapter; 6use Pagerfanta\Adapter\DoctrineORMAdapter;
6use Pagerfanta\Exception\OutOfRangeCurrentPageException; 7use Pagerfanta\Exception\OutOfRangeCurrentPageException;
7use Pagerfanta\Pagerfanta; 8use Pagerfanta\Pagerfanta;
8use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 9use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
9use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 10use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
10use Symfony\Component\HttpFoundation\Request;
11use Symfony\Bundle\FrameworkBundle\Controller\Controller; 11use Symfony\Bundle\FrameworkBundle\Controller\Controller;
12use Symfony\Component\HttpFoundation\Request;
13use Symfony\Component\HttpFoundation\Response;
14use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
12use Wallabag\CoreBundle\Entity\Entry; 15use Wallabag\CoreBundle\Entity\Entry;
16use Wallabag\CoreBundle\Entity\Tag;
13use Wallabag\UserBundle\Entity\User; 17use Wallabag\UserBundle\Entity\User;
14use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
15 18
16class RssController extends Controller 19class RssController extends Controller
17{ 20{
@@ -23,7 +26,7 @@ class RssController extends Controller
23 * 26 *
24 * @return \Symfony\Component\HttpFoundation\Response 27 * @return \Symfony\Component\HttpFoundation\Response
25 */ 28 */
26 public function showUnreadAction(Request $request, User $user) 29 public function showUnreadRSSAction(Request $request, User $user)
27 { 30 {
28 return $this->showEntries('unread', $user, $request->query->get('page', 1)); 31 return $this->showEntries('unread', $user, $request->query->get('page', 1));
29 } 32 }
@@ -31,12 +34,12 @@ class RssController extends Controller
31 /** 34 /**
32 * Shows read entries for current user. 35 * Shows read entries for current user.
33 * 36 *
34 * @Route("/{username}/{token}/archive.xml", name="archive_rss") 37 * @Route("/{username}/{token}/archive.xml", name="archive_rss", defaults={"_format"="xml"})
35 * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") 38 * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter")
36 * 39 *
37 * @return \Symfony\Component\HttpFoundation\Response 40 * @return \Symfony\Component\HttpFoundation\Response
38 */ 41 */
39 public function showArchiveAction(Request $request, User $user) 42 public function showArchiveRSSAction(Request $request, User $user)
40 { 43 {
41 return $this->showEntries('archive', $user, $request->query->get('page', 1)); 44 return $this->showEntries('archive', $user, $request->query->get('page', 1));
42 } 45 }
@@ -44,17 +47,89 @@ class RssController extends Controller
44 /** 47 /**
45 * Shows starred entries for current user. 48 * Shows starred entries for current user.
46 * 49 *
47 * @Route("/{username}/{token}/starred.xml", name="starred_rss") 50 * @Route("/{username}/{token}/starred.xml", name="starred_rss", defaults={"_format"="xml"})
48 * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") 51 * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter")
49 * 52 *
50 * @return \Symfony\Component\HttpFoundation\Response 53 * @return \Symfony\Component\HttpFoundation\Response
51 */ 54 */
52 public function showStarredAction(Request $request, User $user) 55 public function showStarredRSSAction(Request $request, User $user)
53 { 56 {
54 return $this->showEntries('starred', $user, $request->query->get('page', 1)); 57 return $this->showEntries('starred', $user, $request->query->get('page', 1));
55 } 58 }
56 59
57 /** 60 /**
61 * Shows all entries for current user.
62 *
63 * @Route("/{username}/{token}/all.xml", name="all_rss", defaults={"_format"="xml"})
64 * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter")
65 *
66 * @return \Symfony\Component\HttpFoundation\Response
67 */
68 public function showAllRSSAction(Request $request, User $user)
69 {
70 return $this->showEntries('all', $user, $request->query->get('page', 1));
71 }
72
73 /**
74 * Shows entries associated to a tag for current user.
75 *
76 * @Route("/{username}/{token}/tags/{slug}.xml", name="tag_rss", defaults={"_format"="xml"})
77 * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter")
78 * @ParamConverter("tag", options={"mapping": {"slug": "slug"}})
79 *
80 * @return \Symfony\Component\HttpFoundation\Response
81 */
82 public function showTagsAction(Request $request, User $user, Tag $tag)
83 {
84 $page = $request->query->get('page', 1);
85
86 $url = $this->generateUrl(
87 'tag_rss',
88 [
89 'username' => $user->getUsername(),
90 'token' => $user->getConfig()->getRssToken(),
91 'slug' => $tag->getSlug(),
92 ],
93 UrlGeneratorInterface::ABSOLUTE_URL
94 );
95
96 $entriesByTag = $this->get('wallabag_core.entry_repository')->findAllByTagId(
97 $user->getId(),
98 $tag->getId()
99 );
100
101 $pagerAdapter = new ArrayAdapter($entriesByTag);
102
103 $entries = $this->get('wallabag_core.helper.prepare_pager_for_entries')->prepare(
104 $pagerAdapter,
105 $user
106 );
107
108 if (null === $entries) {
109 throw $this->createNotFoundException('No entries found?');
110 }
111
112 try {
113 $entries->setCurrentPage($page);
114 } catch (OutOfRangeCurrentPageException $e) {
115 if ($page > 1) {
116 return $this->redirect($url . '?page=' . $entries->getNbPages(), 302);
117 }
118 }
119
120 return $this->render(
121 '@WallabagCore/themes/common/Entry/entries.xml.twig',
122 [
123 'url_html' => $this->generateUrl('tag_entries', ['slug' => $tag->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL),
124 'type' => 'tag (' . $tag->getLabel() . ')',
125 'url' => $url,
126 'entries' => $entries,
127 ],
128 new Response('', 200, ['Content-Type' => 'application/rss+xml'])
129 );
130 }
131
132 /**
58 * Global method to retrieve entries depending on the given type 133 * Global method to retrieve entries depending on the given type
59 * It returns the response to be send. 134 * It returns the response to be send.
60 * 135 *
@@ -66,21 +141,21 @@ class RssController extends Controller
66 */ 141 */
67 private function showEntries($type, User $user, $page = 1) 142 private function showEntries($type, User $user, $page = 1)
68 { 143 {
69 $repository = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry'); 144 $repository = $this->get('wallabag_core.entry_repository');
70 145
71 switch ($type) { 146 switch ($type) {
72 case 'starred': 147 case 'starred':
73 $qb = $repository->getBuilderForStarredByUser($user->getId()); 148 $qb = $repository->getBuilderForStarredByUser($user->getId());
74 break; 149 break;
75
76 case 'archive': 150 case 'archive':
77 $qb = $repository->getBuilderForArchiveByUser($user->getId()); 151 $qb = $repository->getBuilderForArchiveByUser($user->getId());
78 break; 152 break;
79
80 case 'unread': 153 case 'unread':
81 $qb = $repository->getBuilderForUnreadByUser($user->getId()); 154 $qb = $repository->getBuilderForUnreadByUser($user->getId());
82 break; 155 break;
83 156 case 'all':
157 $qb = $repository->getBuilderForAllByUser($user->getId());
158 break;
84 default: 159 default:
85 throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type)); 160 throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
86 } 161 }
@@ -92,7 +167,7 @@ class RssController extends Controller
92 $entries->setMaxPerPage($perPage); 167 $entries->setMaxPerPage($perPage);
93 168
94 $url = $this->generateUrl( 169 $url = $this->generateUrl(
95 $type.'_rss', 170 $type . '_rss',
96 [ 171 [
97 'username' => $user->getUsername(), 172 'username' => $user->getUsername(),
98 'token' => $user->getConfig()->getRssToken(), 173 'token' => $user->getConfig()->getRssToken(),
@@ -104,14 +179,19 @@ class RssController extends Controller
104 $entries->setCurrentPage((int) $page); 179 $entries->setCurrentPage((int) $page);
105 } catch (OutOfRangeCurrentPageException $e) { 180 } catch (OutOfRangeCurrentPageException $e) {
106 if ($page > 1) { 181 if ($page > 1) {
107 return $this->redirect($url.'?page='.$entries->getNbPages(), 302); 182 return $this->redirect($url . '?page=' . $entries->getNbPages(), 302);
108 } 183 }
109 } 184 }
110 185
111 return $this->render('@WallabagCore/themes/common/Entry/entries.xml.twig', [ 186 return $this->render(
112 'type' => $type, 187 '@WallabagCore/themes/common/Entry/entries.xml.twig',
113 'url' => $url, 188 [
114 'entries' => $entries, 189 'url_html' => $this->generateUrl($type, [], UrlGeneratorInterface::ABSOLUTE_URL),
115 ]); 190 'type' => $type,
191 'url' => $url,
192 'entries' => $entries,
193 ],
194 new Response('', 200, ['Content-Type' => 'application/rss+xml'])
195 );
116 } 196 }
117} 197}
diff --git a/src/Wallabag/CoreBundle/Controller/SiteCredentialController.php b/src/Wallabag/CoreBundle/Controller/SiteCredentialController.php
new file mode 100644
index 00000000..fa2066dc
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Controller/SiteCredentialController.php
@@ -0,0 +1,174 @@
1<?php
2
3namespace Wallabag\CoreBundle\Controller;
4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
7use Symfony\Bundle\FrameworkBundle\Controller\Controller;
8use Symfony\Component\HttpFoundation\Request;
9use Wallabag\CoreBundle\Entity\SiteCredential;
10use Wallabag\UserBundle\Entity\User;
11
12/**
13 * SiteCredential controller.
14 *
15 * @Route("/site-credentials")
16 */
17class SiteCredentialController extends Controller
18{
19 /**
20 * Lists all User entities.
21 *
22 * @Route("/", name="site_credentials_index")
23 * @Method("GET")
24 */
25 public function indexAction()
26 {
27 $credentials = $this->get('wallabag_core.site_credential_repository')->findByUser($this->getUser());
28
29 return $this->render('WallabagCoreBundle:SiteCredential:index.html.twig', [
30 'credentials' => $credentials,
31 ]);
32 }
33
34 /**
35 * Creates a new site credential entity.
36 *
37 * @Route("/new", name="site_credentials_new")
38 * @Method({"GET", "POST"})
39 *
40 * @param Request $request
41 *
42 * @return \Symfony\Component\HttpFoundation\Response
43 */
44 public function newAction(Request $request)
45 {
46 $credential = new SiteCredential($this->getUser());
47
48 $form = $this->createForm('Wallabag\CoreBundle\Form\Type\SiteCredentialType', $credential);
49 $form->handleRequest($request);
50
51 if ($form->isSubmitted() && $form->isValid()) {
52 $credential->setUsername($this->get('wallabag_core.helper.crypto_proxy')->crypt($credential->getUsername()));
53 $credential->setPassword($this->get('wallabag_core.helper.crypto_proxy')->crypt($credential->getPassword()));
54
55 $em = $this->getDoctrine()->getManager();
56 $em->persist($credential);
57 $em->flush();
58
59 $this->get('session')->getFlashBag()->add(
60 'notice',
61 $this->get('translator')->trans('flashes.site_credential.notice.added', ['%host%' => $credential->getHost()])
62 );
63
64 return $this->redirectToRoute('site_credentials_index');
65 }
66
67 return $this->render('WallabagCoreBundle:SiteCredential:new.html.twig', [
68 'credential' => $credential,
69 'form' => $form->createView(),
70 ]);
71 }
72
73 /**
74 * Displays a form to edit an existing site credential entity.
75 *
76 * @Route("/{id}/edit", name="site_credentials_edit")
77 * @Method({"GET", "POST"})
78 *
79 * @param Request $request
80 * @param SiteCredential $siteCredential
81 *
82 * @return \Symfony\Component\HttpFoundation\Response
83 */
84 public function editAction(Request $request, SiteCredential $siteCredential)
85 {
86 $this->checkUserAction($siteCredential);
87
88 $deleteForm = $this->createDeleteForm($siteCredential);
89 $editForm = $this->createForm('Wallabag\CoreBundle\Form\Type\SiteCredentialType', $siteCredential);
90 $editForm->handleRequest($request);
91
92 if ($editForm->isSubmitted() && $editForm->isValid()) {
93 $siteCredential->setUsername($this->get('wallabag_core.helper.crypto_proxy')->crypt($siteCredential->getUsername()));
94 $siteCredential->setPassword($this->get('wallabag_core.helper.crypto_proxy')->crypt($siteCredential->getPassword()));
95
96 $em = $this->getDoctrine()->getManager();
97 $em->persist($siteCredential);
98 $em->flush();
99
100 $this->get('session')->getFlashBag()->add(
101 'notice',
102 $this->get('translator')->trans('flashes.site_credential.notice.updated', ['%host%' => $siteCredential->getHost()])
103 );
104
105 return $this->redirectToRoute('site_credentials_index');
106 }
107
108 return $this->render('WallabagCoreBundle:SiteCredential:edit.html.twig', [
109 'credential' => $siteCredential,
110 'edit_form' => $editForm->createView(),
111 'delete_form' => $deleteForm->createView(),
112 ]);
113 }
114
115 /**
116 * Deletes a site credential entity.
117 *
118 * @Route("/{id}", name="site_credentials_delete")
119 * @Method("DELETE")
120 *
121 * @param Request $request
122 * @param SiteCredential $siteCredential
123 *
124 * @return \Symfony\Component\HttpFoundation\RedirectResponse
125 */
126 public function deleteAction(Request $request, SiteCredential $siteCredential)
127 {
128 $this->checkUserAction($siteCredential);
129
130 $form = $this->createDeleteForm($siteCredential);
131 $form->handleRequest($request);
132
133 if ($form->isSubmitted() && $form->isValid()) {
134 $this->get('session')->getFlashBag()->add(
135 'notice',
136 $this->get('translator')->trans('flashes.site_credential.notice.deleted', ['%host%' => $siteCredential->getHost()])
137 );
138
139 $em = $this->getDoctrine()->getManager();
140 $em->remove($siteCredential);
141 $em->flush();
142 }
143
144 return $this->redirectToRoute('site_credentials_index');
145 }
146
147 /**
148 * Creates a form to delete a site credential entity.
149 *
150 * @param SiteCredential $siteCredential The site credential entity
151 *
152 * @return \Symfony\Component\Form\Form The form
153 */
154 private function createDeleteForm(SiteCredential $siteCredential)
155 {
156 return $this->createFormBuilder()
157 ->setAction($this->generateUrl('site_credentials_delete', ['id' => $siteCredential->getId()]))
158 ->setMethod('DELETE')
159 ->getForm()
160 ;
161 }
162
163 /**
164 * Check if the logged user can manage the given site credential.
165 *
166 * @param SiteCredential $siteCredential The site credential entity
167 */
168 private function checkUserAction(SiteCredential $siteCredential)
169 {
170 if (null === $this->getUser() || $this->getUser()->getId() !== $siteCredential->getUser()->getId()) {
171 throw $this->createAccessDeniedException('You can not access this site credential.');
172 }
173 }
174}
diff --git a/src/Wallabag/CoreBundle/Controller/StaticController.php b/src/Wallabag/CoreBundle/Controller/StaticController.php
index 82714217..318af303 100644
--- a/src/Wallabag/CoreBundle/Controller/StaticController.php
+++ b/src/Wallabag/CoreBundle/Controller/StaticController.php
@@ -16,7 +16,9 @@ class StaticController extends Controller
16 16
17 return $this->render( 17 return $this->render(
18 '@WallabagCore/themes/common/Static/howto.html.twig', 18 '@WallabagCore/themes/common/Static/howto.html.twig',
19 ['addonsUrl' => $addonsUrl] 19 [
20 'addonsUrl' => $addonsUrl,
21 ]
20 ); 22 );
21 } 23 }
22 24
@@ -40,8 +42,7 @@ class StaticController extends Controller
40 public function quickstartAction() 42 public function quickstartAction()
41 { 43 {
42 return $this->render( 44 return $this->render(
43 '@WallabagCore/themes/common/Static/quickstart.html.twig', 45 '@WallabagCore/themes/common/Static/quickstart.html.twig'
44 []
45 ); 46 );
46 } 47 }
47} 48}
diff --git a/src/Wallabag/CoreBundle/Controller/TagController.php b/src/Wallabag/CoreBundle/Controller/TagController.php
index 8a093289..616c37f2 100644
--- a/src/Wallabag/CoreBundle/Controller/TagController.php
+++ b/src/Wallabag/CoreBundle/Controller/TagController.php
@@ -4,13 +4,13 @@ namespace Wallabag\CoreBundle\Controller;
4 4
5use Pagerfanta\Adapter\ArrayAdapter; 5use Pagerfanta\Adapter\ArrayAdapter;
6use Pagerfanta\Exception\OutOfRangeCurrentPageException; 6use Pagerfanta\Exception\OutOfRangeCurrentPageException;
7use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
7use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 8use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
8use Symfony\Bundle\FrameworkBundle\Controller\Controller; 9use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9use Symfony\Component\HttpFoundation\Request; 10use Symfony\Component\HttpFoundation\Request;
10use Wallabag\CoreBundle\Entity\Entry; 11use Wallabag\CoreBundle\Entity\Entry;
11use Wallabag\CoreBundle\Entity\Tag; 12use Wallabag\CoreBundle\Entity\Tag;
12use Wallabag\CoreBundle\Form\Type\NewTagType; 13use Wallabag\CoreBundle\Form\Type\NewTagType;
13use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
14 14
15class TagController extends Controller 15class TagController extends Controller
16{ 16{
@@ -28,7 +28,7 @@ class TagController extends Controller
28 $form->handleRequest($request); 28 $form->handleRequest($request);
29 29
30 if ($form->isSubmitted() && $form->isValid()) { 30 if ($form->isSubmitted() && $form->isValid()) {
31 $this->get('wallabag_core.content_proxy')->assignTagsToEntry( 31 $this->get('wallabag_core.tags_assigner')->assignTagsToEntry(
32 $entry, 32 $entry,
33 $form->get('label')->getData() 33 $form->get('label')->getData()
34 ); 34 );
@@ -65,12 +65,12 @@ class TagController extends Controller
65 $em->flush(); 65 $em->flush();
66 66
67 // remove orphan tag in case no entries are associated to it 67 // remove orphan tag in case no entries are associated to it
68 if (count($tag->getEntries()) === 0) { 68 if (0 === count($tag->getEntries())) {
69 $em->remove($tag); 69 $em->remove($tag);
70 $em->flush(); 70 $em->flush();
71 } 71 }
72 72
73 $redirectUrl = $this->get('wallabag_core.helper.redirect')->to($request->headers->get('referer')); 73 $redirectUrl = $this->get('wallabag_core.helper.redirect')->to($request->headers->get('referer'), '', true);
74 74
75 return $this->redirect($redirectUrl); 75 return $this->redirect($redirectUrl);
76 } 76 }
@@ -84,27 +84,11 @@ class TagController extends Controller
84 */ 84 */
85 public function showTagAction() 85 public function showTagAction()
86 { 86 {
87 $tags = $this->getDoctrine() 87 $tags = $this->get('wallabag_core.tag_repository')
88 ->getRepository('WallabagCoreBundle:Tag') 88 ->findAllFlatTagsWithNbEntries($this->getUser()->getId());
89 ->findAllTags($this->getUser()->getId());
90
91 $flatTags = [];
92
93 foreach ($tags as $tag) {
94 $nbEntries = $this->getDoctrine()
95 ->getRepository('WallabagCoreBundle:Entry')
96 ->countAllEntriesByUserIdAndTagId($this->getUser()->getId(), $tag->getId());
97
98 $flatTags[] = [
99 'id' => $tag->getId(),
100 'label' => $tag->getLabel(),
101 'slug' => $tag->getSlug(),
102 'nbEntries' => $nbEntries,
103 ];
104 }
105 89
106 return $this->render('WallabagCoreBundle:Tag:tags.html.twig', [ 90 return $this->render('WallabagCoreBundle:Tag:tags.html.twig', [
107 'tags' => $flatTags, 91 'tags' => $tags,
108 ]); 92 ]);
109 } 93 }
110 94
@@ -119,14 +103,14 @@ class TagController extends Controller
119 */ 103 */
120 public function showEntriesForTagAction(Tag $tag, $page, Request $request) 104 public function showEntriesForTagAction(Tag $tag, $page, Request $request)
121 { 105 {
122 $entriesByTag = $this->getDoctrine() 106 $entriesByTag = $this->get('wallabag_core.entry_repository')->findAllByTagId(
123 ->getRepository('WallabagCoreBundle:Entry') 107 $this->getUser()->getId(),
124 ->findAllByTagId($this->getUser()->getId(), $tag->getId()); 108 $tag->getId()
109 );
125 110
126 $pagerAdapter = new ArrayAdapter($entriesByTag); 111 $pagerAdapter = new ArrayAdapter($entriesByTag);
127 112
128 $entries = $this->get('wallabag_core.helper.prepare_pager_for_entries') 113 $entries = $this->get('wallabag_core.helper.prepare_pager_for_entries')->prepare($pagerAdapter);
129 ->prepare($pagerAdapter, $page);
130 114
131 try { 115 try {
132 $entries->setCurrentPage($page); 116 $entries->setCurrentPage($page);
@@ -143,7 +127,7 @@ class TagController extends Controller
143 'form' => null, 127 'form' => null,
144 'entries' => $entries, 128 'entries' => $entries,
145 'currentPage' => $page, 129 'currentPage' => $page,
146 'tag' => $tag->getSlug(), 130 'tag' => $tag,
147 ]); 131 ]);
148 } 132 }
149} 133}
diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php
index a723656e..3fe88e7f 100644
--- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php
+++ b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php
@@ -2,167 +2,31 @@
2 2
3namespace Wallabag\CoreBundle\DataFixtures\ORM; 3namespace Wallabag\CoreBundle\DataFixtures\ORM;
4 4
5use Craue\ConfigBundle\Entity\Setting;
5use Doctrine\Common\DataFixtures\AbstractFixture; 6use Doctrine\Common\DataFixtures\AbstractFixture;
6use Doctrine\Common\DataFixtures\OrderedFixtureInterface; 7use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
7use Doctrine\Common\Persistence\ObjectManager; 8use Doctrine\Common\Persistence\ObjectManager;
8use Craue\ConfigBundle\Entity\Setting; 9use Symfony\Component\DependencyInjection\ContainerAwareInterface;
10use Symfony\Component\DependencyInjection\ContainerInterface;
9 11
10class LoadSettingData extends AbstractFixture implements OrderedFixtureInterface 12class LoadSettingData extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
11{ 13{
12 /** 14 /**
15 * @var ContainerInterface
16 */
17 private $container;
18
19 public function setContainer(ContainerInterface $container = null)
20 {
21 $this->container = $container;
22 }
23
24 /**
13 * {@inheritdoc} 25 * {@inheritdoc}
14 */ 26 */
15 public function load(ObjectManager $manager) 27 public function load(ObjectManager $manager)
16 { 28 {
17 $settings = [ 29 foreach ($this->container->getParameter('wallabag_core.default_internal_settings') as $setting) {
18 [
19 'name' => 'share_public',
20 'value' => '1',
21 'section' => 'entry',
22 ],
23 [
24 'name' => 'carrot',
25 'value' => '1',
26 'section' => 'entry',
27 ],
28 [
29 'name' => 'share_diaspora',
30 'value' => '1',
31 'section' => 'entry',
32 ],
33 [
34 'name' => 'diaspora_url',
35 'value' => 'http://diasporapod.com',
36 'section' => 'entry',
37 ],
38 [
39 'name' => 'share_unmark',
40 'value' => '1',
41 'section' => 'entry',
42 ],
43 [
44 'name' => 'unmark_url',
45 'value' => 'https://unmark.it',
46 'section' => 'entry',
47 ],
48 [
49 'name' => 'share_shaarli',
50 'value' => '1',
51 'section' => 'entry',
52 ],
53 [
54 'name' => 'shaarli_url',
55 'value' => 'http://myshaarli.com',
56 'section' => 'entry',
57 ],
58 [
59 'name' => 'share_mail',
60 'value' => '1',
61 'section' => 'entry',
62 ],
63 [
64 'name' => 'share_twitter',
65 'value' => '1',
66 'section' => 'entry',
67 ],
68 [
69 'name' => 'export_epub',
70 'value' => '1',
71 'section' => 'export',
72 ],
73 [
74 'name' => 'export_mobi',
75 'value' => '1',
76 'section' => 'export',
77 ],
78 [
79 'name' => 'export_pdf',
80 'value' => '1',
81 'section' => 'export',
82 ],
83 [
84 'name' => 'export_csv',
85 'value' => '1',
86 'section' => 'export',
87 ],
88 [
89 'name' => 'export_json',
90 'value' => '1',
91 'section' => 'export',
92 ],
93 [
94 'name' => 'export_txt',
95 'value' => '1',
96 'section' => 'export',
97 ],
98 [
99 'name' => 'export_xml',
100 'value' => '1',
101 'section' => 'export',
102 ],
103 [
104 'name' => 'import_with_redis',
105 'value' => '0',
106 'section' => 'import',
107 ],
108 [
109 'name' => 'import_with_rabbitmq',
110 'value' => '0',
111 'section' => 'import',
112 ],
113 [
114 'name' => 'show_printlink',
115 'value' => '1',
116 'section' => 'entry',
117 ],
118 [
119 'name' => 'wallabag_support_url',
120 'value' => 'https://www.wallabag.org/pages/support.html',
121 'section' => 'misc',
122 ],
123 [
124 'name' => 'wallabag_url',
125 'value' => 'http://v2.wallabag.org',
126 'section' => 'misc',
127 ],
128 [
129 'name' => 'piwik_enabled',
130 'value' => '0',
131 'section' => 'analytics',
132 ],
133 [
134 'name' => 'piwik_host',
135 'value' => 'v2.wallabag.org',
136 'section' => 'analytics',
137 ],
138 [
139 'name' => 'piwik_site_id',
140 'value' => '1',
141 'section' => 'analytics',
142 ],
143 [
144 'name' => 'demo_mode_enabled',
145 'value' => '0',
146 'section' => 'misc',
147 ],
148 [
149 'name' => 'demo_mode_username',
150 'value' => 'wallabag',
151 'section' => 'misc',
152 ],
153 [
154 'name' => 'download_images_enabled',
155 'value' => '0',
156 'section' => 'misc',
157 ],
158 [
159 'name' => 'restricted_access',
160 'value' => '0',
161 'section' => 'entry',
162 ],
163 ];
164
165 foreach ($settings as $setting) {
166 $newSetting = new Setting(); 30 $newSetting = new Setting();
167 $newSetting->setName($setting['name']); 31 $newSetting->setName($setting['name']);
168 $newSetting->setValue($setting['value']); 32 $newSetting->setValue($setting['value']);
diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php
new file mode 100644
index 00000000..866f55a4
--- /dev/null
+++ b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php
@@ -0,0 +1,34 @@
1<?php
2
3namespace Wallabag\CoreBundle\DataFixtures\ORM;
4
5use Doctrine\Common\DataFixtures\AbstractFixture;
6use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
7use Doctrine\Common\Persistence\ObjectManager;
8use Wallabag\CoreBundle\Entity\SiteCredential;
9
10class LoadSiteCredentialData extends AbstractFixture implements OrderedFixtureInterface
11{
12 /**
13 * {@inheritdoc}
14 */
15 public function load(ObjectManager $manager)
16 {
17 $credential = new SiteCredential($this->getReference('admin-user'));
18 $credential->setHost('example.com');
19 $credential->setUsername('foo');
20 $credential->setPassword('bar');
21
22 $manager->persist($credential);
23
24 $manager->flush();
25 }
26
27 /**
28 * {@inheritdoc}
29 */
30 public function getOrder()
31 {
32 return 50;
33 }
34}
diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php
index 6de561e0..0ecfd18b 100644
--- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php
+++ b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php
@@ -19,7 +19,7 @@ class LoadTagData extends AbstractFixture implements OrderedFixtureInterface
19 19
20 $manager->persist($tag1); 20 $manager->persist($tag1);
21 21
22 $this->addReference('foo-tag', $tag1); 22 $this->addReference('foo-bar-tag', $tag1);
23 23
24 $tag2 = new Tag(); 24 $tag2 = new Tag();
25 $tag2->setLabel('bar'); 25 $tag2->setLabel('bar');
@@ -35,6 +35,13 @@ class LoadTagData extends AbstractFixture implements OrderedFixtureInterface
35 35
36 $this->addReference('baz-tag', $tag3); 36 $this->addReference('baz-tag', $tag3);
37 37
38 $tag4 = new Tag();
39 $tag4->setLabel('foo');
40
41 $manager->persist($tag4);
42
43 $this->addReference('foo-tag', $tag4);
44
38 $manager->flush(); 45 $manager->flush();
39 } 46 }
40 47
diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php
index 7efe6356..55abd63c 100644
--- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php
+++ b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php
@@ -36,6 +36,13 @@ class LoadTaggingRuleData extends AbstractFixture implements OrderedFixtureInter
36 36
37 $manager->persist($tr3); 37 $manager->persist($tr3);
38 38
39 $tr4 = new TaggingRule();
40 $tr4->setRule('content notmatches "basket"');
41 $tr4->setTags(['foot']);
42 $tr4->setConfig($this->getReference('admin-config'));
43
44 $manager->persist($tr4);
45
39 $manager->flush(); 46 $manager->flush();
40 } 47 }
41 48
diff --git a/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php b/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php
index 006a18c3..a9791f6b 100644
--- a/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php
+++ b/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php
@@ -41,12 +41,30 @@ class Configuration implements ConfigurationInterface
41 ->end() 41 ->end()
42 ->scalarNode('fetching_error_message') 42 ->scalarNode('fetching_error_message')
43 ->end() 43 ->end()
44 ->scalarNode('fetching_error_message_title')
45 ->end()
44 ->scalarNode('action_mark_as_read') 46 ->scalarNode('action_mark_as_read')
45 ->defaultValue(1) 47 ->defaultValue(1)
46 ->end() 48 ->end()
47 ->scalarNode('list_mode') 49 ->scalarNode('list_mode')
48 ->defaultValue(1) 50 ->defaultValue(1)
49 ->end() 51 ->end()
52 ->scalarNode('api_limit_mass_actions')
53 ->defaultValue(10)
54 ->end()
55 ->arrayNode('default_internal_settings')
56 ->prototype('array')
57 ->children()
58 ->scalarNode('name')->end()
59 ->scalarNode('value')->end()
60 ->enumNode('section')
61 ->values(['entry', 'misc', 'api', 'analytics', 'export', 'import'])
62 ->end()
63 ->end()
64 ->end()
65 ->end()
66 ->scalarNode('encryption_key_path')
67 ->end()
50 ->end() 68 ->end()
51 ; 69 ;
52 70
diff --git a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php
index aa9ee339..a3ef2b53 100644
--- a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php
+++ b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php
@@ -26,8 +26,12 @@ class WallabagCoreExtension extends Extension
26 $container->setParameter('wallabag_core.action_mark_as_read', $config['action_mark_as_read']); 26 $container->setParameter('wallabag_core.action_mark_as_read', $config['action_mark_as_read']);
27 $container->setParameter('wallabag_core.list_mode', $config['list_mode']); 27 $container->setParameter('wallabag_core.list_mode', $config['list_mode']);
28 $container->setParameter('wallabag_core.fetching_error_message', $config['fetching_error_message']); 28 $container->setParameter('wallabag_core.fetching_error_message', $config['fetching_error_message']);
29 $container->setParameter('wallabag_core.fetching_error_message_title', $config['fetching_error_message_title']);
30 $container->setParameter('wallabag_core.api_limit_mass_actions', $config['api_limit_mass_actions']);
31 $container->setParameter('wallabag_core.default_internal_settings', $config['default_internal_settings']);
32 $container->setParameter('wallabag_core.site_credentials.encryption_key_path', $config['encryption_key_path']);
29 33
30 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 34 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
31 $loader->load('services.yml'); 35 $loader->load('services.yml');
32 $loader->load('parameters.yml'); 36 $loader->load('parameters.yml');
33 } 37 }
diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php
index 7276b437..cfb8db75 100644
--- a/src/Wallabag/CoreBundle/Entity/Entry.php
+++ b/src/Wallabag/CoreBundle/Entity/Entry.php
@@ -5,14 +5,15 @@ namespace Wallabag\CoreBundle\Entity;
5use Doctrine\Common\Collections\ArrayCollection; 5use Doctrine\Common\Collections\ArrayCollection;
6use Doctrine\ORM\Mapping as ORM; 6use Doctrine\ORM\Mapping as ORM;
7use Hateoas\Configuration\Annotation as Hateoas; 7use Hateoas\Configuration\Annotation as Hateoas;
8use JMS\Serializer\Annotation\Groups;
9use JMS\Serializer\Annotation\XmlRoot;
10use JMS\Serializer\Annotation\Exclude; 8use JMS\Serializer\Annotation\Exclude;
11use JMS\Serializer\Annotation\VirtualProperty; 9use JMS\Serializer\Annotation\Groups;
12use JMS\Serializer\Annotation\SerializedName; 10use JMS\Serializer\Annotation\SerializedName;
11use JMS\Serializer\Annotation\VirtualProperty;
12use JMS\Serializer\Annotation\XmlRoot;
13use Symfony\Component\Validator\Constraints as Assert; 13use Symfony\Component\Validator\Constraints as Assert;
14use Wallabag\UserBundle\Entity\User;
15use Wallabag\AnnotationBundle\Entity\Annotation; 14use Wallabag\AnnotationBundle\Entity\Annotation;
15use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
16use Wallabag\UserBundle\Entity\User;
16 17
17/** 18/**
18 * Entry. 19 * Entry.
@@ -32,6 +33,8 @@ use Wallabag\AnnotationBundle\Entity\Annotation;
32 */ 33 */
33class Entry 34class Entry
34{ 35{
36 use EntityTimestampsTrait;
37
35 /** @Serializer\XmlAttribute */ 38 /** @Serializer\XmlAttribute */
36 /** 39 /**
37 * @var int 40 * @var int
@@ -122,6 +125,33 @@ class Entry
122 private $updatedAt; 125 private $updatedAt;
123 126
124 /** 127 /**
128 * @var \DateTime
129 *
130 * @ORM\Column(name="published_at", type="datetime", nullable=true)
131 *
132 * @Groups({"entries_for_user", "export_all"})
133 */
134 private $publishedAt;
135
136 /**
137 * @var array
138 *
139 * @ORM\Column(name="published_by", type="array", nullable=true)
140 *
141 * @Groups({"entries_for_user", "export_all"})
142 */
143 private $publishedBy;
144
145 /**
146 * @var \DateTime
147 *
148 * @ORM\Column(name="starred_at", type="datetime", nullable=true)
149 *
150 * @Groups({"entries_for_user", "export_all"})
151 */
152 private $starredAt = null;
153
154 /**
125 * @ORM\OneToMany(targetEntity="Wallabag\AnnotationBundle\Entity\Annotation", mappedBy="entry", cascade={"persist", "remove"}) 155 * @ORM\OneToMany(targetEntity="Wallabag\AnnotationBundle\Entity\Annotation", mappedBy="entry", cascade={"persist", "remove"})
126 * @ORM\JoinTable 156 * @ORM\JoinTable
127 * 157 *
@@ -150,11 +180,11 @@ class Entry
150 /** 180 /**
151 * @var int 181 * @var int
152 * 182 *
153 * @ORM\Column(name="reading_time", type="integer", nullable=true) 183 * @ORM\Column(name="reading_time", type="integer", nullable=false)
154 * 184 *
155 * @Groups({"entries_for_user", "export_all"}) 185 * @Groups({"entries_for_user", "export_all"})
156 */ 186 */
157 private $readingTime; 187 private $readingTime = 0;
158 188
159 /** 189 /**
160 * @var string 190 * @var string
@@ -175,22 +205,22 @@ class Entry
175 private $previewPicture; 205 private $previewPicture;
176 206
177 /** 207 /**
178 * @var bool 208 * @var string
179 * 209 *
180 * @ORM\Column(name="is_public", type="boolean", nullable=true, options={"default" = false}) 210 * @ORM\Column(name="http_status", type="string", length=3, nullable=true)
181 * 211 *
182 * @Groups({"export_all"}) 212 * @Groups({"entries_for_user", "export_all"})
183 */ 213 */
184 private $isPublic; 214 private $httpStatus;
185 215
186 /** 216 /**
187 * @var string 217 * @var array
188 * 218 *
189 * @ORM\Column(name="http_status", type="string", length=3, nullable=true) 219 * @ORM\Column(name="headers", type="array", nullable=true)
190 * 220 *
191 * @Groups({"entries_for_user", "export_all"}) 221 * @Groups({"entries_for_user", "export_all"})
192 */ 222 */
193 private $httpStatus; 223 private $headers;
194 224
195 /** 225 /**
196 * @Exclude 226 * @Exclude
@@ -455,16 +485,41 @@ class Entry
455 } 485 }
456 486
457 /** 487 /**
458 * @ORM\PrePersist 488 * @return \DateTime|null
459 * @ORM\PreUpdate
460 */ 489 */
461 public function timestamps() 490 public function getStarredAt()
462 { 491 {
463 if (is_null($this->createdAt)) { 492 return $this->starredAt;
464 $this->createdAt = new \DateTime(); 493 }
494
495 /**
496 * @param \DateTime|null $starredAt
497 *
498 * @return Entry
499 */
500 public function setStarredAt($starredAt = null)
501 {
502 $this->starredAt = $starredAt;
503
504 return $this;
505 }
506
507 /**
508 * update isStarred and starred_at fields.
509 *
510 * @param bool $isStarred
511 *
512 * @return Entry
513 */
514 public function updateStar($isStarred = false)
515 {
516 $this->setStarred($isStarred);
517 $this->setStarredAt(null);
518 if ($this->isStarred()) {
519 $this->setStarredAt(new \DateTime());
465 } 520 }
466 521
467 $this->updatedAt = new \DateTime(); 522 return $this;
468 } 523 }
469 524
470 /** 525 /**
@@ -532,23 +587,7 @@ class Entry
532 } 587 }
533 588
534 /** 589 /**
535 * @return bool 590 * @return ArrayCollection
536 */
537 public function isPublic()
538 {
539 return $this->isPublic;
540 }
541
542 /**
543 * @param bool $isPublic
544 */
545 public function setIsPublic($isPublic)
546 {
547 $this->isPublic = $isPublic;
548 }
549
550 /**
551 * @return ArrayCollection<Tag>
552 */ 591 */
553 public function getTags() 592 public function getTags()
554 { 593 {
@@ -591,6 +630,11 @@ class Entry
591 $tag->addEntry($this); 630 $tag->addEntry($this);
592 } 631 }
593 632
633 /**
634 * Remove the given tag from the entry (if the tag is associated).
635 *
636 * @param Tag $tag
637 */
594 public function removeTag(Tag $tag) 638 public function removeTag(Tag $tag)
595 { 639 {
596 if (!$this->tags->contains($tag)) { 640 if (!$this->tags->contains($tag)) {
@@ -602,6 +646,17 @@ class Entry
602 } 646 }
603 647
604 /** 648 /**
649 * Remove all assigned tags from the entry.
650 */
651 public function removeAllTags()
652 {
653 foreach ($this->tags as $tag) {
654 $this->tags->removeElement($tag);
655 $tag->removeEntry($this);
656 }
657 }
658
659 /**
605 * Set previewPicture. 660 * Set previewPicture.
606 * 661 *
607 * @param string $previewPicture 662 * @param string $previewPicture
@@ -683,7 +738,22 @@ class Entry
683 } 738 }
684 739
685 /** 740 /**
686 * @return int 741 * Used in the entries filter so it's more explicit for the end user than the uid.
742 * Also used in the API.
743 *
744 * @VirtualProperty
745 * @SerializedName("is_public")
746 * @Groups({"entries_for_user"})
747 *
748 * @return bool
749 */
750 public function isPublic()
751 {
752 return null !== $this->uid;
753 }
754
755 /**
756 * @return string
687 */ 757 */
688 public function getHttpStatus() 758 public function getHttpStatus()
689 { 759 {
@@ -691,7 +761,7 @@ class Entry
691 } 761 }
692 762
693 /** 763 /**
694 * @param int $httpStatus 764 * @param string $httpStatus
695 * 765 *
696 * @return Entry 766 * @return Entry
697 */ 767 */
@@ -701,4 +771,64 @@ class Entry
701 771
702 return $this; 772 return $this;
703 } 773 }
774
775 /**
776 * @return \Datetime
777 */
778 public function getPublishedAt()
779 {
780 return $this->publishedAt;
781 }
782
783 /**
784 * @param \Datetime $publishedAt
785 *
786 * @return Entry
787 */
788 public function setPublishedAt(\Datetime $publishedAt)
789 {
790 $this->publishedAt = $publishedAt;
791
792 return $this;
793 }
794
795 /**
796 * @return array
797 */
798 public function getPublishedBy()
799 {
800 return $this->publishedBy;
801 }
802
803 /**
804 * @param array $publishedBy
805 *
806 * @return Entry
807 */
808 public function setPublishedBy($publishedBy)
809 {
810 $this->publishedBy = $publishedBy;
811
812 return $this;
813 }
814
815 /**
816 * @return array
817 */
818 public function getHeaders()
819 {
820 return $this->headers;
821 }
822
823 /**
824 * @param array $headers
825 *
826 * @return Entry
827 */
828 public function setHeaders($headers)
829 {
830 $this->headers = $headers;
831
832 return $this;
833 }
704} 834}
diff --git a/src/Wallabag/CoreBundle/Entity/SiteCredential.php b/src/Wallabag/CoreBundle/Entity/SiteCredential.php
new file mode 100644
index 00000000..ac714359
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Entity/SiteCredential.php
@@ -0,0 +1,188 @@
1<?php
2
3namespace Wallabag\CoreBundle\Entity;
4
5use Doctrine\ORM\Mapping as ORM;
6use Symfony\Component\Validator\Constraints as Assert;
7use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
8use Wallabag\UserBundle\Entity\User;
9
10/**
11 * SiteCredential.
12 *
13 * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\SiteCredentialRepository")
14 * @ORM\Table(name="`site_credential`")
15 * @ORM\HasLifecycleCallbacks()
16 */
17class SiteCredential
18{
19 use EntityTimestampsTrait;
20
21 /**
22 * @var int
23 *
24 * @ORM\Column(name="id", type="integer")
25 * @ORM\Id
26 * @ORM\GeneratedValue(strategy="AUTO")
27 */
28 private $id;
29
30 /**
31 * @var string
32 *
33 * @Assert\NotBlank()
34 * @Assert\Length(max=255)
35 * @ORM\Column(name="host", type="string", length=255)
36 */
37 private $host;
38
39 /**
40 * @var string
41 *
42 * @Assert\NotBlank()
43 * @ORM\Column(name="username", type="text")
44 */
45 private $username;
46
47 /**
48 * @var string
49 *
50 * @Assert\NotBlank()
51 * @ORM\Column(name="password", type="text")
52 */
53 private $password;
54
55 /**
56 * @var \DateTime
57 *
58 * @ORM\Column(name="createdAt", type="datetime")
59 */
60 private $createdAt;
61
62 /**
63 * @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User", inversedBy="siteCredentials")
64 */
65 private $user;
66
67 /*
68 * @param User $user
69 */
70 public function __construct(User $user)
71 {
72 $this->user = $user;
73 }
74
75 /**
76 * Get id.
77 *
78 * @return int
79 */
80 public function getId()
81 {
82 return $this->id;
83 }
84
85 /**
86 * Set host.
87 *
88 * @param string $host
89 *
90 * @return SiteCredential
91 */
92 public function setHost($host)
93 {
94 $this->host = $host;
95
96 return $this;
97 }
98
99 /**
100 * Get host.
101 *
102 * @return string
103 */
104 public function getHost()
105 {
106 return $this->host;
107 }
108
109 /**
110 * Set username.
111 *
112 * @param string $username
113 *
114 * @return SiteCredential
115 */
116 public function setUsername($username)
117 {
118 $this->username = $username;
119
120 return $this;
121 }
122
123 /**
124 * Get username.
125 *
126 * @return string
127 */
128 public function getUsername()
129 {
130 return $this->username;
131 }
132
133 /**
134 * Set password.
135 *
136 * @param string $password
137 *
138 * @return SiteCredential
139 */
140 public function setPassword($password)
141 {
142 $this->password = $password;
143
144 return $this;
145 }
146
147 /**
148 * Get password.
149 *
150 * @return string
151 */
152 public function getPassword()
153 {
154 return $this->password;
155 }
156
157 /**
158 * Set createdAt.
159 *
160 * @param \DateTime $createdAt
161 *
162 * @return SiteCredential
163 */
164 public function setCreatedAt($createdAt)
165 {
166 $this->createdAt = $createdAt;
167
168 return $this;
169 }
170
171 /**
172 * Get createdAt.
173 *
174 * @return \DateTime
175 */
176 public function getCreatedAt()
177 {
178 return $this->createdAt;
179 }
180
181 /**
182 * @return User
183 */
184 public function getUser()
185 {
186 return $this->user;
187 }
188}
diff --git a/src/Wallabag/CoreBundle/Entity/Tag.php b/src/Wallabag/CoreBundle/Entity/Tag.php
index 4b480ff1..a6dc8c50 100644
--- a/src/Wallabag/CoreBundle/Entity/Tag.php
+++ b/src/Wallabag/CoreBundle/Entity/Tag.php
@@ -4,9 +4,9 @@ namespace Wallabag\CoreBundle\Entity;
4 4
5use Doctrine\Common\Collections\ArrayCollection; 5use Doctrine\Common\Collections\ArrayCollection;
6use Doctrine\ORM\Mapping as ORM; 6use Doctrine\ORM\Mapping as ORM;
7use Gedmo\Mapping\Annotation as Gedmo;
7use JMS\Serializer\Annotation\ExclusionPolicy; 8use JMS\Serializer\Annotation\ExclusionPolicy;
8use JMS\Serializer\Annotation\Expose; 9use JMS\Serializer\Annotation\Expose;
9use Gedmo\Mapping\Annotation as Gedmo;
10use JMS\Serializer\Annotation\XmlRoot; 10use JMS\Serializer\Annotation\XmlRoot;
11 11
12/** 12/**
@@ -78,7 +78,7 @@ class Tag
78 */ 78 */
79 public function setLabel($label) 79 public function setLabel($label)
80 { 80 {
81 $this->label = $label; 81 $this->label = mb_convert_case($label, MB_CASE_LOWER);
82 82
83 return $this; 83 return $this;
84 } 84 }
diff --git a/src/Wallabag/CoreBundle/Entity/TaggingRule.php b/src/Wallabag/CoreBundle/Entity/TaggingRule.php
index 72651b19..84e11e26 100644
--- a/src/Wallabag/CoreBundle/Entity/TaggingRule.php
+++ b/src/Wallabag/CoreBundle/Entity/TaggingRule.php
@@ -31,7 +31,7 @@ class TaggingRule
31 * @Assert\Length(max=255) 31 * @Assert\Length(max=255)
32 * @RulerZAssert\ValidRule( 32 * @RulerZAssert\ValidRule(
33 * allowed_variables={"title", "url", "isArchived", "isStared", "content", "language", "mimetype", "readingTime", "domainName"}, 33 * allowed_variables={"title", "url", "isArchived", "isStared", "content", "language", "mimetype", "readingTime", "domainName"},
34 * allowed_operators={">", "<", ">=", "<=", "=", "is", "!=", "and", "not", "or", "matches"} 34 * allowed_operators={">", "<", ">=", "<=", "=", "is", "!=", "and", "not", "or", "matches", "notmatches"}
35 * ) 35 * )
36 * @ORM\Column(name="rule", type="string", nullable=false) 36 * @ORM\Column(name="rule", type="string", nullable=false)
37 */ 37 */
@@ -87,7 +87,7 @@ class TaggingRule
87 /** 87 /**
88 * Set tags. 88 * Set tags.
89 * 89 *
90 * @param array<string> $tags 90 * @param array <string> $tags
91 * 91 *
92 * @return TaggingRule 92 * @return TaggingRule
93 */ 93 */
diff --git a/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php b/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php
index 4ebe837b..1dd0a1a4 100644
--- a/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php
+++ b/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php
@@ -2,13 +2,13 @@
2 2
3namespace Wallabag\CoreBundle\Event\Subscriber; 3namespace Wallabag\CoreBundle\Event\Subscriber;
4 4
5use Symfony\Component\EventDispatcher\EventSubscriberInterface; 5use Doctrine\ORM\EntityManager;
6use Psr\Log\LoggerInterface; 6use Psr\Log\LoggerInterface;
7use Wallabag\CoreBundle\Helper\DownloadImages; 7use Symfony\Component\EventDispatcher\EventSubscriberInterface;
8use Wallabag\CoreBundle\Entity\Entry; 8use Wallabag\CoreBundle\Entity\Entry;
9use Wallabag\CoreBundle\Event\EntrySavedEvent;
10use Wallabag\CoreBundle\Event\EntryDeletedEvent; 9use Wallabag\CoreBundle\Event\EntryDeletedEvent;
11use Doctrine\ORM\EntityManager; 10use Wallabag\CoreBundle\Event\EntrySavedEvent;
11use Wallabag\CoreBundle\Helper\DownloadImages;
12 12
13class DownloadImagesSubscriber implements EventSubscriberInterface 13class DownloadImagesSubscriber implements EventSubscriberInterface
14{ 14{
diff --git a/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php b/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php
index 3b4c4cf9..9c1d8a1d 100644
--- a/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php
+++ b/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php
@@ -2,10 +2,10 @@
2 2
3namespace Wallabag\CoreBundle\Event\Subscriber; 3namespace Wallabag\CoreBundle\Event\Subscriber;
4 4
5use Doctrine\Bundle\DoctrineBundle\Registry;
5use Doctrine\Common\EventSubscriber; 6use Doctrine\Common\EventSubscriber;
6use Doctrine\ORM\Event\LifecycleEventArgs; 7use Doctrine\ORM\Event\LifecycleEventArgs;
7use Wallabag\CoreBundle\Entity\Entry; 8use Wallabag\CoreBundle\Entity\Entry;
8use Doctrine\Bundle\DoctrineBundle\Registry;
9 9
10/** 10/**
11 * SQLite doesn't care about cascading remove, so we need to manually remove associated stuf for an Entry. 11 * SQLite doesn't care about cascading remove, so we need to manually remove associated stuf for an Entry.
@@ -45,9 +45,8 @@ class SQLiteCascadeDeleteSubscriber implements EventSubscriber
45 public function preRemove(LifecycleEventArgs $args) 45 public function preRemove(LifecycleEventArgs $args)
46 { 46 {
47 $entity = $args->getEntity(); 47 $entity = $args->getEntity();
48 48 if (!$this->doctrine->getConnection()->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform
49 if (!$this->doctrine->getConnection()->getDriver() instanceof \Doctrine\DBAL\Driver\PDOSqlite\Driver || 49 || !$entity instanceof Entry) {
50 !$entity instanceof Entry) {
51 return; 50 return;
52 } 51 }
53 52
diff --git a/src/Wallabag/CoreBundle/Event/Subscriber/TablePrefixSubscriber.php b/src/Wallabag/CoreBundle/Event/Subscriber/TablePrefixSubscriber.php
index 711c3bf8..fb8f225f 100644
--- a/src/Wallabag/CoreBundle/Event/Subscriber/TablePrefixSubscriber.php
+++ b/src/Wallabag/CoreBundle/Event/Subscriber/TablePrefixSubscriber.php
@@ -39,12 +39,12 @@ class TablePrefixSubscriber implements EventSubscriber
39 return; 39 return;
40 } 40 }
41 41
42 $classMetadata->setPrimaryTable(['name' => $this->prefix.$classMetadata->getTableName()]); 42 $classMetadata->setPrimaryTable(['name' => $this->prefix . $classMetadata->getTableName()]);
43 43
44 foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) { 44 foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
45 if ($mapping['type'] === ClassMetadataInfo::MANY_TO_MANY && isset($classMetadata->associationMappings[$fieldName]['joinTable']['name'])) { 45 if (ClassMetadataInfo::MANY_TO_MANY === $mapping['type'] && isset($classMetadata->associationMappings[$fieldName]['joinTable']['name'])) {
46 $mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name']; 46 $mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
47 $classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix.$mappedTableName; 47 $classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
48 } 48 }
49 } 49 }
50 } 50 }
diff --git a/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php b/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php
index cb4bee83..57dbc95e 100644
--- a/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php
+++ b/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php
@@ -48,7 +48,7 @@ class StringToListTransformer implements DataTransformerInterface
48 */ 48 */
49 public function reverseTransform($string) 49 public function reverseTransform($string)
50 { 50 {
51 if ($string === null) { 51 if (null === $string) {
52 return; 52 return;
53 } 53 }
54 54
diff --git a/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php b/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php
index c3715646..1627cc44 100644
--- a/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php
+++ b/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php
@@ -3,7 +3,6 @@
3namespace Wallabag\CoreBundle\Form\Type; 3namespace Wallabag\CoreBundle\Form\Type;
4 4
5use Symfony\Component\Form\AbstractType; 5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
7use Symfony\Component\Form\Extension\Core\Type\SubmitType; 6use Symfony\Component\Form\Extension\Core\Type\SubmitType;
8use Symfony\Component\Form\Extension\Core\Type\TextType; 7use Symfony\Component\Form\Extension\Core\Type\TextType;
9use Symfony\Component\Form\FormBuilderInterface; 8use Symfony\Component\Form\FormBuilderInterface;
@@ -18,11 +17,6 @@ class EditEntryType extends AbstractType
18 'required' => true, 17 'required' => true,
19 'label' => 'entry.edit.title_label', 18 'label' => 'entry.edit.title_label',
20 ]) 19 ])
21 ->add('is_public', CheckboxType::class, [
22 'required' => false,
23 'label' => 'entry.edit.is_public_label',
24 'property_path' => 'isPublic',
25 ])
26 ->add('url', TextType::class, [ 20 ->add('url', TextType::class, [
27 'disabled' => true, 21 'disabled' => true,
28 'required' => false, 22 'required' => false,
diff --git a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php
index 556578d1..6f8c9e27 100644
--- a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php
+++ b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php
@@ -4,12 +4,12 @@ namespace Wallabag\CoreBundle\Form\Type;
4 4
5use Doctrine\ORM\EntityRepository; 5use Doctrine\ORM\EntityRepository;
6use Lexik\Bundle\FormFilterBundle\Filter\FilterOperands; 6use Lexik\Bundle\FormFilterBundle\Filter\FilterOperands;
7use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface;
8use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\NumberRangeFilterType;
9use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\DateRangeFilterType;
10use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\TextFilterType;
11use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\CheckboxFilterType; 7use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\CheckboxFilterType;
12use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\ChoiceFilterType; 8use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\ChoiceFilterType;
9use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\DateRangeFilterType;
10use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\NumberRangeFilterType;
11use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\TextFilterType;
12use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface;
13use Symfony\Component\Form\AbstractType; 13use Symfony\Component\Form\AbstractType;
14use Symfony\Component\Form\FormBuilderInterface; 14use Symfony\Component\Form\FormBuilderInterface;
15use Symfony\Component\HttpFoundation\Response; 15use Symfony\Component\HttpFoundation\Response;
@@ -99,7 +99,7 @@ class EntryFilterType extends AbstractType
99 if (strlen($value) <= 2 || empty($value)) { 99 if (strlen($value) <= 2 || empty($value)) {
100 return; 100 return;
101 } 101 }
102 $expression = $filterQuery->getExpr()->like($field, $filterQuery->getExpr()->lower($filterQuery->getExpr()->literal('%'.$value.'%'))); 102 $expression = $filterQuery->getExpr()->like($field, $filterQuery->getExpr()->lower($filterQuery->getExpr()->literal('%' . $value . '%')));
103 103
104 return $filterQuery->createCondition($expression); 104 return $filterQuery->createCondition($expression);
105 }, 105 },
@@ -113,8 +113,8 @@ class EntryFilterType extends AbstractType
113 } 113 }
114 114
115 $paramName = sprintf('%s', str_replace('.', '_', $field)); 115 $paramName = sprintf('%s', str_replace('.', '_', $field));
116 $expression = $filterQuery->getExpr()->eq($field, ':'.$paramName); 116 $expression = $filterQuery->getExpr()->eq($field, ':' . $paramName);
117 $parameters = array($paramName => $value); 117 $parameters = [$paramName => $value];
118 118
119 return $filterQuery->createCondition($expression, $parameters); 119 return $filterQuery->createCondition($expression, $parameters);
120 }, 120 },
@@ -150,6 +150,20 @@ class EntryFilterType extends AbstractType
150 }, 150 },
151 'label' => 'entry.filters.preview_picture_label', 151 'label' => 'entry.filters.preview_picture_label',
152 ]) 152 ])
153 ->add('isPublic', CheckboxFilterType::class, [
154 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
155 if (false === $values['value']) {
156 return;
157 }
158
159 // is_public isn't a real field
160 // we should use the "uid" field to determine if the entry has been made public
161 $expression = $filterQuery->getExpr()->isNotNull($values['alias'] . '.uid');
162
163 return $filterQuery->createCondition($expression);
164 },
165 'label' => 'entry.filters.is_public_label',
166 ])
153 ->add('language', ChoiceFilterType::class, [ 167 ->add('language', ChoiceFilterType::class, [
154 'choices' => array_flip($this->repository->findDistinctLanguageByUser($this->user->getId())), 168 'choices' => array_flip($this->repository->findDistinctLanguageByUser($this->user->getId())),
155 'label' => 'entry.filters.language_label', 169 'label' => 'entry.filters.language_label',
diff --git a/src/Wallabag/CoreBundle/Form/Type/SiteCredentialType.php b/src/Wallabag/CoreBundle/Form/Type/SiteCredentialType.php
new file mode 100644
index 00000000..fd409ad2
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Form/Type/SiteCredentialType.php
@@ -0,0 +1,44 @@
1<?php
2
3namespace Wallabag\CoreBundle\Form\Type;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\Extension\Core\Type\PasswordType;
7use Symfony\Component\Form\Extension\Core\Type\SubmitType;
8use Symfony\Component\Form\Extension\Core\Type\TextType;
9use Symfony\Component\Form\FormBuilderInterface;
10use Symfony\Component\OptionsResolver\OptionsResolver;
11
12class SiteCredentialType extends AbstractType
13{
14 public function buildForm(FormBuilderInterface $builder, array $options)
15 {
16 $builder
17 ->add('host', TextType::class, [
18 'label' => 'site_credential.form.host_label',
19 ])
20 ->add('username', TextType::class, [
21 'label' => 'site_credential.form.username_label',
22 'data' => '',
23 ])
24 ->add('password', PasswordType::class, [
25 'label' => 'site_credential.form.password_label',
26 ])
27 ->add('save', SubmitType::class, [
28 'label' => 'config.form.save',
29 ])
30 ;
31 }
32
33 public function configureOptions(OptionsResolver $resolver)
34 {
35 $resolver->setDefaults([
36 'data_class' => 'Wallabag\CoreBundle\Entity\SiteCredential',
37 ]);
38 }
39
40 public function getBlockPrefix()
41 {
42 return 'site_credential';
43 }
44}
diff --git a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php
index 6d4129e8..2c85da62 100644
--- a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php
+++ b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php
@@ -5,48 +5,74 @@ namespace Wallabag\CoreBundle\GuzzleSiteAuthenticator;
5use BD\GuzzleSiteAuthenticator\SiteConfig\SiteConfig; 5use BD\GuzzleSiteAuthenticator\SiteConfig\SiteConfig;
6use BD\GuzzleSiteAuthenticator\SiteConfig\SiteConfigBuilder; 6use BD\GuzzleSiteAuthenticator\SiteConfig\SiteConfigBuilder;
7use Graby\SiteConfig\ConfigBuilder; 7use Graby\SiteConfig\ConfigBuilder;
8use OutOfRangeException; 8use Psr\Log\LoggerInterface;
9use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
10use Wallabag\CoreBundle\Repository\SiteCredentialRepository;
11use Wallabag\UserBundle\Entity\User;
9 12
10class GrabySiteConfigBuilder implements SiteConfigBuilder 13class GrabySiteConfigBuilder implements SiteConfigBuilder
11{ 14{
12 /** 15 /**
13 * @var \Graby\SiteConfig\ConfigBuilder 16 * @var ConfigBuilder
14 */ 17 */
15 private $grabyConfigBuilder; 18 private $grabyConfigBuilder;
19
20 /**
21 * @var SiteCredentialRepository
22 */
23 private $credentialRepository;
24
16 /** 25 /**
17 * @var array 26 * @var LoggerInterface
18 */ 27 */
19 private $credentials; 28 private $logger;
29
30 /**
31 * @var User|null
32 */
33 private $currentUser;
20 34
21 /** 35 /**
22 * GrabySiteConfigBuilder constructor. 36 * GrabySiteConfigBuilder constructor.
23 * 37 *
24 * @param \Graby\SiteConfig\ConfigBuilder $grabyConfigBuilder 38 * @param ConfigBuilder $grabyConfigBuilder
25 * @param array $credentials 39 * @param TokenStorage $token
40 * @param SiteCredentialRepository $credentialRepository
41 * @param LoggerInterface $logger
26 */ 42 */
27 public function __construct(ConfigBuilder $grabyConfigBuilder, array $credentials = []) 43 public function __construct(ConfigBuilder $grabyConfigBuilder, TokenStorage $token, SiteCredentialRepository $credentialRepository, LoggerInterface $logger)
28 { 44 {
29 $this->grabyConfigBuilder = $grabyConfigBuilder; 45 $this->grabyConfigBuilder = $grabyConfigBuilder;
30 $this->credentials = $credentials; 46 $this->credentialRepository = $credentialRepository;
47 $this->logger = $logger;
48
49 if ($token->getToken()) {
50 $this->currentUser = $token->getToken()->getUser();
51 }
31 } 52 }
32 53
33 /** 54 /**
34 * Builds the SiteConfig for a host. 55 * {@inheritdoc}
35 *
36 * @param string $host The "www." prefix is ignored
37 *
38 * @return SiteConfig
39 *
40 * @throws OutOfRangeException If there is no config for $host
41 */ 56 */
42 public function buildForHost($host) 57 public function buildForHost($host)
43 { 58 {
44 // required by credentials below 59 // required by credentials below
45 $host = strtolower($host); 60 $host = strtolower($host);
46 if (substr($host, 0, 4) == 'www.') { 61 if ('www.' === substr($host, 0, 4)) {
47 $host = substr($host, 4); 62 $host = substr($host, 4);
48 } 63 }
49 64
65 $credentials = null;
66 if ($this->currentUser) {
67 $credentials = $this->credentialRepository->findOneByHostAndUser($host, $this->currentUser->getId());
68 }
69
70 if (null === $credentials) {
71 $this->logger->debug('Auth: no credentials available for host.', ['host' => $host]);
72
73 return false;
74 }
75
50 $config = $this->grabyConfigBuilder->buildForHost($host); 76 $config = $this->grabyConfigBuilder->buildForHost($host);
51 $parameters = [ 77 $parameters = [
52 'host' => $host, 78 'host' => $host,
@@ -54,15 +80,47 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder
54 'loginUri' => $config->login_uri ?: null, 80 'loginUri' => $config->login_uri ?: null,
55 'usernameField' => $config->login_username_field ?: null, 81 'usernameField' => $config->login_username_field ?: null,
56 'passwordField' => $config->login_password_field ?: null, 82 'passwordField' => $config->login_password_field ?: null,
57 'extraFields' => is_array($config->login_extra_fields) ? $config->login_extra_fields : [], 83 'extraFields' => $this->processExtraFields($config->login_extra_fields),
58 'notLoggedInXpath' => $config->not_logged_in_xpath ?: null, 84 'notLoggedInXpath' => $config->not_logged_in_xpath ?: null,
85 'username' => $credentials['username'],
86 'password' => $credentials['password'],
59 ]; 87 ];
60 88
61 if (isset($this->credentials[$host])) { 89 $config = new SiteConfig($parameters);
62 $parameters['username'] = $this->credentials[$host]['username']; 90
63 $parameters['password'] = $this->credentials[$host]['password']; 91 // do not leak usernames and passwords in log
92 $parameters['username'] = '**masked**';
93 $parameters['password'] = '**masked**';
94
95 $this->logger->debug('Auth: add parameters.', ['host' => $host, 'parameters' => $parameters]);
96
97 return $config;
98 }
99
100 /**
101 * Processes login_extra_fields config, transforming an '=' separated array of strings
102 * into a key/value array.
103 *
104 * @param array|mixed $extraFieldsStrings
105 *
106 * @return array
107 */
108 protected function processExtraFields($extraFieldsStrings)
109 {
110 if (!is_array($extraFieldsStrings)) {
111 return [];
112 }
113
114 $extraFields = [];
115 foreach ($extraFieldsStrings as $extraField) {
116 if (false === strpos($extraField, '=')) {
117 continue;
118 }
119
120 list($fieldName, $fieldValue) = explode('=', $extraField, 2);
121 $extraFields[$fieldName] = $fieldValue;
64 } 122 }
65 123
66 return new SiteConfig($parameters); 124 return $extraFields;
67 } 125 }
68} 126}
diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php
index f222dd88..854acb6a 100644
--- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php
+++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php
@@ -4,11 +4,12 @@ namespace Wallabag\CoreBundle\Helper;
4 4
5use Graby\Graby; 5use Graby\Graby;
6use Psr\Log\LoggerInterface; 6use Psr\Log\LoggerInterface;
7use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
8use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint;
9use Symfony\Component\Validator\Constraints\Url as UrlConstraint;
10use Symfony\Component\Validator\Validator\ValidatorInterface;
7use Wallabag\CoreBundle\Entity\Entry; 11use Wallabag\CoreBundle\Entity\Entry;
8use Wallabag\CoreBundle\Entity\Tag;
9use Wallabag\CoreBundle\Tools\Utils; 12use Wallabag\CoreBundle\Tools\Utils;
10use Wallabag\CoreBundle\Repository\TagRepository;
11use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
12 13
13/** 14/**
14 * 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
@@ -18,38 +19,37 @@ class ContentProxy
18{ 19{
19 protected $graby; 20 protected $graby;
20 protected $tagger; 21 protected $tagger;
22 protected $validator;
21 protected $logger; 23 protected $logger;
22 protected $tagRepository;
23 protected $mimeGuesser; 24 protected $mimeGuesser;
24 protected $fetchingErrorMessage; 25 protected $fetchingErrorMessage;
26 protected $eventDispatcher;
25 27
26 public function __construct(Graby $graby, RuleBasedTagger $tagger, TagRepository $tagRepository, LoggerInterface $logger, $fetchingErrorMessage) 28 public function __construct(Graby $graby, RuleBasedTagger $tagger, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage)
27 { 29 {
28 $this->graby = $graby; 30 $this->graby = $graby;
29 $this->tagger = $tagger; 31 $this->tagger = $tagger;
32 $this->validator = $validator;
30 $this->logger = $logger; 33 $this->logger = $logger;
31 $this->tagRepository = $tagRepository;
32 $this->mimeGuesser = new MimeTypeExtensionGuesser(); 34 $this->mimeGuesser = new MimeTypeExtensionGuesser();
33 $this->fetchingErrorMessage = $fetchingErrorMessage; 35 $this->fetchingErrorMessage = $fetchingErrorMessage;
34 } 36 }
35 37
36 /** 38 /**
37 * Fetch content using graby and hydrate given entry with results information. 39 * Update entry using either fetched or provided content.
38 * In case we couldn't find content, we'll try to use Open Graph data.
39 *
40 * We can also force the content, in case of an import from the v1 for example, so the function won't
41 * fetch the content from the website but rather use information given with the $content parameter.
42 * 40 *
43 * @param Entry $entry Entry to update 41 * @param Entry $entry Entry to update
44 * @param string $url Url to grab content for 42 * @param string $url Url of the content
45 * @param array $content An array with AT LEAST keys title, html, url, language & content_type to skip the fetchContent from the url 43 * @param array $content Array with content provided for import with AT LEAST keys title, html, url to skip the fetchContent from the url
46 * 44 * @param bool $disableContentUpdate Whether to skip trying to fetch content using Graby
47 * @return Entry
48 */ 45 */
49 public function updateEntry(Entry $entry, $url, array $content = []) 46 public function updateEntry(Entry $entry, $url, array $content = [], $disableContentUpdate = false)
50 { 47 {
51 // do we have to fetch the content or the provided one is ok? 48 if (!empty($content['html'])) {
52 if (empty($content) || false === $this->validateContent($content)) { 49 $content['html'] = $this->graby->cleanupHtml($content['html'], $url);
50 }
51
52 if ((empty($content) || false === $this->validateContent($content)) && false === $disableContentUpdate) {
53 $fetchedContent = $this->graby->fetchContent($url); 53 $fetchedContent = $this->graby->fetchContent($url);
54 54
55 // when content is imported, we have information in $content 55 // when content is imported, we have information in $content
@@ -59,107 +59,169 @@ class ContentProxy
59 } 59 }
60 } 60 }
61 61
62 $title = $content['title']; 62 // be sure to keep the url in case of error
63 if (!$title && isset($content['open_graph']['og_title'])) { 63 // so we'll be able to refetch it in the future
64 $title = $content['open_graph']['og_title']; 64 $content['url'] = !empty($content['url']) ? $content['url'] : $url;
65 }
66 65
67 $html = $content['html']; 66 $this->stockEntry($entry, $content);
68 if (false === $html) { 67 }
69 $html = $this->fetchingErrorMessage;
70 68
71 if (isset($content['open_graph']['og_description'])) { 69 /**
72 $html .= '<p><i>But we found a short description: </i></p>'; 70 * Use a Symfony validator to ensure the language is well formatted.
73 $html .= $content['open_graph']['og_description']; 71 *
74 } 72 * @param Entry $entry
75 } 73 * @param string $value Language to validate and save
74 */
75 public function updateLanguage(Entry $entry, $value)
76 {
77 // some lang are defined as fr-FR, es-ES.
78 // replacing - by _ might increase language support
79 $value = str_replace('-', '_', $value);
76 80
77 $entry->setUrl($content['url'] ?: $url); 81 $errors = $this->validator->validate(
78 $entry->setTitle($title); 82 $value,
79 $entry->setContent($html); 83 (new LocaleConstraint())
80 $entry->setHttpStatus(isset($content['status']) ? $content['status'] : ''); 84 );
81 85
82 $entry->setLanguage(isset($content['language']) ? $content['language'] : ''); 86 if (0 === count($errors)) {
83 $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); 87 $entry->setLanguage($value);
84 $entry->setReadingTime(Utils::getReadingTime($html));
85 88
86 $domainName = parse_url($entry->getUrl(), PHP_URL_HOST); 89 return;
87 if (false !== $domainName) {
88 $entry->setDomainName($domainName);
89 } 90 }
90 91
91 if (isset($content['open_graph']['og_image']) && $content['open_graph']['og_image']) { 92 $this->logger->warning('Language validation failed. ' . (string) $errors);
92 $entry->setPreviewPicture($content['open_graph']['og_image']); 93 }
94
95 /**
96 * Use a Symfony validator to ensure the preview picture is a real url.
97 *
98 * @param Entry $entry
99 * @param string $value URL to validate and save
100 */
101 public function updatePreviewPicture(Entry $entry, $value)
102 {
103 $errors = $this->validator->validate(
104 $value,
105 (new UrlConstraint())
106 );
107
108 if (0 === count($errors)) {
109 $entry->setPreviewPicture($value);
110
111 return;
93 } 112 }
94 113
95 // if content is an image define as a preview too 114 $this->logger->warning('PreviewPicture validation failed. ' . (string) $errors);
96 if (isset($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { 115 }
97 $entry->setPreviewPicture($content['url']); 116
117 /**
118 * Update date.
119 *
120 * @param Entry $entry
121 * @param string $value Date to validate and save
122 */
123 public function updatePublishedAt(Entry $entry, $value)
124 {
125 $date = $value;
126
127 // is it a timestamp?
128 if (false !== filter_var($date, FILTER_VALIDATE_INT)) {
129 $date = '@' . $date;
98 } 130 }
99 131
100 try { 132 try {
101 $this->tagger->tag($entry); 133 // is it already a DateTime?
134 // (it's inside the try/catch in case of fail to be parse time string)
135 if (!$date instanceof \DateTime) {
136 $date = new \DateTime($date);
137 }
138
139 $entry->setPublishedAt($date);
102 } catch (\Exception $e) { 140 } catch (\Exception $e) {
103 $this->logger->error('Error while trying to automatically tag an entry.', [ 141 $this->logger->warning('Error while defining date', ['e' => $e, 'url' => $entry->getUrl(), 'date' => $value]);
104 'entry_url' => $url,
105 'error_msg' => $e->getMessage(),
106 ]);
107 } 142 }
108
109 return $entry;
110 } 143 }
111 144
112 /** 145 /**
113 * Assign some tags to an entry. 146 * Stock entry with fetched or imported content.
147 * Will fall back to OpenGraph data if available.
114 * 148 *
115 * @param Entry $entry 149 * @param Entry $entry Entry to stock
116 * @param array|string $tags An array of tag or a string coma separated of tag 150 * @param array $content Array with at least title, url & html
117 * @param array $entitiesReady Entities from the EntityManager which are persisted but not yet flushed
118 * It is mostly to fix duplicate tag on import @see http://stackoverflow.com/a/7879164/569101
119 */ 151 */
120 public function assignTagsToEntry(Entry $entry, $tags, array $entitiesReady = []) 152 private function stockEntry(Entry $entry, array $content)
121 { 153 {
122 if (!is_array($tags)) { 154 $entry->setUrl($content['url']);
123 $tags = explode(',', $tags); 155
156 $domainName = parse_url($entry->getUrl(), PHP_URL_HOST);
157 if (false !== $domainName) {
158 $entry->setDomainName($domainName);
124 } 159 }
125 160
126 // keeps only Tag entity from the "not yet flushed entities" 161 if (!empty($content['title'])) {
127 $tagsNotYetFlushed = []; 162 $entry->setTitle($content['title']);
128 foreach ($entitiesReady as $entity) { 163 } elseif (!empty($content['open_graph']['og_title'])) {
129 if ($entity instanceof Tag) { 164 $entry->setTitle($content['open_graph']['og_title']);
130 $tagsNotYetFlushed[$entity->getLabel()] = $entity;
131 }
132 } 165 }
133 166
134 foreach ($tags as $label) { 167 $html = $content['html'];
135 $label = trim($label); 168 if (false === $html) {
169 $html = $this->fetchingErrorMessage;
136 170
137 // avoid empty tag 171 if (!empty($content['open_graph']['og_description'])) {
138 if (0 === strlen($label)) { 172 $html .= '<p><i>But we found a short description: </i></p>';
139 continue; 173 $html .= $content['open_graph']['og_description'];
140 } 174 }
175 }
141 176
142 if (isset($tagsNotYetFlushed[$label])) { 177 $entry->setContent($html);
143 $tagEntity = $tagsNotYetFlushed[$label]; 178 $entry->setReadingTime(Utils::getReadingTime($html));
144 } else {
145 $tagEntity = $this->tagRepository->findOneByLabel($label);
146 179
147 if (is_null($tagEntity)) { 180 if (!empty($content['status'])) {
148 $tagEntity = new Tag(); 181 $entry->setHttpStatus($content['status']);
149 $tagEntity->setLabel($label); 182 }
150 }
151 }
152 183
153 // only add the tag on the entry if the relation doesn't exist 184 if (!empty($content['authors']) && is_array($content['authors'])) {
154 if (false === $entry->getTags()->contains($tagEntity)) { 185 $entry->setPublishedBy($content['authors']);
155 $entry->addTag($tagEntity); 186 }
156 } 187
188 if (!empty($content['all_headers'])) {
189 $entry->setHeaders($content['all_headers']);
190 }
191
192 if (!empty($content['date'])) {
193 $this->updatePublishedAt($entry, $content['date']);
194 }
195
196 if (!empty($content['language'])) {
197 $this->updateLanguage($entry, $content['language']);
198 }
199
200 if (!empty($content['open_graph']['og_image'])) {
201 $this->updatePreviewPicture($entry, $content['open_graph']['og_image']);
202 }
203
204 // if content is an image, define it as a preview too
205 if (!empty($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) {
206 $this->updatePreviewPicture($entry, $content['url']);
207 }
208
209 if (!empty($content['content_type'])) {
210 $entry->setMimetype($content['content_type']);
211 }
212
213 try {
214 $this->tagger->tag($entry);
215 } catch (\Exception $e) {
216 $this->logger->error('Error while trying to automatically tag an entry.', [
217 'entry_url' => $content['url'],
218 'error_msg' => $e->getMessage(),
219 ]);
157 } 220 }
158 } 221 }
159 222
160 /** 223 /**
161 * Validate that the given content as enough value to be used 224 * Validate that the given content has at least a title, an html and a url.
162 * instead of fetch the content from the url.
163 * 225 *
164 * @param array $content 226 * @param array $content
165 * 227 *
@@ -167,6 +229,6 @@ class ContentProxy
167 */ 229 */
168 private function validateContent(array $content) 230 private function validateContent(array $content)
169 { 231 {
170 return isset($content['title']) && isset($content['html']) && isset($content['url']) && isset($content['language']) && isset($content['content_type']); 232 return !empty($content['title']) && !empty($content['html']) && !empty($content['url']);
171 } 233 }
172} 234}
diff --git a/src/Wallabag/CoreBundle/Helper/CryptoProxy.php b/src/Wallabag/CoreBundle/Helper/CryptoProxy.php
new file mode 100644
index 00000000..7d8c9888
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Helper/CryptoProxy.php
@@ -0,0 +1,86 @@
1<?php
2
3namespace Wallabag\CoreBundle\Helper;
4
5use Defuse\Crypto\Crypto;
6use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException;
7use Defuse\Crypto\Key;
8use Psr\Log\LoggerInterface;
9
10/**
11 * This is a proxy to crypt and decrypt password used by SiteCredential entity.
12 * BTW, It might be re-use for sth else.
13 */
14class CryptoProxy
15{
16 private $logger;
17 private $encryptionKey;
18
19 public function __construct($encryptionKeyPath, LoggerInterface $logger)
20 {
21 $this->logger = $logger;
22
23 if (!file_exists($encryptionKeyPath)) {
24 $key = Key::createNewRandomKey();
25
26 file_put_contents($encryptionKeyPath, $key->saveToAsciiSafeString());
27 chmod($encryptionKeyPath, 0600);
28 }
29
30 $this->encryptionKey = file_get_contents($encryptionKeyPath);
31 }
32
33 /**
34 * Ensure the given value will be crypted.
35 *
36 * @param string $secretValue Secret valye to crypt
37 *
38 * @return string
39 */
40 public function crypt($secretValue)
41 {
42 $this->logger->debug('Crypto: crypting value: ' . $this->mask($secretValue));
43
44 return Crypto::encrypt($secretValue, $this->loadKey());
45 }
46
47 /**
48 * Ensure the given crypted value will be decrypted.
49 *
50 * @param string $cryptedValue The value to be decrypted
51 *
52 * @return string
53 */
54 public function decrypt($cryptedValue)
55 {
56 $this->logger->debug('Crypto: decrypting value: ' . $this->mask($cryptedValue));
57
58 try {
59 return Crypto::decrypt($cryptedValue, $this->loadKey());
60 } catch (WrongKeyOrModifiedCiphertextException $e) {
61 throw new \RuntimeException('Decrypt fail: ' . $e->getMessage());
62 }
63 }
64
65 /**
66 * Load the private key.
67 *
68 * @return Key
69 */
70 private function loadKey()
71 {
72 return Key::loadFromAsciiSafeString($this->encryptionKey);
73 }
74
75 /**
76 * Keep first and last character and put some stars in between.
77 *
78 * @param string $value Value to mask
79 *
80 * @return string
81 */
82 private function mask($value)
83 {
84 return strlen($value) > 0 ? $value[0] . '*****' . $value[strlen($value) - 1] : 'Empty value';
85 }
86}
diff --git a/src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php b/src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php
index 23e98042..9f90ee3e 100644
--- a/src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php
+++ b/src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php
@@ -44,7 +44,7 @@ class DetectActiveTheme implements DeviceDetectionInterface
44 { 44 {
45 $token = $this->tokenStorage->getToken(); 45 $token = $this->tokenStorage->getToken();
46 46
47 if (is_null($token)) { 47 if (null === $token) {
48 return $this->defaultTheme; 48 return $this->defaultTheme;
49 } 49 }
50 50
diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php
index 0d330d2a..252ba57c 100644
--- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php
+++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php
@@ -2,11 +2,12 @@
2 2
3namespace Wallabag\CoreBundle\Helper; 3namespace Wallabag\CoreBundle\Helper;
4 4
5use GuzzleHttp\Client;
6use GuzzleHttp\Message\Response;
5use Psr\Log\LoggerInterface; 7use Psr\Log\LoggerInterface;
6use Symfony\Component\DomCrawler\Crawler; 8use Symfony\Component\DomCrawler\Crawler;
7use GuzzleHttp\Client;
8use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
9use Symfony\Component\Finder\Finder; 9use Symfony\Component\Finder\Finder;
10use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
10 11
11class DownloadImages 12class DownloadImages
12{ 13{
@@ -30,17 +31,6 @@ class DownloadImages
30 } 31 }
31 32
32 /** 33 /**
33 * Setup base folder where all images are going to be saved.
34 */
35 private function setFolder()
36 {
37 // if folder doesn't exist, attempt to create one and store the folder name in property $folder
38 if (!file_exists($this->baseFolder)) {
39 mkdir($this->baseFolder, 0755, true);
40 }
41 }
42
43 /**
44 * Process the html and extract image from it, save them to local and return the updated html. 34 * Process the html and extract image from it, save them to local and return the updated html.
45 * 35 *
46 * @param int $entryId ID of the entry 36 * @param int $entryId ID of the entry
@@ -54,7 +44,7 @@ class DownloadImages
54 $crawler = new Crawler($html); 44 $crawler = new Crawler($html);
55 $result = $crawler 45 $result = $crawler
56 ->filterXpath('//img') 46 ->filterXpath('//img')
57 ->extract(array('src')); 47 ->extract(['src']);
58 48
59 $relativePath = $this->getRelativePath($entryId); 49 $relativePath = $this->getRelativePath($entryId);
60 50
@@ -66,6 +56,11 @@ class DownloadImages
66 continue; 56 continue;
67 } 57 }
68 58
59 // if image contains "&" and we can't find it in the html it might be because it's encoded as &amp;
60 if (false !== stripos($image, '&') && false === stripos($html, $image)) {
61 $image = str_replace('&', '&amp;', $image);
62 }
63
69 $html = str_replace($image, $imagePath, $html); 64 $html = str_replace($image, $imagePath, $html);
70 } 65 }
71 66
@@ -91,9 +86,9 @@ class DownloadImages
91 $relativePath = $this->getRelativePath($entryId); 86 $relativePath = $this->getRelativePath($entryId);
92 } 87 }
93 88
94 $this->logger->debug('DownloadImages: working on image: '.$imagePath); 89 $this->logger->debug('DownloadImages: working on image: ' . $imagePath);
95 90
96 $folderPath = $this->baseFolder.'/'.$relativePath; 91 $folderPath = $this->baseFolder . '/' . $relativePath;
97 92
98 // build image path 93 // build image path
99 $absolutePath = $this->getAbsoluteLink($url, $imagePath); 94 $absolutePath = $this->getAbsoluteLink($url, $imagePath);
@@ -111,15 +106,13 @@ class DownloadImages
111 return false; 106 return false;
112 } 107 }
113 108
114 $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); 109 $ext = $this->getExtensionFromResponse($res, $imagePath);
115 $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); 110 if (false === $res) {
116 if (!in_array($ext, ['jpeg', 'jpg', 'gif', 'png'], true)) {
117 $this->logger->error('DownloadImages: Processed image with not allowed extension. Skipping '.$imagePath);
118
119 return false; 111 return false;
120 } 112 }
113
121 $hashImage = hash('crc32', $absolutePath); 114 $hashImage = hash('crc32', $absolutePath);
122 $localPath = $folderPath.'/'.$hashImage.'.'.$ext; 115 $localPath = $folderPath . '/' . $hashImage . '.' . $ext;
123 116
124 try { 117 try {
125 $im = imagecreatefromstring($res->getBody()); 118 $im = imagecreatefromstring($res->getBody());
@@ -152,7 +145,7 @@ class DownloadImages
152 145
153 imagedestroy($im); 146 imagedestroy($im);
154 147
155 return $this->wallabagUrl.'/assets/images/'.$relativePath.'/'.$hashImage.'.'.$ext; 148 return $this->wallabagUrl . '/assets/images/' . $relativePath . '/' . $hashImage . '.' . $ext;
156 } 149 }
157 150
158 /** 151 /**
@@ -163,7 +156,7 @@ class DownloadImages
163 public function removeImages($entryId) 156 public function removeImages($entryId)
164 { 157 {
165 $relativePath = $this->getRelativePath($entryId); 158 $relativePath = $this->getRelativePath($entryId);
166 $folderPath = $this->baseFolder.'/'.$relativePath; 159 $folderPath = $this->baseFolder . '/' . $relativePath;
167 160
168 $finder = new Finder(); 161 $finder = new Finder();
169 $finder 162 $finder
@@ -179,6 +172,17 @@ class DownloadImages
179 } 172 }
180 173
181 /** 174 /**
175 * Setup base folder where all images are going to be saved.
176 */
177 private function setFolder()
178 {
179 // if folder doesn't exist, attempt to create one and store the folder name in property $folder
180 if (!file_exists($this->baseFolder)) {
181 mkdir($this->baseFolder, 0755, true);
182 }
183 }
184
185 /**
182 * Generate the folder where we are going to save images based on the entry url. 186 * Generate the folder where we are going to save images based on the entry url.
183 * 187 *
184 * @param int $entryId ID of the entry 188 * @param int $entryId ID of the entry
@@ -188,8 +192,8 @@ class DownloadImages
188 private function getRelativePath($entryId) 192 private function getRelativePath($entryId)
189 { 193 {
190 $hashId = hash('crc32', $entryId); 194 $hashId = hash('crc32', $entryId);
191 $relativePath = $hashId[0].'/'.$hashId[1].'/'.$hashId; 195 $relativePath = $hashId[0] . '/' . $hashId[1] . '/' . $hashId;
192 $folderPath = $this->baseFolder.'/'.$relativePath; 196 $folderPath = $this->baseFolder . '/' . $relativePath;
193 197
194 if (!file_exists($folderPath)) { 198 if (!file_exists($folderPath)) {
195 mkdir($folderPath, 0777, true); 199 mkdir($folderPath, 0777, true);
@@ -232,4 +236,45 @@ class DownloadImages
232 236
233 return false; 237 return false;
234 } 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 }
235} 280}
diff --git a/src/Wallabag/CoreBundle/Helper/EntityTimestampsTrait.php b/src/Wallabag/CoreBundle/Helper/EntityTimestampsTrait.php
new file mode 100644
index 00000000..1b1ff54a
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Helper/EntityTimestampsTrait.php
@@ -0,0 +1,24 @@
1<?php
2
3namespace Wallabag\CoreBundle\Helper;
4
5use Doctrine\ORM\Mapping as ORM;
6
7/**
8 * Trait to handle created & updated date of an Entity.
9 */
10trait EntityTimestampsTrait
11{
12 /**
13 * @ORM\PrePersist
14 * @ORM\PreUpdate
15 */
16 public function timestamps()
17 {
18 if (null === $this->createdAt) {
19 $this->createdAt = new \DateTime();
20 }
21
22 $this->updatedAt = new \DateTime();
23 }
24}
diff --git a/src/Wallabag/CoreBundle/Helper/EntriesExport.php b/src/Wallabag/CoreBundle/Helper/EntriesExport.php
index 3d36a4c8..830798b8 100644
--- a/src/Wallabag/CoreBundle/Helper/EntriesExport.php
+++ b/src/Wallabag/CoreBundle/Helper/EntriesExport.php
@@ -2,12 +2,14 @@
2 2
3namespace Wallabag\CoreBundle\Helper; 3namespace Wallabag\CoreBundle\Helper;
4 4
5use JMS\Serializer; 5use Html2Text\Html2Text;
6use JMS\Serializer\SerializationContext; 6use JMS\Serializer\SerializationContext;
7use JMS\Serializer\SerializerBuilder; 7use JMS\Serializer\SerializerBuilder;
8use PHPePub\Core\EPub; 8use PHPePub\Core\EPub;
9use PHPePub\Core\Structure\OPF\DublinCore; 9use PHPePub\Core\Structure\OPF\DublinCore;
10use Symfony\Component\HttpFoundation\Response; 10use Symfony\Component\HttpFoundation\Response;
11use Symfony\Component\Translation\TranslatorInterface;
12use Wallabag\CoreBundle\Entity\Entry;
11 13
12/** 14/**
13 * This class doesn't have unit test BUT it's fully covered by a functional test with ExportControllerTest. 15 * This class doesn't have unit test BUT it's fully covered by a functional test with ExportControllerTest.
@@ -16,21 +18,20 @@ class EntriesExport
16{ 18{
17 private $wallabagUrl; 19 private $wallabagUrl;
18 private $logoPath; 20 private $logoPath;
21 private $translator;
19 private $title = ''; 22 private $title = '';
20 private $entries = []; 23 private $entries = [];
21 private $authors = ['wallabag']; 24 private $author = 'wallabag';
22 private $language = ''; 25 private $language = '';
23 private $footerTemplate = '<div style="text-align:center;">
24 <p>Produced by wallabag with %EXPORT_METHOD%</p>
25 <p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p>
26 </div>';
27 26
28 /** 27 /**
29 * @param string $wallabagUrl Wallabag instance url 28 * @param TranslatorInterface $translator Translator service
30 * @param string $logoPath Path to the logo FROM THE BUNDLE SCOPE 29 * @param string $wallabagUrl Wallabag instance url
30 * @param string $logoPath Path to the logo FROM THE BUNDLE SCOPE
31 */ 31 */
32 public function __construct($wallabagUrl, $logoPath) 32 public function __construct(TranslatorInterface $translator, $wallabagUrl, $logoPath)
33 { 33 {
34 $this->translator = $translator;
34 $this->wallabagUrl = $wallabagUrl; 35 $this->wallabagUrl = $wallabagUrl;
35 $this->logoPath = $logoPath; 36 $this->logoPath = $logoPath;
36 } 37 }
@@ -63,7 +64,7 @@ class EntriesExport
63 */ 64 */
64 public function updateTitle($method) 65 public function updateTitle($method)
65 { 66 {
66 $this->title = $method.' articles'; 67 $this->title = $method . ' articles';
67 68
68 if ('entry' === $method) { 69 if ('entry' === $method) {
69 $this->title = $this->entries[0]->getTitle(); 70 $this->title = $this->entries[0]->getTitle();
@@ -73,6 +74,33 @@ class EntriesExport
73 } 74 }
74 75
75 /** 76 /**
77 * Sets the author for one entry or category.
78 *
79 * The publishers are used, or the domain name if empty.
80 *
81 * @param string $method Method to get articles
82 *
83 * @return EntriesExport
84 */
85 public function updateAuthor($method)
86 {
87 if ('entry' !== $method) {
88 $this->author = $method . ' authors';
89
90 return $this;
91 }
92
93 $this->author = $this->entries[0]->getDomainName();
94
95 $publishedBy = $this->entries[0]->getPublishedBy();
96 if (!empty($publishedBy)) {
97 $this->author = implode(', ', $publishedBy);
98 }
99
100 return $this;
101 }
102
103 /**
76 * Sets the output format. 104 * Sets the output format.
77 * 105 *
78 * @param string $format 106 * @param string $format
@@ -81,7 +109,7 @@ class EntriesExport
81 */ 109 */
82 public function exportAs($format) 110 public function exportAs($format)
83 { 111 {
84 $functionName = 'produce'.ucfirst($format); 112 $functionName = 'produce' . ucfirst($format);
85 if (method_exists($this, $functionName)) { 113 if (method_exists($this, $functionName)) {
86 return $this->$functionName(); 114 return $this->$functionName();
87 } 115 }
@@ -106,12 +134,12 @@ class EntriesExport
106 */ 134 */
107 $content_start = 135 $content_start =
108 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" 136 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
109 ."<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n" 137 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
110 .'<head>' 138 . '<head>'
111 ."<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n" 139 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
112 ."<title>wallabag articles book</title>\n" 140 . "<title>wallabag articles book</title>\n"
113 ."</head>\n" 141 . "</head>\n"
114 ."<body>\n"; 142 . "<body>\n";
115 143
116 $bookEnd = "</body>\n</html>\n"; 144 $bookEnd = "</body>\n</html>\n";
117 145
@@ -128,9 +156,7 @@ class EntriesExport
128 $book->setLanguage($this->language); 156 $book->setLanguage($this->language);
129 $book->setDescription('Some articles saved on my wallabag'); 157 $book->setDescription('Some articles saved on my wallabag');
130 158
131 foreach ($this->authors as $author) { 159 $book->setAuthor($this->author, $this->author);
132 $book->setAuthor($author, $author);
133 }
134 160
135 // I hope this is a non existant address :) 161 // I hope this is a non existant address :)
136 $book->setPublisher('wallabag', 'wallabag'); 162 $book->setPublisher('wallabag', 'wallabag');
@@ -164,11 +190,11 @@ class EntriesExport
164 // in filenames, we limit to A-z/0-9 190 // in filenames, we limit to A-z/0-9
165 $filename = preg_replace('/[^A-Za-z0-9\-]/', '', $entry->getTitle()); 191 $filename = preg_replace('/[^A-Za-z0-9\-]/', '', $entry->getTitle());
166 192
167 $chapter = $content_start.$entry->getContent().$bookEnd; 193 $chapter = $content_start . $entry->getContent() . $bookEnd;
168 $book->addChapter($entry->getTitle(), htmlspecialchars($filename).'.html', $chapter, true, EPub::EXTERNAL_REF_ADD); 194 $book->addChapter($entry->getTitle(), htmlspecialchars($filename) . '.html', $chapter, true, EPub::EXTERNAL_REF_ADD);
169 } 195 }
170 196
171 $book->addChapter('Notices', 'Cover2.html', $content_start.$this->getExportInformation('PHPePub').$bookEnd); 197 $book->addChapter('Notices', 'Cover2.html', $content_start . $this->getExportInformation('PHPePub') . $bookEnd);
172 198
173 return Response::create( 199 return Response::create(
174 $book->getBook(), 200 $book->getBook(),
@@ -176,7 +202,7 @@ class EntriesExport
176 [ 202 [
177 'Content-Description' => 'File Transfer', 203 'Content-Description' => 'File Transfer',
178 'Content-type' => 'application/epub+zip', 204 'Content-type' => 'application/epub+zip',
179 'Content-Disposition' => 'attachment; filename="'.$this->title.'.epub"', 205 'Content-Disposition' => 'attachment; filename="' . $this->title . '.epub"',
180 'Content-Transfer-Encoding' => 'binary', 206 'Content-Transfer-Encoding' => 'binary',
181 ] 207 ]
182 ); 208 );
@@ -196,7 +222,7 @@ class EntriesExport
196 * Book metadata 222 * Book metadata
197 */ 223 */
198 $content->set('title', $this->title); 224 $content->set('title', $this->title);
199 $content->set('author', implode($this->authors)); 225 $content->set('author', $this->author);
200 $content->set('subject', $this->title); 226 $content->set('subject', $this->title);
201 227
202 /* 228 /*
@@ -228,7 +254,7 @@ class EntriesExport
228 'Accept-Ranges' => 'bytes', 254 'Accept-Ranges' => 'bytes',
229 'Content-Description' => 'File Transfer', 255 'Content-Description' => 'File Transfer',
230 'Content-type' => 'application/x-mobipocket-ebook', 256 'Content-type' => 'application/x-mobipocket-ebook',
231 'Content-Disposition' => 'attachment; filename="'.$this->title.'.mobi"', 257 'Content-Disposition' => 'attachment; filename="' . $this->title . '.mobi"',
232 'Content-Transfer-Encoding' => 'binary', 258 'Content-Transfer-Encoding' => 'binary',
233 ] 259 ]
234 ); 260 );
@@ -247,7 +273,7 @@ class EntriesExport
247 * Book metadata 273 * Book metadata
248 */ 274 */
249 $pdf->SetCreator(PDF_CREATOR); 275 $pdf->SetCreator(PDF_CREATOR);
250 $pdf->SetAuthor('wallabag'); 276 $pdf->SetAuthor($this->author);
251 $pdf->SetTitle($this->title); 277 $pdf->SetTitle($this->title);
252 $pdf->SetSubject('Articles via wallabag'); 278 $pdf->SetSubject('Articles via wallabag');
253 $pdf->SetKeywords('wallabag'); 279 $pdf->SetKeywords('wallabag');
@@ -256,7 +282,7 @@ class EntriesExport
256 * Front page 282 * Front page
257 */ 283 */
258 $pdf->AddPage(); 284 $pdf->AddPage();
259 $intro = '<h1>'.$this->title.'</h1>'.$this->getExportInformation('tcpdf'); 285 $intro = '<h1>' . $this->title . '</h1>' . $this->getExportInformation('tcpdf');
260 286
261 $pdf->writeHTMLCell(0, 0, '', '', $intro, 0, 1, 0, true, '', true); 287 $pdf->writeHTMLCell(0, 0, '', '', $intro, 0, 1, 0, true, '', true);
262 288
@@ -269,7 +295,7 @@ class EntriesExport
269 } 295 }
270 296
271 $pdf->AddPage(); 297 $pdf->AddPage();
272 $html = '<h1>'.$entry->getTitle().'</h1>'; 298 $html = '<h1>' . $entry->getTitle() . '</h1>';
273 $html .= $entry->getContent(); 299 $html .= $entry->getContent();
274 300
275 $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); 301 $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true);
@@ -284,7 +310,7 @@ class EntriesExport
284 [ 310 [
285 'Content-Description' => 'File Transfer', 311 'Content-Description' => 'File Transfer',
286 'Content-type' => 'application/pdf', 312 'Content-type' => 'application/pdf',
287 'Content-Disposition' => 'attachment; filename="'.$this->title.'.pdf"', 313 'Content-Disposition' => 'attachment; filename="' . $this->title . '.pdf"',
288 'Content-Transfer-Encoding' => 'binary', 314 'Content-Transfer-Encoding' => 'binary',
289 ] 315 ]
290 ); 316 );
@@ -330,7 +356,7 @@ class EntriesExport
330 200, 356 200,
331 [ 357 [
332 'Content-type' => 'application/csv', 358 'Content-type' => 'application/csv',
333 'Content-Disposition' => 'attachment; filename="'.$this->title.'.csv"', 359 'Content-Disposition' => 'attachment; filename="' . $this->title . '.csv"',
334 'Content-Transfer-Encoding' => 'UTF-8', 360 'Content-Transfer-Encoding' => 'UTF-8',
335 ] 361 ]
336 ); 362 );
@@ -348,7 +374,7 @@ class EntriesExport
348 200, 374 200,
349 [ 375 [
350 'Content-type' => 'application/json', 376 'Content-type' => 'application/json',
351 'Content-Disposition' => 'attachment; filename="'.$this->title.'.json"', 377 'Content-Disposition' => 'attachment; filename="' . $this->title . '.json"',
352 'Content-Transfer-Encoding' => 'UTF-8', 378 'Content-Transfer-Encoding' => 'UTF-8',
353 ] 379 ]
354 ); 380 );
@@ -366,7 +392,7 @@ class EntriesExport
366 200, 392 200,
367 [ 393 [
368 'Content-type' => 'application/xml', 394 'Content-type' => 'application/xml',
369 'Content-Disposition' => 'attachment; filename="'.$this->title.'.xml"', 395 'Content-Disposition' => 'attachment; filename="' . $this->title . '.xml"',
370 'Content-Transfer-Encoding' => 'UTF-8', 396 'Content-Transfer-Encoding' => 'UTF-8',
371 ] 397 ]
372 ); 398 );
@@ -382,8 +408,9 @@ class EntriesExport
382 $content = ''; 408 $content = '';
383 $bar = str_repeat('=', 100); 409 $bar = str_repeat('=', 100);
384 foreach ($this->entries as $entry) { 410 foreach ($this->entries as $entry) {
385 $content .= "\n\n".$bar."\n\n".$entry->getTitle()."\n\n".$bar."\n\n"; 411 $content .= "\n\n" . $bar . "\n\n" . $entry->getTitle() . "\n\n" . $bar . "\n\n";
386 $content .= trim(preg_replace('/\s+/S', ' ', strip_tags($entry->getContent())))."\n\n"; 412 $html = new Html2Text($entry->getContent(), ['do_links' => 'none', 'width' => 100]);
413 $content .= $html->getText();
387 } 414 }
388 415
389 return Response::create( 416 return Response::create(
@@ -391,7 +418,7 @@ class EntriesExport
391 200, 418 200,
392 [ 419 [
393 'Content-type' => 'text/plain', 420 'Content-type' => 'text/plain',
394 'Content-Disposition' => 'attachment; filename="'.$this->title.'.txt"', 421 'Content-Disposition' => 'attachment; filename="' . $this->title . '.txt"',
395 'Content-Transfer-Encoding' => 'UTF-8', 422 'Content-Transfer-Encoding' => 'UTF-8',
396 ] 423 ]
397 ); 424 );
@@ -402,7 +429,7 @@ class EntriesExport
402 * 429 *
403 * @param string $format 430 * @param string $format
404 * 431 *
405 * @return Serializer 432 * @return string
406 */ 433 */
407 private function prepareSerializingContent($format) 434 private function prepareSerializingContent($format)
408 { 435 {
@@ -424,10 +451,12 @@ class EntriesExport
424 */ 451 */
425 private function getExportInformation($type) 452 private function getExportInformation($type)
426 { 453 {
427 $info = str_replace('%EXPORT_METHOD%', $type, $this->footerTemplate); 454 $info = $this->translator->trans('export.footer_template', [
455 '%method%' => $type,
456 ]);
428 457
429 if ('tcpdf' === $type) { 458 if ('tcpdf' === $type) {
430 return str_replace('%IMAGE%', '<img src="'.$this->logoPath.'" />', $info); 459 return str_replace('%IMAGE%', '<img src="' . $this->logoPath . '" />', $info);
431 } 460 }
432 461
433 return str_replace('%IMAGE%', '', $info); 462 return str_replace('%IMAGE%', '', $info);
diff --git a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php
index 1ac8feb1..4602a684 100644
--- a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php
+++ b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php
@@ -13,8 +13,8 @@ use Psr\Log\LoggerInterface;
13 */ 13 */
14class HttpClientFactory 14class HttpClientFactory
15{ 15{
16 /** @var \GuzzleHttp\Event\SubscriberInterface */ 16 /** @var [\GuzzleHttp\Event\SubscriberInterface] */
17 private $authenticatorSubscriber; 17 private $subscribers = [];
18 18
19 /** @var \GuzzleHttp\Cookie\CookieJar */ 19 /** @var \GuzzleHttp\Cookie\CookieJar */
20 private $cookieJar; 20 private $cookieJar;
@@ -25,14 +25,12 @@ class HttpClientFactory
25 /** 25 /**
26 * HttpClientFactory constructor. 26 * HttpClientFactory constructor.
27 * 27 *
28 * @param \GuzzleHttp\Event\SubscriberInterface $authenticatorSubscriber 28 * @param \GuzzleHttp\Cookie\CookieJar $cookieJar
29 * @param \GuzzleHttp\Cookie\CookieJar $cookieJar 29 * @param string $restrictedAccess This param is a kind of boolean. Values: 0 or 1
30 * @param string $restrictedAccess this param is a kind of boolean. Values: 0 or 1 30 * @param LoggerInterface $logger
31 * @param LoggerInterface $logger
32 */ 31 */
33 public function __construct(SubscriberInterface $authenticatorSubscriber, CookieJar $cookieJar, $restrictedAccess, LoggerInterface $logger) 32 public function __construct(CookieJar $cookieJar, $restrictedAccess, LoggerInterface $logger)
34 { 33 {
35 $this->authenticatorSubscriber = $authenticatorSubscriber;
36 $this->cookieJar = $cookieJar; 34 $this->cookieJar = $cookieJar;
37 $this->restrictedAccess = $restrictedAccess; 35 $this->restrictedAccess = $restrictedAccess;
38 $this->logger = $logger; 36 $this->logger = $logger;
@@ -43,7 +41,7 @@ class HttpClientFactory
43 */ 41 */
44 public function buildHttpClient() 42 public function buildHttpClient()
45 { 43 {
46 $this->logger->log('debug', 'Restricted access config enabled?', array('enabled' => (int) $this->restrictedAccess)); 44 $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]);
47 45
48 if (0 === (int) $this->restrictedAccess) { 46 if (0 === (int) $this->restrictedAccess) {
49 return; 47 return;
@@ -53,8 +51,21 @@ class HttpClientFactory
53 $this->cookieJar->clear(); 51 $this->cookieJar->clear();
54 // need to set the (shared) cookie jar 52 // need to set the (shared) cookie jar
55 $client = new Client(['handler' => new SafeCurlHandler(), 'defaults' => ['cookies' => $this->cookieJar]]); 53 $client = new Client(['handler' => new SafeCurlHandler(), 'defaults' => ['cookies' => $this->cookieJar]]);
56 $client->getEmitter()->attach($this->authenticatorSubscriber); 54
55 foreach ($this->subscribers as $subscriber) {
56 $client->getEmitter()->attach($subscriber);
57 }
57 58
58 return $client; 59 return $client;
59 } 60 }
61
62 /**
63 * Adds a subscriber to the HTTP client.
64 *
65 * @param SubscriberInterface $subscriber
66 */
67 public function addSubscriber(SubscriberInterface $subscriber)
68 {
69 $this->subscribers[] = $subscriber;
70 }
60} 71}
diff --git a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php
index 7d3798b9..49c1ea41 100644
--- a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php
+++ b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php
@@ -6,6 +6,7 @@ use Pagerfanta\Adapter\AdapterInterface;
6use Pagerfanta\Pagerfanta; 6use Pagerfanta\Pagerfanta;
7use Symfony\Component\Routing\Router; 7use Symfony\Component\Routing\Router;
8use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 8use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
9use Wallabag\UserBundle\Entity\User;
9 10
10class PreparePagerForEntries 11class PreparePagerForEntries
11{ 12{
@@ -20,16 +21,18 @@ class PreparePagerForEntries
20 21
21 /** 22 /**
22 * @param AdapterInterface $adapter 23 * @param AdapterInterface $adapter
23 * @param int $page 24 * @param User $user If user isn't logged in, we can force it (like for rss)
24 * 25 *
25 * @return null|Pagerfanta 26 * @return null|Pagerfanta
26 */ 27 */
27 public function prepare(AdapterInterface $adapter, $page = 1) 28 public function prepare(AdapterInterface $adapter, User $user = null)
28 { 29 {
29 $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; 30 if (null === $user) {
31 $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
32 }
30 33
31 if (null === $user || !is_object($user)) { 34 if (null === $user || !is_object($user)) {
32 return null; 35 return;
33 } 36 }
34 37
35 $entries = new Pagerfanta($adapter); 38 $entries = new Pagerfanta($adapter);
diff --git a/src/Wallabag/CoreBundle/Helper/Redirect.php b/src/Wallabag/CoreBundle/Helper/Redirect.php
index f78b7fe0..abc84d08 100644
--- a/src/Wallabag/CoreBundle/Helper/Redirect.php
+++ b/src/Wallabag/CoreBundle/Helper/Redirect.php
@@ -21,12 +21,13 @@ class Redirect
21 } 21 }
22 22
23 /** 23 /**
24 * @param string $url URL to redirect 24 * @param string $url URL to redirect
25 * @param string $fallback Fallback URL if $url is null 25 * @param string $fallback Fallback URL if $url is null
26 * @param bool $ignoreActionMarkAsRead Ignore configured action when mark as read
26 * 27 *
27 * @return string 28 * @return string
28 */ 29 */
29 public function to($url, $fallback = '') 30 public function to($url, $fallback = '', $ignoreActionMarkAsRead = false)
30 { 31 {
31 $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; 32 $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
32 33
@@ -34,7 +35,8 @@ class Redirect
34 return $url; 35 return $url;
35 } 36 }
36 37
37 if (Config::REDIRECT_TO_HOMEPAGE === $user->getConfig()->getActionMarkAsRead()) { 38 if (!$ignoreActionMarkAsRead &&
39 Config::REDIRECT_TO_HOMEPAGE === $user->getConfig()->getActionMarkAsRead()) {
38 return $this->router->generate('homepage'); 40 return $this->router->generate('homepage');
39 } 41 }
40 42
diff --git a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php
index b490e209..63f65067 100644
--- a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php
+++ b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php
@@ -2,6 +2,7 @@
2 2
3namespace Wallabag\CoreBundle\Helper; 3namespace Wallabag\CoreBundle\Helper;
4 4
5use Psr\Log\LoggerInterface;
5use RulerZ\RulerZ; 6use RulerZ\RulerZ;
6use Wallabag\CoreBundle\Entity\Entry; 7use Wallabag\CoreBundle\Entity\Entry;
7use Wallabag\CoreBundle\Entity\Tag; 8use Wallabag\CoreBundle\Entity\Tag;
@@ -14,12 +15,14 @@ class RuleBasedTagger
14 private $rulerz; 15 private $rulerz;
15 private $tagRepository; 16 private $tagRepository;
16 private $entryRepository; 17 private $entryRepository;
18 private $logger;
17 19
18 public function __construct(RulerZ $rulerz, TagRepository $tagRepository, EntryRepository $entryRepository) 20 public function __construct(RulerZ $rulerz, TagRepository $tagRepository, EntryRepository $entryRepository, LoggerInterface $logger)
19 { 21 {
20 $this->rulerz = $rulerz; 22 $this->rulerz = $rulerz;
21 $this->tagRepository = $tagRepository; 23 $this->tagRepository = $tagRepository;
22 $this->entryRepository = $entryRepository; 24 $this->entryRepository = $entryRepository;
25 $this->logger = $logger;
23 } 26 }
24 27
25 /** 28 /**
@@ -36,6 +39,11 @@ class RuleBasedTagger
36 continue; 39 continue;
37 } 40 }
38 41
42 $this->logger->info('Matching rule.', [
43 'rule' => $rule->getRule(),
44 'tags' => $rule->getTags(),
45 ]);
46
39 foreach ($rule->getTags() as $label) { 47 foreach ($rule->getTags() as $label) {
40 $tag = $this->getTag($label); 48 $tag = $this->getTag($label);
41 49
diff --git a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php
new file mode 100644
index 00000000..0bfe5c57
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php
@@ -0,0 +1,75 @@
1<?php
2
3namespace Wallabag\CoreBundle\Helper;
4
5use Wallabag\CoreBundle\Entity\Entry;
6use Wallabag\CoreBundle\Entity\Tag;
7use Wallabag\CoreBundle\Repository\TagRepository;
8
9class TagsAssigner
10{
11 /**
12 * @var TagRepository
13 */
14 protected $tagRepository;
15
16 public function __construct(TagRepository $tagRepository)
17 {
18 $this->tagRepository = $tagRepository;
19 }
20
21 /**
22 * Assign some tags to an entry.
23 *
24 * @param Entry $entry
25 * @param array|string $tags An array of tag or a string coma separated of tag
26 * @param array $entitiesReady Entities from the EntityManager which are persisted but not yet flushed
27 * It is mostly to fix duplicate tag on import @see http://stackoverflow.com/a/7879164/569101
28 *
29 * @return Tag[]
30 */
31 public function assignTagsToEntry(Entry $entry, $tags, array $entitiesReady = [])
32 {
33 $tagsEntities = [];
34
35 if (!is_array($tags)) {
36 $tags = explode(',', $tags);
37 }
38
39 // keeps only Tag entity from the "not yet flushed entities"
40 $tagsNotYetFlushed = [];
41 foreach ($entitiesReady as $entity) {
42 if ($entity instanceof Tag) {
43 $tagsNotYetFlushed[$entity->getLabel()] = $entity;
44 }
45 }
46
47 foreach ($tags as $label) {
48 $label = trim(mb_convert_case($label, MB_CASE_LOWER));
49
50 // avoid empty tag
51 if (0 === strlen($label)) {
52 continue;
53 }
54
55 if (isset($tagsNotYetFlushed[$label])) {
56 $tagEntity = $tagsNotYetFlushed[$label];
57 } else {
58 $tagEntity = $this->tagRepository->findOneByLabel($label);
59
60 if (null === $tagEntity) {
61 $tagEntity = new Tag();
62 $tagEntity->setLabel($label);
63 }
64 }
65
66 // only add the tag on the entry if the relation doesn't exist
67 if (false === $entry->getTags()->contains($tagEntity)) {
68 $entry->addTag($tagEntity);
69 $tagsEntities[] = $tagEntity;
70 }
71 }
72
73 return $tagsEntities;
74 }
75}
diff --git a/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php b/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php
index e6bb03b1..e1610161 100644
--- a/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php
+++ b/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php
@@ -16,7 +16,7 @@ class Matches
16{ 16{
17 public function __invoke($subject, $pattern) 17 public function __invoke($subject, $pattern)
18 { 18 {
19 if ($pattern[0] === "'") { 19 if ("'" === $pattern[0]) {
20 $pattern = sprintf("'%%%s%%'", substr($pattern, 1, -1)); 20 $pattern = sprintf("'%%%s%%'", substr($pattern, 1, -1));
21 } 21 }
22 22
diff --git a/src/Wallabag/CoreBundle/Operator/Doctrine/NotMatches.php b/src/Wallabag/CoreBundle/Operator/Doctrine/NotMatches.php
new file mode 100644
index 00000000..8e50f8d6
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Operator/Doctrine/NotMatches.php
@@ -0,0 +1,25 @@
1<?php
2
3namespace Wallabag\CoreBundle\Operator\Doctrine;
4
5/**
6 * Provides a "notmatches" operator used for tagging rules.
7 *
8 * It asserts that a given pattern is not contained in a subject, in a
9 * case-insensitive way.
10 *
11 * This operator will be used to compile tagging rules in DQL, usable
12 * by Doctrine ORM.
13 * It's registered in RulerZ using a service (wallabag.operator.doctrine.notmatches);
14 */
15class NotMatches
16{
17 public function __invoke($subject, $pattern)
18 {
19 if ("'" === $pattern[0]) {
20 $pattern = sprintf("'%%%s%%'", substr($pattern, 1, -1));
21 }
22
23 return sprintf('UPPER(%s) NOT LIKE UPPER(%s)', $subject, $pattern);
24 }
25}
diff --git a/src/Wallabag/CoreBundle/Operator/PHP/Matches.php b/src/Wallabag/CoreBundle/Operator/PHP/Matches.php
index 987ed2a5..bc0c3f8f 100644
--- a/src/Wallabag/CoreBundle/Operator/PHP/Matches.php
+++ b/src/Wallabag/CoreBundle/Operator/PHP/Matches.php
@@ -16,6 +16,6 @@ class Matches
16{ 16{
17 public function __invoke($subject, $pattern) 17 public function __invoke($subject, $pattern)
18 { 18 {
19 return stripos($subject, $pattern) !== false; 19 return false !== stripos($subject, $pattern);
20 } 20 }
21} 21}
diff --git a/src/Wallabag/CoreBundle/Operator/PHP/NotMatches.php b/src/Wallabag/CoreBundle/Operator/PHP/NotMatches.php
new file mode 100644
index 00000000..bd4d887a
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Operator/PHP/NotMatches.php
@@ -0,0 +1,21 @@
1<?php
2
3namespace Wallabag\CoreBundle\Operator\PHP;
4
5/**
6 * Provides a "notmatches" operator used for tagging rules.
7 *
8 * It asserts that a given pattern is not contained in a subject, in a
9 * case-insensitive way.
10 *
11 * This operator will be used to compile tagging rules in PHP, usable
12 * directly on Entry objects for instance.
13 * It's registered in RulerZ using a service (wallabag.operator.array.notmatches);
14 */
15class NotMatches
16{
17 public function __invoke($subject, $pattern)
18 {
19 return false === stripos($subject, $pattern);
20 }
21}
diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
index 4071301d..b5e35eff 100644
--- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php
+++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
@@ -3,29 +3,15 @@
3namespace Wallabag\CoreBundle\Repository; 3namespace Wallabag\CoreBundle\Repository;
4 4
5use Doctrine\ORM\EntityRepository; 5use Doctrine\ORM\EntityRepository;
6use Doctrine\ORM\Query; 6use Doctrine\ORM\QueryBuilder;
7use Pagerfanta\Adapter\DoctrineORMAdapter; 7use Pagerfanta\Adapter\DoctrineORMAdapter;
8use Pagerfanta\Pagerfanta; 8use Pagerfanta\Pagerfanta;
9use Wallabag\CoreBundle\Entity\Entry;
9use Wallabag\CoreBundle\Entity\Tag; 10use Wallabag\CoreBundle\Entity\Tag;
10 11
11class EntryRepository extends EntityRepository 12class EntryRepository extends EntityRepository
12{ 13{
13 /** 14 /**
14 * Return a query builder to used by other getBuilderFor* method.
15 *
16 * @param int $userId
17 *
18 * @return QueryBuilder
19 */
20 private function getBuilderByUser($userId)
21 {
22 return $this->createQueryBuilder('e')
23 ->andWhere('e.user = :userId')->setParameter('userId', $userId)
24 ->orderBy('e.createdAt', 'desc')
25 ;
26 }
27
28 /**
29 * Retrieves all entries for a user. 15 * Retrieves all entries for a user.
30 * 16 *
31 * @param int $userId 17 * @param int $userId
@@ -79,7 +65,7 @@ class EntryRepository extends EntityRepository
79 public function getBuilderForStarredByUser($userId) 65 public function getBuilderForStarredByUser($userId)
80 { 66 {
81 return $this 67 return $this
82 ->getBuilderByUser($userId) 68 ->getBuilderByUser($userId, 'starredAt', 'desc')
83 ->andWhere('e.isStarred = true') 69 ->andWhere('e.isStarred = true')
84 ; 70 ;
85 } 71 }
@@ -89,7 +75,7 @@ class EntryRepository extends EntityRepository
89 * 75 *
90 * @param int $userId 76 * @param int $userId
91 * @param string $term 77 * @param string $term
92 * @param strint $currentRoute 78 * @param string $currentRoute
93 * 79 *
94 * @return QueryBuilder 80 * @return QueryBuilder
95 */ 81 */
@@ -108,7 +94,7 @@ class EntryRepository extends EntityRepository
108 94
109 // We lower() all parts here because PostgreSQL 'LIKE' verb is case-sensitive 95 // We lower() all parts here because PostgreSQL 'LIKE' verb is case-sensitive
110 $qb 96 $qb
111 ->andWhere('lower(e.content) LIKE lower(:term) OR lower(e.title) LIKE lower(:term) OR lower(e.url) LIKE lower(:term)')->setParameter('term', '%'.$term.'%') 97 ->andWhere('lower(e.content) LIKE lower(:term) OR lower(e.title) LIKE lower(:term) OR lower(e.url) LIKE lower(:term)')->setParameter('term', '%' . $term . '%')
112 ->leftJoin('e.tags', 't') 98 ->leftJoin('e.tags', 't')
113 ->groupBy('e.id'); 99 ->groupBy('e.id');
114 100
@@ -135,34 +121,54 @@ class EntryRepository extends EntityRepository
135 * @param int $userId 121 * @param int $userId
136 * @param bool $isArchived 122 * @param bool $isArchived
137 * @param bool $isStarred 123 * @param bool $isStarred
124 * @param bool $isPublic
138 * @param string $sort 125 * @param string $sort
139 * @param string $order 126 * @param string $order
140 * @param int $since 127 * @param int $since
141 * @param string $tags 128 * @param string $tags
142 * 129 *
143 * @return array 130 * @return Pagerfanta
144 */ 131 */
145 public function findEntries($userId, $isArchived = null, $isStarred = null, $sort = 'created', $order = 'ASC', $since = 0, $tags = '') 132 public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'ASC', $since = 0, $tags = '')
146 { 133 {
147 $qb = $this->createQueryBuilder('e') 134 $qb = $this->createQueryBuilder('e')
148 ->leftJoin('e.tags', 't') 135 ->leftJoin('e.tags', 't')
149 ->where('e.user =:userId')->setParameter('userId', $userId); 136 ->where('e.user = :userId')->setParameter('userId', $userId);
150 137
151 if (null !== $isArchived) { 138 if (null !== $isArchived) {
152 $qb->andWhere('e.isArchived =:isArchived')->setParameter('isArchived', (bool) $isArchived); 139 $qb->andWhere('e.isArchived = :isArchived')->setParameter('isArchived', (bool) $isArchived);
153 } 140 }
154 141
155 if (null !== $isStarred) { 142 if (null !== $isStarred) {
156 $qb->andWhere('e.isStarred =:isStarred')->setParameter('isStarred', (bool) $isStarred); 143 $qb->andWhere('e.isStarred = :isStarred')->setParameter('isStarred', (bool) $isStarred);
144 }
145
146 if (null !== $isPublic) {
147 $qb->andWhere('e.uid IS ' . (true === $isPublic ? 'NOT' : '') . ' NULL');
157 } 148 }
158 149
159 if ($since > 0) { 150 if ($since > 0) {
160 $qb->andWhere('e.updatedAt > :since')->setParameter('since', new \DateTime(date('Y-m-d H:i:s', $since))); 151 $qb->andWhere('e.updatedAt > :since')->setParameter('since', new \DateTime(date('Y-m-d H:i:s', $since)));
161 } 152 }
162 153
163 if ('' !== $tags) { 154 if (is_string($tags) && '' !== $tags) {
164 foreach (explode(',', $tags) as $tag) { 155 foreach (explode(',', $tags) as $i => $tag) {
165 $qb->andWhere('t.label = :label')->setParameter('label', $tag); 156 $entryAlias = 'e' . $i;
157 $tagAlias = 't' . $i;
158
159 // Complexe queries to ensure multiple tags are associated to an entry
160 // https://stackoverflow.com/a/6638146/569101
161 $qb->andWhere($qb->expr()->in(
162 'e.id',
163 $this->createQueryBuilder($entryAlias)
164 ->select($entryAlias . '.id')
165 ->leftJoin($entryAlias . '.tags', $tagAlias)
166 ->where($tagAlias . '.label = :label' . $i)
167 ->getDQL()
168 ));
169
170 // bound parameter to the main query builder
171 $qb->setParameter('label' . $i, $tag);
166 } 172 }
167 } 173 }
168 174
@@ -182,7 +188,7 @@ class EntryRepository extends EntityRepository
182 * 188 *
183 * @param int $userId 189 * @param int $userId
184 * 190 *
185 * @return Entry 191 * @return array
186 */ 192 */
187 public function findOneWithTags($userId) 193 public function findOneWithTags($userId)
188 { 194 {
@@ -190,7 +196,7 @@ class EntryRepository extends EntityRepository
190 ->innerJoin('e.tags', 't') 196 ->innerJoin('e.tags', 't')
191 ->innerJoin('e.user', 'u') 197 ->innerJoin('e.user', 'u')
192 ->addSelect('t', 'u') 198 ->addSelect('t', 'u')
193 ->where('e.user=:userId')->setParameter('userId', $userId) 199 ->where('e.user = :userId')->setParameter('userId', $userId)
194 ; 200 ;
195 201
196 return $qb->getQuery()->getResult(); 202 return $qb->getQuery()->getResult();
@@ -328,47 +334,98 @@ class EntryRepository extends EntityRepository
328 * 334 *
329 * @return int 335 * @return int
330 */ 336 */
331 public function countAllEntriesByUsername($userId) 337 public function countAllEntriesByUser($userId)
332 { 338 {
333 $qb = $this->createQueryBuilder('e') 339 $qb = $this->createQueryBuilder('e')
334 ->select('count(e)') 340 ->select('count(e)')
335 ->where('e.user=:userId')->setParameter('userId', $userId) 341 ->where('e.user = :userId')->setParameter('userId', $userId)
336 ; 342 ;
337 343
338 return $qb->getQuery()->getSingleScalarResult(); 344 return (int) $qb->getQuery()->getSingleScalarResult();
339 } 345 }
340 346
341 /** 347 /**
342 * Count all entries for a tag and a user. 348 * Remove all entries for a user id.
349 * Used when a user want to reset all informations.
343 * 350 *
344 * @param int $userId 351 * @param int $userId
345 * @param int $tagId 352 */
353 public function removeAllByUserId($userId)
354 {
355 $this->getEntityManager()
356 ->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.user = :userId')
357 ->setParameter('userId', $userId)
358 ->execute();
359 }
360
361 public function removeArchivedByUserId($userId)
362 {
363 $this->getEntityManager()
364 ->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.user = :userId AND e.isArchived = TRUE')
365 ->setParameter('userId', $userId)
366 ->execute();
367 }
368
369 /**
370 * Get id and url from all entries
371 * Used for the clean-duplicates command.
372 */
373 public function findAllEntriesIdAndUrlByUserId($userId)
374 {
375 $qb = $this->createQueryBuilder('e')
376 ->select('e.id, e.url')
377 ->where('e.user = :userid')->setParameter(':userid', $userId);
378
379 return $qb->getQuery()->getArrayResult();
380 }
381
382 /**
383 * @param int $userId
346 * 384 *
347 * @return int 385 * @return array
348 */ 386 */
349 public function countAllEntriesByUserIdAndTagId($userId, $tagId) 387 public function findAllEntriesIdByUserId($userId = null)
350 { 388 {
351 $qb = $this->createQueryBuilder('e') 389 $qb = $this->createQueryBuilder('e')
352 ->select('count(e.id)') 390 ->select('e.id');
353 ->leftJoin('e.tags', 't')
354 ->where('e.user=:userId')->setParameter('userId', $userId)
355 ->andWhere('t.id=:tagId')->setParameter('tagId', $tagId)
356 ;
357 391
358 return $qb->getQuery()->getSingleScalarResult(); 392 if (null !== $userId) {
393 $qb->where('e.user = :userid')->setParameter(':userid', $userId);
394 }
395
396 return $qb->getQuery()->getArrayResult();
359 } 397 }
360 398
361 /** 399 /**
362 * Remove all entries for a user id. 400 * Find all entries by url and owner.
363 * Used when a user want to reset all informations.
364 * 401 *
365 * @param int $userId 402 * @param $url
403 * @param $userId
404 *
405 * @return array
366 */ 406 */
367 public function removeAllByUserId($userId) 407 public function findAllByUrlAndUserId($url, $userId)
368 { 408 {
369 $this->getEntityManager() 409 return $this->createQueryBuilder('e')
370 ->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.user = :userId') 410 ->where('e.url = :url')->setParameter('url', urldecode($url))
371 ->setParameter('userId', $userId) 411 ->andWhere('e.user = :user_id')->setParameter('user_id', $userId)
372 ->execute(); 412 ->getQuery()
413 ->getResult();
414 }
415
416 /**
417 * Return a query builder to used by other getBuilderFor* method.
418 *
419 * @param int $userId
420 * @param string $sortBy
421 * @param string $direction
422 *
423 * @return QueryBuilder
424 */
425 private function getBuilderByUser($userId, $sortBy = 'createdAt', $direction = 'desc')
426 {
427 return $this->createQueryBuilder('e')
428 ->andWhere('e.user = :userId')->setParameter('userId', $userId)
429 ->orderBy(sprintf('e.%s', $sortBy), $direction);
373 } 430 }
374} 431}
diff --git a/src/Wallabag/CoreBundle/Repository/SiteCredentialRepository.php b/src/Wallabag/CoreBundle/Repository/SiteCredentialRepository.php
new file mode 100644
index 00000000..36906761
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Repository/SiteCredentialRepository.php
@@ -0,0 +1,47 @@
1<?php
2
3namespace Wallabag\CoreBundle\Repository;
4
5use Wallabag\CoreBundle\Helper\CryptoProxy;
6
7/**
8 * SiteCredentialRepository.
9 */
10class SiteCredentialRepository extends \Doctrine\ORM\EntityRepository
11{
12 private $cryptoProxy;
13
14 public function setCrypto(CryptoProxy $cryptoProxy)
15 {
16 $this->cryptoProxy = $cryptoProxy;
17 }
18
19 /**
20 * Retrieve one username/password for the given host and userId.
21 *
22 * @param string $host
23 * @param int $userId
24 *
25 * @return null|array
26 */
27 public function findOneByHostAndUser($host, $userId)
28 {
29 $res = $this->createQueryBuilder('s')
30 ->select('s.username', 's.password')
31 ->where('s.host = :hostname')->setParameter('hostname', $host)
32 ->andWhere('s.user = :userId')->setParameter('userId', $userId)
33 ->setMaxResults(1)
34 ->getQuery()
35 ->getOneOrNullResult();
36
37 if (null === $res) {
38 return;
39 }
40
41 // decrypt user & password before returning them
42 $res['username'] = $this->cryptoProxy->decrypt($res['username']);
43 $res['password'] = $this->cryptoProxy->decrypt($res['password']);
44
45 return $res;
46 }
47}
diff --git a/src/Wallabag/CoreBundle/Repository/TagRepository.php b/src/Wallabag/CoreBundle/Repository/TagRepository.php
index 2182df25..5c45211f 100644
--- a/src/Wallabag/CoreBundle/Repository/TagRepository.php
+++ b/src/Wallabag/CoreBundle/Repository/TagRepository.php
@@ -3,6 +3,7 @@
3namespace Wallabag\CoreBundle\Repository; 3namespace Wallabag\CoreBundle\Repository;
4 4
5use Doctrine\ORM\EntityRepository; 5use Doctrine\ORM\EntityRepository;
6use Wallabag\CoreBundle\Entity\Tag;
6 7
7class TagRepository extends EntityRepository 8class TagRepository extends EntityRepository
8{ 9{
@@ -62,6 +63,27 @@ class TagRepository extends EntityRepository
62 } 63 }
63 64
64 /** 65 /**
66 * Find all tags (flat) per user with nb entries.
67 *
68 * @param int $userId
69 *
70 * @return array
71 */
72 public function findAllFlatTagsWithNbEntries($userId)
73 {
74 return $this->createQueryBuilder('t')
75 ->select('t.id, t.label, t.slug, count(e.id) as nbEntries')
76 ->distinct(true)
77 ->leftJoin('t.entries', 'e')
78 ->where('e.user = :userId')
79 ->groupBy('t.id')
80 ->orderBy('t.slug')
81 ->setParameter('userId', $userId)
82 ->getQuery()
83 ->getArrayResult();
84 }
85
86 /**
65 * Used only in test case to get a tag for our entry. 87 * Used only in test case to get a tag for our entry.
66 * 88 *
67 * @return Tag 89 * @return Tag
@@ -76,4 +98,24 @@ class TagRepository extends EntityRepository
76 ->getQuery() 98 ->getQuery()
77 ->getSingleResult(); 99 ->getSingleResult();
78 } 100 }
101
102 public function findForArchivedArticlesByUser($userId)
103 {
104 $ids = $this->createQueryBuilder('t')
105 ->select('t.id')
106 ->leftJoin('t.entries', 'e')
107 ->where('e.user = :userId')->setParameter('userId', $userId)
108 ->andWhere('e.isArchived = true')
109 ->groupBy('t.id')
110 ->orderBy('t.slug')
111 ->getQuery()
112 ->getArrayResult();
113
114 $tags = [];
115 foreach ($ids as $id) {
116 $tags[] = $this->find($id);
117 }
118
119 return $tags;
120 }
79} 121}
diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml
index 51d6ab47..31b16739 100644
--- a/src/Wallabag/CoreBundle/Resources/config/services.yml
+++ b/src/Wallabag/CoreBundle/Resources/config/services.yml
@@ -41,6 +41,7 @@ services:
41 arguments: 41 arguments:
42 - 42 -
43 error_message: '%wallabag_core.fetching_error_message%' 43 error_message: '%wallabag_core.fetching_error_message%'
44 error_message_title: '%wallabag_core.fetching_error_message_title%'
44 - "@wallabag_core.guzzle.http_client" 45 - "@wallabag_core.guzzle.http_client"
45 - "@wallabag_core.graby.config_builder" 46 - "@wallabag_core.graby.config_builder"
46 calls: 47 calls:
@@ -62,7 +63,11 @@ services:
62 class: Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder 63 class: Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder
63 arguments: 64 arguments:
64 - "@wallabag_core.graby.config_builder" 65 - "@wallabag_core.graby.config_builder"
65 - "%sites_credentials%" 66 - "@security.token_storage"
67 - "@wallabag_core.site_credential_repository"
68 - '@logger'
69 tags:
70 - { name: monolog.logger, channel: graby }
66 71
67 # service alias override 72 # service alias override
68 bd_guzzle_site_authenticator.site_config_builder: 73 bd_guzzle_site_authenticator.site_config_builder:
@@ -71,10 +76,11 @@ services:
71 wallabag_core.guzzle.http_client_factory: 76 wallabag_core.guzzle.http_client_factory:
72 class: Wallabag\CoreBundle\Helper\HttpClientFactory 77 class: Wallabag\CoreBundle\Helper\HttpClientFactory
73 arguments: 78 arguments:
74 - "@bd_guzzle_site_authenticator.authenticator_subscriber"
75 - "@wallabag_core.guzzle.cookie_jar" 79 - "@wallabag_core.guzzle.cookie_jar"
76 - '@=service(''craue_config'').get(''restricted_access'')' 80 - '@=service(''craue_config'').get(''restricted_access'')'
77 - '@logger' 81 - '@logger'
82 calls:
83 - ["addSubscriber", ["@bd_guzzle_site_authenticator.authenticator_subscriber"]]
78 84
79 wallabag_core.guzzle.cookie_jar: 85 wallabag_core.guzzle.cookie_jar:
80 class: GuzzleHttp\Cookie\FileCookieJar 86 class: GuzzleHttp\Cookie\FileCookieJar
@@ -85,16 +91,22 @@ services:
85 arguments: 91 arguments:
86 - "@wallabag_core.graby" 92 - "@wallabag_core.graby"
87 - "@wallabag_core.rule_based_tagger" 93 - "@wallabag_core.rule_based_tagger"
88 - "@wallabag_core.tag_repository" 94 - "@validator"
89 - "@logger" 95 - "@logger"
90 - '%wallabag_core.fetching_error_message%' 96 - '%wallabag_core.fetching_error_message%'
91 97
98 wallabag_core.tags_assigner:
99 class: Wallabag\CoreBundle\Helper\TagsAssigner
100 arguments:
101 - "@wallabag_core.tag_repository"
102
92 wallabag_core.rule_based_tagger: 103 wallabag_core.rule_based_tagger:
93 class: Wallabag\CoreBundle\Helper\RuleBasedTagger 104 class: Wallabag\CoreBundle\Helper\RuleBasedTagger
94 arguments: 105 arguments:
95 - "@rulerz" 106 - "@rulerz"
96 - "@wallabag_core.tag_repository" 107 - "@wallabag_core.tag_repository"
97 - "@wallabag_core.entry_repository" 108 - "@wallabag_core.entry_repository"
109 - "@logger"
98 110
99 # repository as a service 111 # repository as a service
100 wallabag_core.entry_repository: 112 wallabag_core.entry_repository:
@@ -109,10 +121,19 @@ services:
109 arguments: 121 arguments:
110 - WallabagCoreBundle:Tag 122 - WallabagCoreBundle:Tag
111 123
124 wallabag_core.site_credential_repository:
125 class: Wallabag\CoreBundle\Repository\SiteCredentialRepository
126 factory: [ "@doctrine.orm.default_entity_manager", getRepository ]
127 arguments:
128 - WallabagCoreBundle:SiteCredential
129 calls:
130 - [ setCrypto, [ "@wallabag_core.helper.crypto_proxy" ] ]
131
112 wallabag_core.helper.entries_export: 132 wallabag_core.helper.entries_export:
113 class: Wallabag\CoreBundle\Helper\EntriesExport 133 class: Wallabag\CoreBundle\Helper\EntriesExport
114 arguments: 134 arguments:
115 - '@=service(''craue_config'').get(''wallabag_url'')' 135 - "@translator"
136 - '%domain_name%'
116 - src/Wallabag/CoreBundle/Resources/public/themes/_global/img/appicon/apple-touch-icon-152.png 137 - src/Wallabag/CoreBundle/Resources/public/themes/_global/img/appicon/apple-touch-icon-152.png
117 138
118 wallabag.operator.array.matches: 139 wallabag.operator.array.matches:
@@ -125,6 +146,16 @@ services:
125 tags: 146 tags:
126 - { name: rulerz.operator, target: doctrine, operator: matches, inline: true } 147 - { name: rulerz.operator, target: doctrine, operator: matches, inline: true }
127 148
149 wallabag.operator.array.notmatches:
150 class: Wallabag\CoreBundle\Operator\PHP\NotMatches
151 tags:
152 - { name: rulerz.operator, target: native, operator: notmatches }
153
154 wallabag.operator.doctrine.notmatches:
155 class: Wallabag\CoreBundle\Operator\Doctrine\NotMatches
156 tags:
157 - { name: rulerz.operator, target: doctrine, operator: notmatches, inline: true }
158
128 wallabag_core.helper.redirect: 159 wallabag_core.helper.redirect:
129 class: Wallabag\CoreBundle\Helper\Redirect 160 class: Wallabag\CoreBundle\Helper\Redirect
130 arguments: 161 arguments:
@@ -174,9 +205,15 @@ services:
174 class: Wallabag\CoreBundle\Helper\DownloadImages 205 class: Wallabag\CoreBundle\Helper\DownloadImages
175 arguments: 206 arguments:
176 - "@wallabag_core.entry.download_images.client" 207 - "@wallabag_core.entry.download_images.client"
177 - "%kernel.root_dir%/../web/assets/images" 208 - "%kernel.project_dir%/web/assets/images"
178 - '@=service(''craue_config'').get(''wallabag_url'')' 209 - '%domain_name%'
179 - "@logger" 210 - "@logger"
180 211
181 wallabag_core.entry.download_images.client: 212 wallabag_core.entry.download_images.client:
182 class: GuzzleHttp\Client 213 class: GuzzleHttp\Client
214
215 wallabag_core.helper.crypto_proxy:
216 class: Wallabag\CoreBundle\Helper\CryptoProxy
217 arguments:
218 - "%wallabag_core.site_credentials.encryption_key_path%"
219 - "@logger"
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
index a52b579a..d0a38f7e 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
@@ -32,6 +32,7 @@ menu:
32 # save_link: 'Save a link' 32 # save_link: 'Save a link'
33 back_to_unread: 'Tilbage til de ulæste artikler' 33 back_to_unread: 'Tilbage til de ulæste artikler'
34 # users_management: 'Users management' 34 # users_management: 'Users management'
35 # site_credentials: 'Site credentials'
35 top: 36 top:
36 add_new_entry: 'Tilføj ny artikel' 37 add_new_entry: 'Tilføj ny artikel'
37 search: 'Søg' 38 search: 'Søg'
@@ -76,6 +77,7 @@ config:
76 # redirect_current_page: 'To the current page' 77 # redirect_current_page: 'To the current page'
77 pocket_consumer_key_label: Brugers nøgle til Pocket for at importere materialer 78 pocket_consumer_key_label: Brugers nøgle til Pocket for at importere materialer
78 # android_configuration: Configure your Android application 79 # android_configuration: Configure your Android application
80 # android_instruction: "Touch here to prefill your Android application"
79 # help_theme: "wallabag is customizable. You can choose your prefered theme here." 81 # help_theme: "wallabag is customizable. You can choose your prefered theme here."
80 # help_items_per_page: "You can change the number of articles displayed on each page." 82 # help_items_per_page: "You can change the number of articles displayed on each page."
81 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article." 83 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article."
@@ -89,9 +91,10 @@ config:
89 token_reset: 'Nulstil token' 91 token_reset: 'Nulstil token'
90 rss_links: 'RSS-Links' 92 rss_links: 'RSS-Links'
91 rss_link: 93 rss_link:
92 unread: 'ulæst' 94 unread: 'Ulæst'
93 starred: 'favoritter' 95 starred: 'Favoritter'
94 archive: 'arkiv' 96 archive: 'Arkiv'
97 # all: 'All'
95 # rss_limit: 'Number of items in the feed' 98 # rss_limit: 'Number of items in the feed'
96 form_user: 99 form_user:
97 # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connexion" 100 # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connexion"
@@ -110,6 +113,7 @@ config:
110 # annotations: Remove ALL annotations 113 # annotations: Remove ALL annotations
111 # tags: Remove ALL tags 114 # tags: Remove ALL tags
112 # entries: Remove ALL entries 115 # entries: Remove ALL entries
116 # archived: Remove ALL archived entries
113 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE) 117 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
114 form_password: 118 form_password:
115 # description: "You can change your password here. Your new password should by at least 8 characters long." 119 # description: "You can change your password here. Your new password should by at least 8 characters long."
@@ -154,6 +158,7 @@ config:
154 # or: 'One rule OR another' 158 # or: 'One rule OR another'
155 # and: 'One rule AND another' 159 # and: 'One rule AND another'
156 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>' 160 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
161 # notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 # filtered_tags: 'Filtered by tags:' 169 # filtered_tags: 'Filtered by tags:'
165 # filtered_search: 'Filtered by search:' 170 # filtered_search: 'Filtered by search:'
166 # untagged: 'Untagged entries' 171 # untagged: 'Untagged entries'
172 # all: 'All entries'
167 list: 173 list:
168 # number_on_the_page: '{0} There is no entry.|{1} There is one entry.|]1,Inf[ There are %count% entries.' 174 # number_on_the_page: '{0} There is no entry.|{1} There is one entry.|]1,Inf[ There are %count% entries.'
169 reading_time: 'estimeret læsetid' 175 reading_time: 'estimeret læsetid'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'Ulæst' 191 unread_label: 'Ulæst'
186 preview_picture_label: 'Har et vist billede' 192 preview_picture_label: 'Har et vist billede'
187 preview_picture_help: 'Forhåndsvis billede' 193 preview_picture_help: 'Forhåndsvis billede'
194 # is_public_label: 'Has a public link'
195 # is_public_help: 'Public link'
188 language_label: 'Sprog' 196 language_label: 'Sprog'
189 # http_status_label: 'HTTP status' 197 # http_status_label: 'HTTP status'
190 reading_time: 198 reading_time:
@@ -223,6 +231,8 @@ entry:
223 original_article: 'original' 231 original_article: 'original'
224 # annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations' 232 # annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations'
225 created_at: 'Oprettelsesdato' 233 created_at: 'Oprettelsesdato'
234 # published_at: 'Publication date'
235 # published_by: 'Published by'
226 new: 236 new:
227 page_title: 'Gem ny artikel' 237 page_title: 'Gem ny artikel'
228 placeholder: 'http://website.com' 238 placeholder: 'http://website.com'
@@ -234,10 +244,12 @@ entry:
234 # page_title: 'Edit an entry' 244 # page_title: 'Edit an entry'
235 # title_label: 'Title' 245 # title_label: 'Title'
236 url_label: 'Url' 246 url_label: 'Url'
237 # is_public_label: 'Public'
238 save_label: 'Gem' 247 save_label: 'Gem'
239 public: 248 public:
240 # shared_by_wallabag: "This article has been shared by <a href=%wallabag_instance%'>wallabag</a>" 249 # shared_by_wallabag: "This article has been shared by %username% with <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 # delete: "Are you sure you want to remove that article?"
252 # delete_tag: "Are you sure you want to remove that tag from that article?"
241 253
242about: 254about:
243 page_title: 'Om' 255 page_title: 'Om'
@@ -385,6 +397,9 @@ tag:
385 # add: 'Add' 397 # add: 'Add'
386 # placeholder: 'You can add several tags, separated by a comma.' 398 # placeholder: 'You can add several tags, separated by a comma.'
387 399
400# export:
401# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
388import: 403import:
389 # page_title: 'Import' 404 # page_title: 'Import'
390 # page_description: 'Welcome to wallabag importer. Please select your previous service that you want to migrate.' 405 # page_description: 'Welcome to wallabag importer. Please select your previous service that you want to migrate.'
@@ -510,6 +525,28 @@ user:
510 # delete: Delete 525 # delete: Delete
511 # delete_confirm: Are you sure? 526 # delete_confirm: Are you sure?
512 # back_to_list: Back to list 527 # back_to_list: Back to list
528 search:
529 # placeholder: Filter by username or email
530
531site_credential:
532 # page_title: Site credentials management
533 # new_site_credential: Create a credential
534 # edit_site_credential: Edit an existing credential
535 # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc."
536 # list:
537 # actions: Actions
538 # edit_action: Edit
539 # yes: Yes
540 # no: No
541 # create_new_one: Create a new credential
542 # form:
543 # username_label: 'Username'
544 # host_label: 'Host'
545 # password_label: 'Password'
546 # save: Save
547 # delete: Delete
548 # delete_confirm: Are you sure?
549 # back_to_list: Back to list
513 550
514error: 551error:
515 # page_title: An error occurred 552 # page_title: An error occurred
@@ -528,6 +565,7 @@ flashes:
528 # annotations_reset: Annotations reset 565 # annotations_reset: Annotations reset
529 # tags_reset: Tags reset 566 # tags_reset: Tags reset
530 # entries_reset: Entries reset 567 # entries_reset: Entries reset
568 # archived_reset: Archived entries deleted
531 entry: 569 entry:
532 notice: 570 notice:
533 # entry_already_saved: 'Entry already saved on %date%' 571 # entry_already_saved: 'Entry already saved on %date%'
@@ -562,3 +600,8 @@ flashes:
562 # added: 'User "%username%" added' 600 # added: 'User "%username%" added'
563 # updated: 'User "%username%" updated' 601 # updated: 'User "%username%" updated'
564 # deleted: 'User "%username%" deleted' 602 # deleted: 'User "%username%" deleted'
603 site_credential:
604 notice:
605 # added: 'Site credential for "%host%" added'
606 # updated: 'Site credential for "%host%" updated'
607 # deleted: 'Site credential for "%host%" deleted'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
index 35cb4b5b..158762a9 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
@@ -32,6 +32,7 @@ menu:
32 save_link: 'Link speichern' 32 save_link: 'Link speichern'
33 back_to_unread: 'Zurück zu ungelesenen Artikeln' 33 back_to_unread: 'Zurück zu ungelesenen Artikeln'
34 users_management: 'Benutzerverwaltung' 34 users_management: 'Benutzerverwaltung'
35 site_credentials: 'Zugangsdaten'
35 top: 36 top:
36 add_new_entry: 'Neuen Artikel hinzufügen' 37 add_new_entry: 'Neuen Artikel hinzufügen'
37 search: 'Suche' 38 search: 'Suche'
@@ -46,7 +47,7 @@ footer:
46 social: 'Soziales' 47 social: 'Soziales'
47 powered_by: 'angetrieben von' 48 powered_by: 'angetrieben von'
48 about: 'Über' 49 about: 'Über'
49 stats: Seit %user_creation% hast du %nb_archives% Artikel gelesen. Das sind ungefähr %per_day% pro Tag! 50 stats: 'Seit %user_creation% hast du %nb_archives% Artikel gelesen. Das sind ungefähr %per_day% pro Tag!'
50 51
51config: 52config:
52 page_title: 'Einstellungen' 53 page_title: 'Einstellungen'
@@ -74,8 +75,9 @@ config:
74 label: 'Wohin soll nach dem Gelesenmarkieren eines Artikels weitergeleitet werden?' 75 label: 'Wohin soll nach dem Gelesenmarkieren eines Artikels weitergeleitet werden?'
75 redirect_homepage: 'Zur Homepage' 76 redirect_homepage: 'Zur Homepage'
76 redirect_current_page: 'Zur aktuellen Seite' 77 redirect_current_page: 'Zur aktuellen Seite'
77 pocket_consumer_key_label: Consumer-Key für Pocket, um Inhalte zu importieren 78 pocket_consumer_key_label: 'Consumer-Key für Pocket, um Inhalte zu importieren'
78 android_configuration: Konfiguriere deine Android Application 79 android_configuration: 'Konfiguriere deine Android-App'
80 android_instruction: "Klicke hier, um deine Android-App auszufüllen"
79 help_theme: "wallabag ist anpassbar. Du kannst dein bevorzugtes Theme hier auswählen." 81 help_theme: "wallabag ist anpassbar. Du kannst dein bevorzugtes Theme hier auswählen."
80 help_items_per_page: "Du kannst die Nummer von Artikeln pro Seite anpassen." 82 help_items_per_page: "Du kannst die Nummer von Artikeln pro Seite anpassen."
81 help_reading_speed: "wallabag berechnet eine Lesezeit pro Artikel. Hier kannst du definieren, ob du ein schneller oder langsamer Leser bist. wallabag wird die Lesezeiten danach neu berechnen." 83 help_reading_speed: "wallabag berechnet eine Lesezeit pro Artikel. Hier kannst du definieren, ob du ein schneller oder langsamer Leser bist. wallabag wird die Lesezeiten danach neu berechnen."
@@ -92,6 +94,7 @@ config:
92 unread: 'Ungelesene' 94 unread: 'Ungelesene'
93 starred: 'Favoriten' 95 starred: 'Favoriten'
94 archive: 'Archivierte' 96 archive: 'Archivierte'
97 all: 'Alle'
95 rss_limit: 'Anzahl der Einträge pro Feed' 98 rss_limit: 'Anzahl der Einträge pro Feed'
96 form_user: 99 form_user:
97 two_factor_description: "Wenn du die Zwei-Faktor-Authentifizierung aktivierst, erhältst du eine E-Mail mit einem Code bei jeder nicht vertrauenswürdigen Verbindung" 100 two_factor_description: "Wenn du die Zwei-Faktor-Authentifizierung aktivierst, erhältst du eine E-Mail mit einem Code bei jeder nicht vertrauenswürdigen Verbindung"
@@ -100,17 +103,18 @@ config:
100 twoFactorAuthentication_label: 'Zwei-Faktor-Authentifizierung' 103 twoFactorAuthentication_label: 'Zwei-Faktor-Authentifizierung'
101 help_twoFactorAuthentication: "Wenn du 2FA aktivierst, wirst du bei jedem Login einen Code per E-Mail bekommen." 104 help_twoFactorAuthentication: "Wenn du 2FA aktivierst, wirst du bei jedem Login einen Code per E-Mail bekommen."
102 delete: 105 delete:
103 title: Lösche mein Konto (a.k.a Gefahrenzone) 106 title: 'Lösche mein Konto (a.k.a Gefahrenzone)'
104 description: Wenn du dein Konto löschst, werden ALL deine Artikel, ALL deine Tags, ALL deine Anmerkungen und dein Konto dauerhaft gelöscht (kann NICHT RÜCKGÄNGIG gemacht werden). Du wirst anschließend ausgeloggt. 107 description: 'Wenn du dein Konto löschst, werden ALL deine Artikel, ALL deine Tags, ALL deine Anmerkungen und dein Konto dauerhaft gelöscht (kann NICHT RÜCKGÄNGIG gemacht werden). Du wirst anschließend ausgeloggt.'
105 confirm: Bist du wirklich sicher? (DIES KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN) 108 confirm: 'Bist du wirklich sicher? (DIES KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN)'
106 button: Lösche mein Konto 109 button: 'Lösche mein Konto'
107 reset: 110 reset:
108 title: Zurücksetzen (a.k.a Gefahrenzone) 111 title: 'Zurücksetzen (a.k.a Gefahrenzone)'
109 description: Beim Nutzen der folgenden Schaltflächenhast du die Möglichkeit, einige Informationen von deinem Konto zu entfernen. Sei dir bewusst, dass dies NICHT RÜCKGÄNGIG zu machen ist. 112 description: 'Beim Nutzen der folgenden Schaltflächenhast du die Möglichkeit, einige Informationen von deinem Konto zu entfernen. Sei dir bewusst, dass dies NICHT RÜCKGÄNGIG zu machen ist.'
110 annotations: Entferne ALLE Annotationen 113 annotations: 'Entferne ALLE Annotationen'
111 tags: Entferne ALLE Tags 114 tags: 'Entferne ALLE Tags'
112 entries: Entferne ALLE Einträge 115 entries: 'Entferne ALLE Einträge'
113 confirm: Bist du wirklich sicher? (DIES KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN) 116 archived: 'Entferne ALLE archivierten Einträge'
117 confirm: 'Bist du wirklich sicher? (DIES KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN)'
114 form_password: 118 form_password:
115 description: "Hier kannst du dein Kennwort ändern. Dieses sollte mindestens acht Zeichen enthalten." 119 description: "Hier kannst du dein Kennwort ändern. Dieses sollte mindestens acht Zeichen enthalten."
116 old_password_label: 'Altes Kennwort' 120 old_password_label: 'Altes Kennwort'
@@ -154,6 +158,7 @@ config:
154 or: 'Eine Regel ODER die andere' 158 or: 'Eine Regel ODER die andere'
155 and: 'Eine Regel UND eine andere' 159 and: 'Eine Regel UND eine andere'
156 matches: 'Testet, ob eine <i>Variable</i> auf eine <i>Suche</i> zutrifft (Groß- und Kleinschreibung wird nicht berücksichtigt).<br />Beispiel: <code>title matches "Fußball"</code>' 160 matches: 'Testet, ob eine <i>Variable</i> auf eine <i>Suche</i> zutrifft (Groß- und Kleinschreibung wird nicht berücksichtigt).<br />Beispiel: <code>title matches "Fußball"</code>'
161 notmatches: 'Testet, ob ein <i>Titel</i> nicht auf eine <i>Suche</i> zutrifft (Groß- und Kleinschreibung wird nicht berücksichtigt).<br />Beispiel: <code>title notmatches "Fußball"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 filtered_tags: 'Gefiltert nach Tags:' 169 filtered_tags: 'Gefiltert nach Tags:'
165 filtered_search: 'Gefiltert nach Suche:' 170 filtered_search: 'Gefiltert nach Suche:'
166 untagged: 'Nicht getaggte Einträge' 171 untagged: 'Nicht getaggte Einträge'
172 all: 'Alle Einträge'
167 list: 173 list:
168 number_on_the_page: '{0} Es gibt keine Einträge.|{1} Es gibt einen Eintrag.|]1,Inf[ Es gibt %count% Einträge.' 174 number_on_the_page: '{0} Es gibt keine Einträge.|{1} Es gibt einen Eintrag.|]1,Inf[ Es gibt %count% Einträge.'
169 reading_time: 'geschätzte Lesezeit' 175 reading_time: 'geschätzte Lesezeit'
@@ -172,7 +178,7 @@ entry:
172 number_of_tags: '{1}und ein anderer Tag|]1,Inf[und %count% andere Tags' 178 number_of_tags: '{1}und ein anderer Tag|]1,Inf[und %count% andere Tags'
173 reading_time_minutes_short: '%readingTime% min' 179 reading_time_minutes_short: '%readingTime% min'
174 reading_time_less_one_minute_short: '&lt; 1 min' 180 reading_time_less_one_minute_short: '&lt; 1 min'
175 original_article: 'Original' 181 original_article: 'Originalartikel'
176 toogle_as_read: 'Gelesen-Status ändern' 182 toogle_as_read: 'Gelesen-Status ändern'
177 toogle_as_star: 'Favoriten-Status ändern' 183 toogle_as_star: 'Favoriten-Status ändern'
178 delete: 'Löschen' 184 delete: 'Löschen'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'Ungelesene' 191 unread_label: 'Ungelesene'
186 preview_picture_label: 'Vorschaubild vorhanden' 192 preview_picture_label: 'Vorschaubild vorhanden'
187 preview_picture_help: 'Vorschaubild' 193 preview_picture_help: 'Vorschaubild'
194 is_public_label: 'Hat einen öffentlichen Link'
195 is_public_help: 'Öffentlicher Link'
188 language_label: 'Sprache' 196 language_label: 'Sprache'
189 http_status_label: 'HTTP-Status' 197 http_status_label: 'HTTP-Status'
190 reading_time: 198 reading_time:
@@ -220,9 +228,11 @@ entry:
220 label: 'Probleme?' 228 label: 'Probleme?'
221 description: 'Erscheint dieser Artikel falsch?' 229 description: 'Erscheint dieser Artikel falsch?'
222 edit_title: 'Titel ändern' 230 edit_title: 'Titel ändern'
223 original_article: 'original' 231 original_article: 'Originalartikel'
224 annotations_on_the_entry: '{0} Keine Anmerkungen|{1} Eine Anmerkung|]1,Inf[ %count% Anmerkungen' 232 annotations_on_the_entry: '{0} Keine Anmerkungen|{1} Eine Anmerkung|]1,Inf[ %count% Anmerkungen'
225 created_at: 'Erstellungsdatum' 233 created_at: 'Erstellungsdatum'
234 published_at: 'Erscheinungsdatum'
235 published_by: 'Veröffentlicht von'
226 new: 236 new:
227 page_title: 'Neuen Artikel speichern' 237 page_title: 'Neuen Artikel speichern'
228 placeholder: 'https://website.de' 238 placeholder: 'https://website.de'
@@ -234,10 +244,12 @@ entry:
234 page_title: 'Eintrag bearbeiten' 244 page_title: 'Eintrag bearbeiten'
235 title_label: 'Titel' 245 title_label: 'Titel'
236 url_label: 'URL' 246 url_label: 'URL'
237 is_public_label: 'Öffentlich'
238 save_label: 'Speichern' 247 save_label: 'Speichern'
239 public: 248 public:
240 shared_by_wallabag: "Dieser Artikel wurde mittels <a href='%wallabag_instance%'>wallabag</a> geteilt" 249 shared_by_wallabag: 'Dieser Artikel wurde von %username% mittels <a href="%wallabag_instance%">wallabag</a> geteilt'
250 confirm:
251 delete: 'Bist du sicher, dass du diesen Artikel löschen möchtest?'
252 delete_tag: 'Bist du sicher, dass du diesen Tag vom Artikel entfernen möchtest?'
241 253
242about: 254about:
243 page_title: 'Über' 255 page_title: 'Über'
@@ -295,32 +307,32 @@ howto:
295 bookmarklet: 307 bookmarklet:
296 description: 'Ziehe diesen Link in deine Lesezeichenleiste:' 308 description: 'Ziehe diesen Link in deine Lesezeichenleiste:'
297 shortcuts: 309 shortcuts:
298 page_description: Dies sind die verfügbaren Tastenkürzel in wallabag. 310 page_description: 'Dies sind die verfügbaren Tastenkürzel in wallabag.'
299 shortcut: Tastenkürzel 311 shortcut: 'Tastenkürzel'
300 action: Aktion 312 action: 'Aktion'
301 all_pages_title: Tastenkürzel auf allen Seiten 313 all_pages_title: 'Tastenkürzel auf allen Seiten'
302 go_unread: Zu ungelesen gehen 314 go_unread: 'Zu ungelesen gehen'
303 go_starred: Zu Favoriten gehen 315 go_starred: 'Zu Favoriten gehen'
304 go_archive: Zu archivierten gehen 316 go_archive: 'Zu archivierten gehen'
305 go_all: Zu allen Artikel gehen 317 go_all: 'Zu allen Artikel gehen'
306 go_tags: Zu den Tags gehen 318 go_tags: 'Zu den Tags gehen'
307 go_config: Einstellungen öffnen 319 go_config: 'Einstellungen öffnen'
308 go_import: Import-Sektion öffnen 320 go_import: 'Import-Sektion öffnen'
309 go_developers: Zur Entwickler-Seite gehen 321 go_developers: 'Zur Entwickler-Seite gehen'
310 go_howto: Zum Howto gehen (diese Seite!) 322 go_howto: 'Zum Howto gehen (diese Seite!)'
311 go_logout: Abmelden 323 go_logout: 'Abmelden'
312 list_title: Tastenkürzel verfügbar auf den Listen-Seiten 324 list_title: 'Tastenkürzel verfügbar auf den Listen-Seiten'
313 search: Suche 325 search: 'Suche'
314 article_title: Tastenkürzel verfügbar auf der Artikel-Seite 326 article_title: 'Tastenkürzel verfügbar auf der Artikel-Seite'
315 open_original: Original-Artikel öffnen 327 open_original: 'Original-Artikel öffnen'
316 toggle_favorite: Favorit-Status für den Artikel ändern 328 toggle_favorite: 'Favorit-Status für den Artikel ändern'
317 toggle_archive: Archiviert-Status für den Artikel ändern 329 toggle_archive: 'Archiviert-Status für den Artikel ändern'
318 delete: Artikel löschen 330 delete: 'Artikel löschen'
319 material_title: Tastenkürzel des Material-Theme 331 material_title: 'Tastenkürzel des Material-Theme'
320 add_link: Neuen Link hinzufügen 332 add_link: 'Neuen Link hinzufügen'
321 hide_form: Aktuelles Formular verstecken (Suche oder neuer Link) 333 hide_form: 'Aktuelles Formular verstecken (Suche oder neuer Link)'
322 arrows_navigation: Durch Artikel navigieren 334 arrows_navigation: 'Durch Artikel navigieren'
323 open_article: Gewählten Artikel anzeigen 335 open_article: 'Gewählten Artikel anzeigen'
324 336
325quickstart: 337quickstart:
326 page_title: 'Schnelleinstieg' 338 page_title: 'Schnelleinstieg'
@@ -385,6 +397,9 @@ tag:
385 add: 'Hinzufügen' 397 add: 'Hinzufügen'
386 placeholder: 'Du kannst verschiedene Tags, getrennt von einem Komma, hinzufügen.' 398 placeholder: 'Du kannst verschiedene Tags, getrennt von einem Komma, hinzufügen.'
387 399
400export:
401 footer_template: '<div style="text-align:center;"><p>Generiert von wallabag mit Hilfe von %method%</p><p>Bitte öffne <a href="https://github.com/wallabag/wallabag/issues">ein Ticket</a> wenn du ein Problem mit der Darstellung von diesem E-Book auf deinem Gerät hast.</p></div>'
402
388import: 403import:
389 page_title: 'Importieren' 404 page_title: 'Importieren'
390 page_description: 'Willkommen beim wallabag-Importer. Wähle deinen vorherigen Service aus, von dem du die Daten migrieren willst.' 405 page_description: 'Willkommen beim wallabag-Importer. Wähle deinen vorherigen Service aus, von dem du die Daten migrieren willst.'
@@ -471,7 +486,7 @@ developer:
471 field_id: 'Client-ID' 486 field_id: 'Client-ID'
472 field_secret: 'Client-Secret' 487 field_secret: 'Client-Secret'
473 back: 'Zurück' 488 back: 'Zurück'
474 read_howto: 'Lese des How-To zu "Wie erstelle ich meine erste Anwendung"' 489 read_howto: 'Lese das How-To zu "Wie erstelle ich meine erste Anwendung"'
475 howto: 490 howto:
476 page_title: 'API-Client-Verwaltung > Wie erstelle ich meine erste Anwendung' 491 page_title: 'API-Client-Verwaltung > Wie erstelle ich meine erste Anwendung'
477 description: 492 description:
@@ -486,16 +501,16 @@ developer:
486 back: 'Zurück' 501 back: 'Zurück'
487 502
488user: 503user:
489 page_title: Benutzerverwaltung 504 page_title: 'Benutzerverwaltung'
490 new_user: Erstelle einen neuen Benutzer 505 new_user: 'Erstelle einen neuen Benutzer'
491 edit_user: Bearbeite einen existierenden Benutzer 506 edit_user: 'Bearbeite einen existierenden Benutzer'
492 description: "Hier kannst du alle Benutzer verwalten (erstellen, bearbeiten und löschen)" 507 description: "Hier kannst du alle Benutzer verwalten (erstellen, bearbeiten und löschen)"
493 list: 508 list:
494 actions: Aktionen 509 actions: 'Aktionen'
495 edit_action: Bearbeiten 510 edit_action: 'Bearbeiten'
496 yes: Ja 511 yes: 'Ja'
497 no: Nein 512 no: 'Nein'
498 create_new_one: Erstelle einen neuen Benutzer 513 create_new_one: 'Erstelle einen neuen Benutzer'
499 form: 514 form:
500 username_label: 'Benutzername' 515 username_label: 'Benutzername'
501 name_label: 'Name' 516 name_label: 'Name'
@@ -505,29 +520,52 @@ user:
505 email_label: 'E-Mail-Adresse' 520 email_label: 'E-Mail-Adresse'
506 enabled_label: 'Aktiviert' 521 enabled_label: 'Aktiviert'
507 last_login_label: 'Letzter Login' 522 last_login_label: 'Letzter Login'
508 twofactor_label: Zwei-Faktor-Authentifizierung 523 twofactor_label: 'Zwei-Faktor-Authentifizierung'
509 save: Speichern 524 save: 'Speichern'
510 delete: Löschen 525 delete: 'Löschen'
511 delete_confirm: Bist du sicher? 526 delete_confirm: 'Bist du sicher?'
512 back_to_list: Zurück zur Liste 527 back_to_list: 'Zurück zur Liste'
528 search:
529 placeholder: 'Filtere nach Benutzer oder E-Mail-Adresse'
530
531site_credential:
532 page_title: 'Verwaltung Zugangsdaten'
533 new_site_credential: 'Zugangsdaten anlegen'
534 edit_site_credential: 'Bestehende Zugangsdaten bearbeiten'
535 description: 'Hier kannst du alle Zugangsdaten für Webseiten verwalten, die diese benötigen (anlegen, bearbeiten und löschen), wie z. B. eine Paywall, eine Authentifizierung, etc.'
536 list:
537 actions: 'Aktionen'
538 edit_action: 'Bearbeiten'
539 yes: 'Ja'
540 no: 'Nein'
541 create_new_one: 'Einen neuen Seitenzugang anlegen'
542 form:
543 username_label: 'Benutzername'
544 host_label: 'Host'
545 password_label: 'Passwort'
546 save: 'Speichern'
547 delete: 'Löschen'
548 delete_confirm: 'Bist du sicher?'
549 back_to_list: 'Zurück zur Liste'
513 550
514error: 551error:
515 page_title: Ein Fehler ist aufgetreten 552 page_title: 'Ein Fehler ist aufgetreten'
516 553
517flashes: 554flashes:
518 config: 555 config:
519 notice: 556 notice:
520 config_saved: 'Konfiguration gespeichert.' 557 config_saved: 'Konfiguration gespeichert.'
521 password_updated: 'Kennwort aktualisiert' 558 password_updated: 'Kennwort aktualisiert'
522 password_not_updated_demo: "Im Testmodus kannst du das Kennwort nicht ändern." 559 password_not_updated_demo: 'Im Testmodus kannst du das Kennwort nicht ändern.'
523 user_updated: 'Information aktualisiert' 560 user_updated: 'Information aktualisiert'
524 rss_updated: 'RSS-Informationen aktualisiert' 561 rss_updated: 'RSS-Informationen aktualisiert'
525 tagging_rules_updated: 'Tagging-Regeln aktualisiert' 562 tagging_rules_updated: 'Tagging-Regeln aktualisiert'
526 tagging_rules_deleted: 'Tagging-Regel gelöscht' 563 tagging_rules_deleted: 'Tagging-Regel gelöscht'
527 rss_token_updated: 'RSS-Token aktualisiert' 564 rss_token_updated: 'RSS-Token aktualisiert'
528 annotations_reset: Anmerkungen zurücksetzen 565 annotations_reset: 'Anmerkungen zurücksetzen'
529 tags_reset: Tags zurücksetzen 566 tags_reset: 'Tags zurücksetzen'
530 entries_reset: Einträge zurücksetzen 567 entries_reset: 'Einträge zurücksetzen'
568 archived_reset: 'Archiverte Einträge zurücksetzen'
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: 'Eintrag bereits am %date% gespeichert' 571 entry_already_saved: 'Eintrag bereits am %date% gespeichert'
@@ -562,3 +600,8 @@ flashes:
562 added: 'Benutzer "%username%" hinzugefügt' 600 added: 'Benutzer "%username%" hinzugefügt'
563 updated: 'Benutzer "%username%" aktualisiert' 601 updated: 'Benutzer "%username%" aktualisiert'
564 deleted: 'Benutzer "%username%" gelöscht' 602 deleted: 'Benutzer "%username%" gelöscht'
603 site_credential:
604 notice:
605 added: 'Zugangsdaten für "%host%" hinzugefügt'
606 updated: 'Zugangsdaten für "%host%" aktualisiert'
607 deleted: 'Zugangsdaten für "%host%" gelöscht'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
index c72e5958..de3e11fe 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
@@ -32,6 +32,7 @@ menu:
32 save_link: 'Save a link' 32 save_link: 'Save a link'
33 back_to_unread: 'Back to unread articles' 33 back_to_unread: 'Back to unread articles'
34 users_management: 'Users management' 34 users_management: 'Users management'
35 site_credentials: 'Site credentials'
35 top: 36 top:
36 add_new_entry: 'Add a new entry' 37 add_new_entry: 'Add a new entry'
37 search: 'Search' 38 search: 'Search'
@@ -76,6 +77,7 @@ config:
76 redirect_current_page: 'To the current page' 77 redirect_current_page: 'To the current page'
77 pocket_consumer_key_label: Consumer key for Pocket to import contents 78 pocket_consumer_key_label: Consumer key for Pocket to import contents
78 android_configuration: Configure your Android application 79 android_configuration: Configure your Android application
80 android_instruction: "Touch here to prefill your Android application"
79 help_theme: "wallabag is customizable. You can choose your prefered theme here." 81 help_theme: "wallabag is customizable. You can choose your prefered theme here."
80 help_items_per_page: "You can change the number of articles displayed on each page." 82 help_items_per_page: "You can change the number of articles displayed on each page."
81 help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article." 83 help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article."
@@ -89,9 +91,10 @@ config:
89 token_reset: 'Regenerate your token' 91 token_reset: 'Regenerate your token'
90 rss_links: 'RSS links' 92 rss_links: 'RSS links'
91 rss_link: 93 rss_link:
92 unread: 'unread' 94 unread: 'Unread'
93 starred: 'starred' 95 starred: 'Starred'
94 archive: 'archived' 96 archive: 'Archived'
97 all: 'All'
95 rss_limit: 'Number of items in the feed' 98 rss_limit: 'Number of items in the feed'
96 form_user: 99 form_user:
97 two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connection." 100 two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connection."
@@ -110,6 +113,7 @@ config:
110 annotations: Remove ALL annotations 113 annotations: Remove ALL annotations
111 tags: Remove ALL tags 114 tags: Remove ALL tags
112 entries: Remove ALL entries 115 entries: Remove ALL entries
116 archived: Remove ALL archived entries
113 confirm: Are you really sure? (THIS CAN'T BE UNDONE) 117 confirm: Are you really sure? (THIS CAN'T BE UNDONE)
114 form_password: 118 form_password:
115 description: "You can change your password here. Your new password should by at least 8 characters long." 119 description: "You can change your password here. Your new password should by at least 8 characters long."
@@ -154,6 +158,7 @@ config:
154 or: 'One rule OR another' 158 or: 'One rule OR another'
155 and: 'One rule AND another' 159 and: 'One rule AND another'
156 matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>' 160 matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
161 notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 filtered_tags: 'Filtered by tags:' 169 filtered_tags: 'Filtered by tags:'
165 filtered_search: 'Filtered by search:' 170 filtered_search: 'Filtered by search:'
166 untagged: 'Untagged entries' 171 untagged: 'Untagged entries'
172 all: 'All entries'
167 list: 173 list:
168 number_on_the_page: '{0} There are no entries.|{1} There is one entry.|]1,Inf[ There are %count% entries.' 174 number_on_the_page: '{0} There are no entries.|{1} There is one entry.|]1,Inf[ There are %count% entries.'
169 reading_time: 'estimated reading time' 175 reading_time: 'estimated reading time'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'Unread' 191 unread_label: 'Unread'
186 preview_picture_label: 'Has a preview picture' 192 preview_picture_label: 'Has a preview picture'
187 preview_picture_help: 'Preview picture' 193 preview_picture_help: 'Preview picture'
194 is_public_label: 'Has a public link'
195 is_public_help: 'Public link'
188 language_label: 'Language' 196 language_label: 'Language'
189 http_status_label: 'HTTP status' 197 http_status_label: 'HTTP status'
190 reading_time: 198 reading_time:
@@ -223,6 +231,8 @@ entry:
223 original_article: 'original' 231 original_article: 'original'
224 annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations' 232 annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations'
225 created_at: 'Creation date' 233 created_at: 'Creation date'
234 published_at: 'Publication date'
235 published_by: 'Published by'
226 new: 236 new:
227 page_title: 'Save new entry' 237 page_title: 'Save new entry'
228 placeholder: 'http://website.com' 238 placeholder: 'http://website.com'
@@ -234,10 +244,12 @@ entry:
234 page_title: 'Edit an entry' 244 page_title: 'Edit an entry'
235 title_label: 'Title' 245 title_label: 'Title'
236 url_label: 'Url' 246 url_label: 'Url'
237 is_public_label: 'Public'
238 save_label: 'Save' 247 save_label: 'Save'
239 public: 248 public:
240 shared_by_wallabag: "This article has been shared by <a href='%wallabag_instance%'>wallabag</a>" 249 shared_by_wallabag: "This article has been shared by %username% with <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 delete: "Are you sure you want to remove that article?"
252 delete_tag: "Are you sure you want to remove that tag from that article?"
241 253
242about: 254about:
243 page_title: 'About' 255 page_title: 'About'
@@ -385,6 +397,9 @@ tag:
385 add: 'Add' 397 add: 'Add'
386 placeholder: 'You can add several tags, separated by a comma.' 398 placeholder: 'You can add several tags, separated by a comma.'
387 399
400export:
401 footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
388import: 403import:
389 page_title: 'Import' 404 page_title: 'Import'
390 page_description: 'Welcome to wallabag importer. Please select your previous service from which you want to migrate.' 405 page_description: 'Welcome to wallabag importer. Please select your previous service from which you want to migrate.'
@@ -510,6 +525,28 @@ user:
510 delete: Delete 525 delete: Delete
511 delete_confirm: Are you sure? 526 delete_confirm: Are you sure?
512 back_to_list: Back to list 527 back_to_list: Back to list
528 search:
529 placeholder: Filter by username or email
530
531site_credential:
532 page_title: Site credentials management
533 new_site_credential: Create a credential
534 edit_site_credential: Edit an existing credential
535 description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc."
536 list:
537 actions: Actions
538 edit_action: Edit
539 yes: Yes
540 no: No
541 create_new_one: Create a new credential
542 form:
543 username_label: 'Username'
544 host_label: 'Host'
545 password_label: 'Password'
546 save: Save
547 delete: Delete
548 delete_confirm: Are you sure?
549 back_to_list: Back to list
513 550
514error: 551error:
515 page_title: An error occurred 552 page_title: An error occurred
@@ -528,6 +565,7 @@ flashes:
528 annotations_reset: Annotations reset 565 annotations_reset: Annotations reset
529 tags_reset: Tags reset 566 tags_reset: Tags reset
530 entries_reset: Entries reset 567 entries_reset: Entries reset
568 archived_reset: Archived entries deleted
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: 'Entry already saved on %date%' 571 entry_already_saved: 'Entry already saved on %date%'
@@ -562,3 +600,8 @@ flashes:
562 added: 'User "%username%" added' 600 added: 'User "%username%" added'
563 updated: 'User "%username%" updated' 601 updated: 'User "%username%" updated'
564 deleted: 'User "%username%" deleted' 602 deleted: 'User "%username%" deleted'
603 site_credential:
604 notice:
605 added: 'Site credential for "%host%" added'
606 updated: 'Site credential for "%host%" updated'
607 deleted: 'Site credential for "%host%" deleted'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
index 2f769b7e..6dfc1525 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
@@ -32,6 +32,7 @@ menu:
32 save_link: 'Guardar un enlace' 32 save_link: 'Guardar un enlace'
33 back_to_unread: 'Volver a los artículos sin leer' 33 back_to_unread: 'Volver a los artículos sin leer'
34 users_management: 'Configuración de usuarios' 34 users_management: 'Configuración de usuarios'
35 # site_credentials: 'Site credentials'
35 top: 36 top:
36 add_new_entry: 'Añadir un nuevo artículo' 37 add_new_entry: 'Añadir un nuevo artículo'
37 search: 'Buscar' 38 search: 'Buscar'
@@ -76,6 +77,7 @@ config:
76 redirect_current_page: 'A la página actual' 77 redirect_current_page: 'A la página actual'
77 pocket_consumer_key_label: Clave de consumidor para importar contenidos de Pocket 78 pocket_consumer_key_label: Clave de consumidor para importar contenidos de Pocket
78 android_configuration: Configura tu aplicación Android 79 android_configuration: Configura tu aplicación Android
80 # android_instruction: "Touch here to prefill your Android application"
79 help_theme: "wallabag es personalizable. Puedes elegir tu tema preferido aquí." 81 help_theme: "wallabag es personalizable. Puedes elegir tu tema preferido aquí."
80 help_items_per_page: "Puedes cambiar el número de artículos mostrados en cada página." 82 help_items_per_page: "Puedes cambiar el número de artículos mostrados en cada página."
81 help_reading_speed: "wallabag calcula un tiempo de lectura para cada artículo. Puedes definir aquí, gracias a esta lista, si eres un lector rápido o lento. wallabag recalculará el tiempo de lectura para cada artículo." 83 help_reading_speed: "wallabag calcula un tiempo de lectura para cada artículo. Puedes definir aquí, gracias a esta lista, si eres un lector rápido o lento. wallabag recalculará el tiempo de lectura para cada artículo."
@@ -92,6 +94,7 @@ config:
92 unread: 'sin leer' 94 unread: 'sin leer'
93 starred: 'favoritos' 95 starred: 'favoritos'
94 archive: 'archivados' 96 archive: 'archivados'
97 # all: 'All'
95 rss_limit: 'Límite de artículos en feed RSS' 98 rss_limit: 'Límite de artículos en feed RSS'
96 form_user: 99 form_user:
97 two_factor_description: "Con la autenticación en dos pasos recibirá código por e-mail en cada nueva conexión que no sea de confianza." 100 two_factor_description: "Con la autenticación en dos pasos recibirá código por e-mail en cada nueva conexión que no sea de confianza."
@@ -110,6 +113,7 @@ config:
110 annotations: Eliminar TODAS las anotaciones 113 annotations: Eliminar TODAS las anotaciones
111 tags: Eliminar TODAS las etiquetas 114 tags: Eliminar TODAS las etiquetas
112 entries: Eliminar TODOS los artículos 115 entries: Eliminar TODOS los artículos
116 # archived: Remove ALL archived entries
113 confirm: ¿Estás completamente seguro? (NO SE PUEDE DESHACER) 117 confirm: ¿Estás completamente seguro? (NO SE PUEDE DESHACER)
114 form_password: 118 form_password:
115 description: "Puedes cambiar la contraseña aquí. Tu nueva contraseña debe tener al menos 8 caracteres." 119 description: "Puedes cambiar la contraseña aquí. Tu nueva contraseña debe tener al menos 8 caracteres."
@@ -154,6 +158,7 @@ config:
154 or: 'Una regla U otra' 158 or: 'Una regla U otra'
155 and: 'Una regla Y la otra' 159 and: 'Una regla Y la otra'
156 matches: 'Prueba si un <i>sujeto</i> corresponde a una <i>búsqueda</i> (insensible a mayusculas).<br />Ejemplo : <code>title matches "fútbol"</code>' 160 matches: 'Prueba si un <i>sujeto</i> corresponde a una <i>búsqueda</i> (insensible a mayusculas).<br />Ejemplo : <code>title matches "fútbol"</code>'
161 # notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 filtered_tags: 'Filtrado por etiquetas:' 169 filtered_tags: 'Filtrado por etiquetas:'
165 filtered_search: 'Filtrado por búsqueda:' 170 filtered_search: 'Filtrado por búsqueda:'
166 untagged: 'Artículos sin etiquetas' 171 untagged: 'Artículos sin etiquetas'
172 all: "Todos los artículos"
167 list: 173 list:
168 number_on_the_page: '{0} No hay artículos.|{1} Hay un artículo.|]1,Inf[ Hay %count% artículos.' 174 number_on_the_page: '{0} No hay artículos.|{1} Hay un artículo.|]1,Inf[ Hay %count% artículos.'
169 reading_time: 'tiempo estimado de lectura' 175 reading_time: 'tiempo estimado de lectura'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'Sin leer' 191 unread_label: 'Sin leer'
186 preview_picture_label: 'Tiene imagen de previsualización' 192 preview_picture_label: 'Tiene imagen de previsualización'
187 preview_picture_help: 'Imagen de previsualización' 193 preview_picture_help: 'Imagen de previsualización'
194 # is_public_label: 'Has a public link'
195 # is_public_help: 'Public link'
188 language_label: 'Idioma' 196 language_label: 'Idioma'
189 http_status_label: 'Código de estado HTTP' 197 http_status_label: 'Código de estado HTTP'
190 reading_time: 198 reading_time:
@@ -223,6 +231,8 @@ entry:
223 original_article: 'original' 231 original_article: 'original'
224 annotations_on_the_entry: '{0} Sin anotaciones|{1} Una anotación|]1,Inf[ %count% anotaciones' 232 annotations_on_the_entry: '{0} Sin anotaciones|{1} Una anotación|]1,Inf[ %count% anotaciones'
225 created_at: 'Fecha de creación' 233 created_at: 'Fecha de creación'
234 # published_at: 'Publication date'
235 # published_by: 'Published by'
226 new: 236 new:
227 page_title: 'Guardar un nuevo artículo' 237 page_title: 'Guardar un nuevo artículo'
228 placeholder: 'http://sitioweb.com' 238 placeholder: 'http://sitioweb.com'
@@ -234,10 +244,12 @@ entry:
234 page_title: 'Editar un artículo' 244 page_title: 'Editar un artículo'
235 title_label: 'Título' 245 title_label: 'Título'
236 url_label: 'URL' 246 url_label: 'URL'
237 is_public_label: 'Es público'
238 save_label: 'Guardar' 247 save_label: 'Guardar'
239 public: 248 public:
240 shared_by_wallabag: "Este artículo se ha compartido con <a href='%wallabag_instance%'>wallabag</a>" 249 shared_by_wallabag: "Este artículo se ha compartido con <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 # delete: "Are you sure you want to remove that article?"
252 # delete_tag: "Are you sure you want to remove that tag from that article?"
241 253
242about: 254about:
243 page_title: 'Acerca de' 255 page_title: 'Acerca de'
@@ -385,6 +397,9 @@ tag:
385 add: 'Añadir' 397 add: 'Añadir'
386 placeholder: 'Puedes añadir varias etiquetas, separadas por una coma.' 398 placeholder: 'Puedes añadir varias etiquetas, separadas por una coma.'
387 399
400# export:
401# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
388import: 403import:
389 page_title: 'Importar' 404 page_title: 'Importar'
390 page_description: 'Bienvenido a la herramienta de importación de wallabag. Seleccione el servicio desde el que desea migrar.' 405 page_description: 'Bienvenido a la herramienta de importación de wallabag. Seleccione el servicio desde el que desea migrar.'
@@ -510,6 +525,28 @@ user:
510 delete: Eliminar 525 delete: Eliminar
511 delete_confirm: ¿Estás seguro? 526 delete_confirm: ¿Estás seguro?
512 back_to_list: Volver a la lista 527 back_to_list: Volver a la lista
528 search:
529 # placeholder: Filter by username or email
530
531site_credential:
532 # page_title: Site credentials management
533 # new_site_credential: Create a credential
534 # edit_site_credential: Edit an existing credential
535 # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc."
536 # list:
537 # actions: Actions
538 # edit_action: Edit
539 # yes: Yes
540 # no: No
541 # create_new_one: Create a new credential
542 # form:
543 # username_label: 'Username'
544 # host_label: 'Host'
545 # password_label: 'Password'
546 # save: Save
547 # delete: Delete
548 # delete_confirm: Are you sure?
549 # back_to_list: Back to list
513 550
514error: 551error:
515 page_title: Ha ocurrido un error 552 page_title: Ha ocurrido un error
@@ -528,6 +565,7 @@ flashes:
528 annotations_reset: Anotaciones reiniciadas 565 annotations_reset: Anotaciones reiniciadas
529 tags_reset: Etiquetas reiniciadas 566 tags_reset: Etiquetas reiniciadas
530 entries_reset: Artículos reiniciados 567 entries_reset: Artículos reiniciados
568 # archived_reset: Archived entries deleted
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: 'Artículo ya guardado el %fecha%' 571 entry_already_saved: 'Artículo ya guardado el %fecha%'
@@ -562,3 +600,8 @@ flashes:
562 added: 'Añadido el usuario "%username%"' 600 added: 'Añadido el usuario "%username%"'
563 updated: 'Actualizado el usuario "%username%"' 601 updated: 'Actualizado el usuario "%username%"'
564 deleted: 'Eliminado el usuario "%username%"' 602 deleted: 'Eliminado el usuario "%username%"'
603 site_credential:
604 notice:
605 # added: 'Site credential for "%host%" added'
606 # updated: 'Site credential for "%host%" updated'
607 # deleted: 'Site credential for "%host%" deleted'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
index 647f12ad..ffc48933 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
@@ -32,6 +32,7 @@ menu:
32 save_link: 'ذخیرهٔ یک پیوند' 32 save_link: 'ذخیرهٔ یک پیوند'
33 back_to_unread: 'بازگشت به خوانده‌نشده‌ها' 33 back_to_unread: 'بازگشت به خوانده‌نشده‌ها'
34 # users_management: 'Users management' 34 # users_management: 'Users management'
35 # site_credentials: 'Site credentials'
35 top: 36 top:
36 add_new_entry: 'افزودن مقالهٔ تازه' 37 add_new_entry: 'افزودن مقالهٔ تازه'
37 search: 'جستجو' 38 search: 'جستجو'
@@ -77,6 +78,7 @@ config:
77 pocket_consumer_key_label: کلید کاربری Pocket برای درون‌ریزی مطالب 78 pocket_consumer_key_label: کلید کاربری Pocket برای درون‌ریزی مطالب
78 # android_configuration: Configure your Android application 79 # android_configuration: Configure your Android application
79 # help_theme: "wallabag is customizable. You can choose your prefered theme here." 80 # help_theme: "wallabag is customizable. You can choose your prefered theme here."
81 # android_instruction: "Touch here to prefill your Android application"
80 # help_items_per_page: "You can change the number of articles displayed on each page." 82 # help_items_per_page: "You can change the number of articles displayed on each page."
81 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article." 83 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article."
82 # help_language: "You can change the language of wallabag interface." 84 # help_language: "You can change the language of wallabag interface."
@@ -92,6 +94,7 @@ config:
92 unread: 'خوانده‌نشده' 94 unread: 'خوانده‌نشده'
93 starred: 'برگزیده' 95 starred: 'برگزیده'
94 archive: 'بایگانی' 96 archive: 'بایگانی'
97 # all: 'All'
95 rss_limit: 'محدودیت آر-اس-اس' 98 rss_limit: 'محدودیت آر-اس-اس'
96 form_user: 99 form_user:
97 two_factor_description: "با فعال‌کردن تأیید ۲مرحله‌ای هر بار که اتصال تأییدنشده‌ای برقرار شد، به شما یک کد از راه ایمیل فرستاده می‌شود" 100 two_factor_description: "با فعال‌کردن تأیید ۲مرحله‌ای هر بار که اتصال تأییدنشده‌ای برقرار شد، به شما یک کد از راه ایمیل فرستاده می‌شود"
@@ -110,6 +113,7 @@ config:
110 # annotations: Remove ALL annotations 113 # annotations: Remove ALL annotations
111 # tags: Remove ALL tags 114 # tags: Remove ALL tags
112 # entries: Remove ALL entries 115 # entries: Remove ALL entries
116 # archived: Remove ALL archived entries
113 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE) 117 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
114 form_password: 118 form_password:
115 # description: "You can change your password here. Your new password should by at least 8 characters long." 119 # description: "You can change your password here. Your new password should by at least 8 characters long."
@@ -154,6 +158,7 @@ config:
154 # or: 'One rule OR another' 158 # or: 'One rule OR another'
155 # and: 'One rule AND another' 159 # and: 'One rule AND another'
156 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>' 160 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
161 # notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 # filtered_tags: 'Filtered by tags:' 169 # filtered_tags: 'Filtered by tags:'
165 # filtered_search: 'Filtered by search:' 170 # filtered_search: 'Filtered by search:'
166 # untagged: 'Untagged entries' 171 # untagged: 'Untagged entries'
172 # all: 'All entries'
167 list: 173 list:
168 number_on_the_page: '{0} هیج مقاله‌ای نیست.|{1} یک مقاله هست.|]1,Inf[ %count% مقاله هست.' 174 number_on_the_page: '{0} هیج مقاله‌ای نیست.|{1} یک مقاله هست.|]1,Inf[ %count% مقاله هست.'
169 reading_time: 'زمان تخمینی برای خواندن' 175 reading_time: 'زمان تخمینی برای خواندن'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'خوانده‌نشده' 191 unread_label: 'خوانده‌نشده'
186 preview_picture_label: 'دارای عکس پیش‌نمایش' 192 preview_picture_label: 'دارای عکس پیش‌نمایش'
187 preview_picture_help: 'پیش‌نمایش عکس' 193 preview_picture_help: 'پیش‌نمایش عکس'
194 # is_public_label: 'Has a public link'
195 # is_public_help: 'Public link'
188 language_label: 'زبان' 196 language_label: 'زبان'
189 # http_status_label: 'HTTP status' 197 # http_status_label: 'HTTP status'
190 reading_time: 198 reading_time:
@@ -223,6 +231,8 @@ entry:
223 original_article: 'اصلی' 231 original_article: 'اصلی'
224 annotations_on_the_entry: '{0} بدون حاشیه|{1} یک حاشیه|]1,Inf[ %nbحاشیه% annotations' 232 annotations_on_the_entry: '{0} بدون حاشیه|{1} یک حاشیه|]1,Inf[ %nbحاشیه% annotations'
225 created_at: 'زمان ساخت' 233 created_at: 'زمان ساخت'
234 # published_at: 'Publication date'
235 # published_by: 'Published by'
226 new: 236 new:
227 page_title: 'ذخیرهٔ مقالهٔ تازه' 237 page_title: 'ذخیرهٔ مقالهٔ تازه'
228 placeholder: 'http://website.com' 238 placeholder: 'http://website.com'
@@ -234,10 +244,12 @@ entry:
234 page_title: 'ویرایش مقاله' 244 page_title: 'ویرایش مقاله'
235 title_label: 'عنوان' 245 title_label: 'عنوان'
236 url_label: 'نشانی' 246 url_label: 'نشانی'
237 is_public_label: 'عمومی'
238 save_label: 'ذخیره' 247 save_label: 'ذخیره'
239 public: 248 public:
240 # shared_by_wallabag: "This article has been shared by <a href='%wallabag_instance%'>wallabag</a>" 249 # shared_by_wallabag: "This article has been shared by %username% with <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 # delete: "Are you sure you want to remove that article?"
252 # delete_tag: "Are you sure you want to remove that tag from that article?"
241 253
242about: 254about:
243 page_title: 'درباره' 255 page_title: 'درباره'
@@ -385,6 +397,9 @@ tag:
385 # add: 'Add' 397 # add: 'Add'
386 # placeholder: 'You can add several tags, separated by a comma.' 398 # placeholder: 'You can add several tags, separated by a comma.'
387 399
400# export:
401# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
388import: 403import:
389 page_title: 'درون‌ریزی' 404 page_title: 'درون‌ریزی'
390 page_description: 'به درون‌ریز wallabag خوش آمدید. لطفاً سرویس قبلی خود را که می‌خواهید از آن مهاجرت کنید انتخاب کنید.' 405 page_description: 'به درون‌ریز wallabag خوش آمدید. لطفاً سرویس قبلی خود را که می‌خواهید از آن مهاجرت کنید انتخاب کنید.'
@@ -510,6 +525,28 @@ user:
510 # delete: Delete 525 # delete: Delete
511 # delete_confirm: Are you sure? 526 # delete_confirm: Are you sure?
512 # back_to_list: Back to list 527 # back_to_list: Back to list
528 search:
529 # placeholder: Filter by username or email
530
531site_credential:
532 # page_title: Site credentials management
533 # new_site_credential: Create a credential
534 # edit_site_credential: Edit an existing credential
535 # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc."
536 # list:
537 # actions: Actions
538 # edit_action: Edit
539 # yes: Yes
540 # no: No
541 # create_new_one: Create a new credential
542 # form:
543 # username_label: 'Username'
544 # host_label: 'Host'
545 # password_label: 'Password'
546 # save: Save
547 # delete: Delete
548 # delete_confirm: Are you sure?
549 # back_to_list: Back to list
513 550
514error: 551error:
515 # page_title: An error occurred 552 # page_title: An error occurred
@@ -528,6 +565,7 @@ flashes:
528 # annotations_reset: Annotations reset 565 # annotations_reset: Annotations reset
529 # tags_reset: Tags reset 566 # tags_reset: Tags reset
530 # entries_reset: Entries reset 567 # entries_reset: Entries reset
568 # archived_reset: Archived entries deleted
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: 'این مقاله در تاریخ %date% ذخیره شده بود' 571 entry_already_saved: 'این مقاله در تاریخ %date% ذخیره شده بود'
@@ -562,3 +600,8 @@ flashes:
562 # added: 'User "%username%" added' 600 # added: 'User "%username%" added'
563 # updated: 'User "%username%" updated' 601 # updated: 'User "%username%" updated'
564 # deleted: 'User "%username%" deleted' 602 # deleted: 'User "%username%" deleted'
603 site_credential:
604 notice:
605 # added: 'Site credential for "%host%" added'
606 # updated: 'Site credential for "%host%" updated'
607 # deleted: 'Site credential for "%host%" deleted'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
index efddc46a..c9d95e2b 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
@@ -32,6 +32,7 @@ menu:
32 save_link: "Sauvegarder un nouvel article" 32 save_link: "Sauvegarder un nouvel article"
33 back_to_unread: "Retour aux articles non lus" 33 back_to_unread: "Retour aux articles non lus"
34 users_management: "Gestion des utilisateurs" 34 users_management: "Gestion des utilisateurs"
35 site_credentials: 'Accès aux sites'
35 top: 36 top:
36 add_new_entry: "Sauvegarder un nouvel article" 37 add_new_entry: "Sauvegarder un nouvel article"
37 search: "Rechercher" 38 search: "Rechercher"
@@ -46,7 +47,7 @@ footer:
46 social: "Social" 47 social: "Social"
47 powered_by: "propulsé par" 48 powered_by: "propulsé par"
48 about: "À propos" 49 about: "À propos"
49 stats: Depuis le %user_creation%, vous avez lu %nb_archives% articles. Ce qui fait %per_day% par jour ! 50 stats: "Depuis le %user_creation%, vous avez lu %nb_archives% articles. Ce qui fait %per_day% par jour !"
50 51
51config: 52config:
52 page_title: "Configuration" 53 page_title: "Configuration"
@@ -71,27 +72,29 @@ config:
71 300_word: "Je lis environ 300 mots par minute" 72 300_word: "Je lis environ 300 mots par minute"
72 400_word: "Je lis environ 400 mots par minute" 73 400_word: "Je lis environ 400 mots par minute"
73 action_mark_as_read: 74 action_mark_as_read:
74 label: 'Où souhaitez-vous être redirigé après avoir marqué un article comme lu ?' 75 label: "Où souhaitez-vous être redirigé après avoir marqué un article comme lu ?"
75 redirect_homepage: "À la page d'accueil" 76 redirect_homepage: "À la page d’accueil"
76 redirect_current_page: 'À la page courante' 77 redirect_current_page: "À la page courante"
77 pocket_consumer_key_label: Clé d’authentification Pocket pour importer les données 78 pocket_consumer_key_label: "Clé d’authentification Pocket pour importer les données"
78 android_configuration: Configurez votre application Android 79 android_configuration: "Configurez votre application Android"
79 help_theme: "L'affichage de wallabag est personnalisable. C'est ici que vous choisissez le thème que vous préférez." 80 android_instruction: "Appuyez ici pour préremplir votre application Android"
80 help_items_per_page: "Vous pouvez définir le nombre d'articles affichés sur chaque page." 81 help_theme: "L’affichage de wallabag est personnalisable. C’est ici que vous choisissez le thème que vous préférez."
82 help_items_per_page: "Vous pouvez définir le nombre d’articles affichés sur chaque page."
81 help_reading_speed: "wallabag calcule une durée de lecture pour chaque article. Vous pouvez définir ici, grâce à cette liste déroulante, si vous lisez plus ou moins vite. wallabag recalculera la durée de lecture de chaque article." 83 help_reading_speed: "wallabag calcule une durée de lecture pour chaque article. Vous pouvez définir ici, grâce à cette liste déroulante, si vous lisez plus ou moins vite. wallabag recalculera la durée de lecture de chaque article."
82 help_language: "Vous pouvez définir la langue de l'interface de wallabag." 84 help_language: "Vous pouvez définir la langue de linterface de wallabag."
83 help_pocket_consumer_key: "Nécessaire pour l'import depuis Pocket. Vous pouvez le créer depuis votre compte Pocket." 85 help_pocket_consumer_key: "Nécessaire pour l’import depuis Pocket. Vous pouvez le créer depuis votre compte Pocket."
84 form_rss: 86 form_rss:
85 description: "Les flux RSS fournis par wallabag vous permettent de lire vos articles sauvegardés dans votre lecteur de flux préféré. Pour pouvoir les utiliser, vous devez d’abord créer un jeton." 87 description: "Les flux RSS fournis par wallabag vous permettent de lire vos articles sauvegardés dans votre lecteur de flux préféré. Pour pouvoir les utiliser, vous devez d’abord créer un jeton."
86 token_label: "Jeton RSS" 88 token_label: "Jeton RSS"
87 no_token: "Aucun jeton généré" 89 no_token: "Aucun jeton généré"
88 token_create: "Créez votre jeton" 90 token_create: "Créez votre jeton"
89 token_reset: "Réinitialisez votre jeton" 91 token_reset: "Réinitialisez votre jeton"
90 rss_links: "Adresse de vos flux RSS" 92 rss_links: "Adresses de vos flux RSS"
91 rss_link: 93 rss_link:
92 unread: "non lus" 94 unread: "Non lus"
93 starred: "favoris" 95 starred: "Favoris"
94 archive: "lus" 96 archive: "Lus"
97 all: "Tous"
95 rss_limit: "Nombre d’articles dans le flux" 98 rss_limit: "Nombre d’articles dans le flux"
96 form_user: 99 form_user:
97 two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel à chaque nouvelle connexion non approuvée." 100 two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel à chaque nouvelle connexion non approuvée."
@@ -100,17 +103,18 @@ config:
100 twoFactorAuthentication_label: "Double authentification" 103 twoFactorAuthentication_label: "Double authentification"
101 help_twoFactorAuthentication: "Si vous activez 2FA, à chaque tentative de connexion à wallabag, vous recevrez un code par email." 104 help_twoFactorAuthentication: "Si vous activez 2FA, à chaque tentative de connexion à wallabag, vous recevrez un code par email."
102 delete: 105 delete:
103 title: Supprimer mon compte (attention danger !) 106 title: "Supprimer mon compte (attention danger !)"
104 description: Si vous confirmez la suppression de votre compte, TOUS les articles, TOUS les tags, TOUTES les annotations et votre compte seront DÉFINITIVEMENT supprimé (c'est IRRÉVERSIBLE). Vous serez ensuite déconnecté. 107 description: "Si vous confirmez la suppression de votre compte, TOUS les articles, TOUS les tags, TOUTES les annotations et votre compte seront DÉFINITIVEMENT supprimé (c’est IRRÉVERSIBLE). Vous serez ensuite déconnecté."
105 confirm: Vous êtes vraiment sûr ? (C'EST IRRÉVERSIBLE) 108 confirm: "Vous êtes vraiment sûr ? (CEST IRRÉVERSIBLE)"
106 button: 'Supprimer mon compte' 109 button: "Supprimer mon compte"
107 reset: 110 reset:
108 title: Réinitialisation (attention danger !) 111 title: "Réinitialisation (attention danger !)"
109 description: En cliquant sur les boutons ci-dessous vous avez la possibilité de supprimer certaines informations de votre compte. Attention, ces actions sont IRRÉVERSIBLES ! 112 description: "En cliquant sur les boutons ci-dessous vous avez la possibilité de supprimer certaines informations de votre compte. Attention, ces actions sont IRRÉVERSIBLES !"
110 annotations: Supprimer TOUTES les annotations 113 annotations: "Supprimer TOUTES les annotations"
111 tags: Supprimer TOUS les tags 114 tags: "Supprimer TOUS les tags"
112 entries: Supprimer TOUS les articles 115 entries: "Supprimer TOUS les articles"
113 confirm: Êtes-vous vraiment vraiment sûr ? (C'EST IRRÉVERSIBLE) 116 archived: "Supprimer TOUS les articles archivés"
117 confirm: "Êtes-vous vraiment vraiment sûr ? (C’EST IRRÉVERSIBLE)"
114 form_password: 118 form_password:
115 description: "Vous pouvez changer ici votre mot de passe. Le mot de passe doit contenir au moins 8 caractères." 119 description: "Vous pouvez changer ici votre mot de passe. Le mot de passe doit contenir au moins 8 caractères."
116 old_password_label: "Mot de passe actuel" 120 old_password_label: "Mot de passe actuel"
@@ -154,6 +158,7 @@ config:
154 or: "Une règle OU l’autre" 158 or: "Une règle OU l’autre"
155 and: "Une règle ET l’autre" 159 and: "Une règle ET l’autre"
156 matches: "Teste si un <i>sujet</i> correspond à une <i>recherche</i> (non sensible à la casse).<br />Exemple : <code>title matches \"football\"</code>" 160 matches: "Teste si un <i>sujet</i> correspond à une <i>recherche</i> (non sensible à la casse).<br />Exemple : <code>title matches \"football\"</code>"
161 notmatches: "Teste si un <i>sujet</i> ne correspond pas à une <i>recherche</i> (non sensible à la casse).<br />Exemple : <code>title notmatches \"football\"</code>"
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -162,8 +167,9 @@ entry:
162 archived: "Articles lus" 167 archived: "Articles lus"
163 filtered: "Articles filtrés" 168 filtered: "Articles filtrés"
164 filtered_tags: "Articles filtrés par tags :" 169 filtered_tags: "Articles filtrés par tags :"
165 filtered_search: 'Articles filtrés par recherche :' 170 filtered_search: "Articles filtrés par recherche :"
166 untagged: "Article sans tag" 171 untagged: "Article sans tag"
172 all: "Tous les articles"
167 list: 173 list:
168 number_on_the_page: "{0} Il n’y a pas d’article.|{1} Il y a un article.|]1,Inf[ Il y a %count% articles." 174 number_on_the_page: "{0} Il n’y a pas d’article.|{1} Il y a un article.|]1,Inf[ Il y a %count% articles."
169 reading_time: "durée de lecture" 175 reading_time: "durée de lecture"
@@ -185,8 +191,10 @@ entry:
185 unread_label: "Non lus" 191 unread_label: "Non lus"
186 preview_picture_label: "A une photo" 192 preview_picture_label: "A une photo"
187 preview_picture_help: "Photo" 193 preview_picture_help: "Photo"
194 is_public_label: 'A un lien public'
195 is_public_help: 'Lien public'
188 language_label: "Langue" 196 language_label: "Langue"
189 http_status_label: 'Statut HTTP' 197 http_status_label: "Statut HTTP"
190 reading_time: 198 reading_time:
191 label: "Durée de lecture en minutes" 199 label: "Durée de lecture en minutes"
192 from: "de" 200 from: "de"
@@ -223,6 +231,8 @@ entry:
223 original_article: "original" 231 original_article: "original"
224 annotations_on_the_entry: "{0} Aucune annotation|{1} Une annotation|]1,Inf[ %count% annotations" 232 annotations_on_the_entry: "{0} Aucune annotation|{1} Une annotation|]1,Inf[ %count% annotations"
225 created_at: "Date de création" 233 created_at: "Date de création"
234 published_at: "Date de publication"
235 published_by: "Publié par"
226 new: 236 new:
227 page_title: "Sauvegarder un nouvel article" 237 page_title: "Sauvegarder un nouvel article"
228 placeholder: "http://website.com" 238 placeholder: "http://website.com"
@@ -234,10 +244,12 @@ entry:
234 page_title: "Éditer un article" 244 page_title: "Éditer un article"
235 title_label: "Titre" 245 title_label: "Titre"
236 url_label: "Adresse" 246 url_label: "Adresse"
237 is_public_label: "Public"
238 save_label: "Enregistrer" 247 save_label: "Enregistrer"
239 public: 248 public:
240 shared_by_wallabag: "Cet article a été partagé par <a href=\"%wallabag_instance%\">wallabag</a>" 249 shared_by_wallabag: "Cet article a été partagé par %username% avec <a href=\"%wallabag_instance%\">wallabag</a>"
250 confirm:
251 delete: "Voulez-vous vraiment supprimer cet article ?"
252 delete_tag: "Voulez-vous vraiment supprimer ce tag de cet article ?"
241 253
242about: 254about:
243 page_title: "À propos" 255 page_title: "À propos"
@@ -295,32 +307,32 @@ howto:
295 bookmarklet: 307 bookmarklet:
296 description: "Glissez et déposez ce lien dans votre barre de favoris :" 308 description: "Glissez et déposez ce lien dans votre barre de favoris :"
297 shortcuts: 309 shortcuts:
298 page_description: Voici les raccourcis disponibles dans wallabag. 310 page_description: "Voici les raccourcis disponibles dans wallabag."
299 shortcut: Raccourci 311 shortcut: "Raccourci"
300 action: Action 312 action: "Action"
301 all_pages_title: Raccourcis disponibles dans toutes les pages 313 all_pages_title: "Raccourcis disponibles dans toutes les pages"
302 go_unread: Afficher les articles non lus 314 go_unread: "Afficher les articles non lus"
303 go_starred: Afficher les articles favoris 315 go_starred: "Afficher les articles favoris"
304 go_archive: Afficher les articles lus 316 go_archive: "Afficher les articles lus"
305 go_all: Afficher tous les articles 317 go_all: "Afficher tous les articles"
306 go_tags: Afficher les tags 318 go_tags: "Afficher les tags"
307 go_config: Aller à la configuration 319 go_config: "Aller à la configuration"
308 go_import: Aller aux imports 320 go_import: "Aller aux imports"
309 go_developers: Aller à la section Développeurs 321 go_developers: "Aller à la section Développeurs"
310 go_howto: Afficher l'aide (cette page !) 322 go_howto: "Afficher laide (cette page !)"
311 go_logout: Se déconnecter 323 go_logout: "Se déconnecter"
312 list_title: Raccourcis disponibles dans les pages de liste 324 list_title: "Raccourcis disponibles dans les pages de liste"
313 search: Afficher le formulaire de recherche 325 search: "Afficher le formulaire de recherche"
314 article_title: Raccourcis disponibles quand on affiche un article 326 article_title: "Raccourcis disponibles quand on affiche un article"
315 open_original: Ouvrir l'URL originale de l'article 327 open_original: "Ouvrir lURL originale de larticle"
316 toggle_favorite: Changer le statut Favori de l'article 328 toggle_favorite: "Changer le statut Favori de larticle"
317 toggle_archive: Changer le status Lu de l'article 329 toggle_archive: "Changer le status Lu de larticle"
318 delete: Supprimer l'article 330 delete: "Supprimer larticle"
319 material_title: Raccourcis disponibles avec le thème Material uniquement 331 material_title: "Raccourcis disponibles avec le thème Material uniquement"
320 add_link: Ajouter un nouvel article 332 add_link: "Ajouter un nouvel article"
321 hide_form: Masquer le formulaire courant (recherche ou nouvel article) 333 hide_form: "Masquer le formulaire courant (recherche ou nouvel article)"
322 arrows_navigation: Naviguer à travers les articles 334 arrows_navigation: "Naviguer à travers les articles"
323 open_article: Afficher l'article sélectionné 335 open_article: "Afficher larticle sélectionné"
324 336
325quickstart: 337quickstart:
326 page_title: "Pour bien débuter" 338 page_title: "Pour bien débuter"
@@ -382,8 +394,11 @@ tag:
382 number_on_the_page: "{0} Il n’y a pas de tag.|{1} Il y a un tag.|]1,Inf[ Il y a %count% tags." 394 number_on_the_page: "{0} Il n’y a pas de tag.|{1} Il y a un tag.|]1,Inf[ Il y a %count% tags."
383 see_untagged_entries: "Voir les articles sans tag" 395 see_untagged_entries: "Voir les articles sans tag"
384 new: 396 new:
385 add: 'Ajouter' 397 add: "Ajouter"
386 placeholder: 'Vous pouvez ajouter plusieurs tags, séparés par une virgule.' 398 placeholder: "Vous pouvez ajouter plusieurs tags, séparés par une virgule."
399
400export:
401 footer_template: '<div style="text-align:center;"><p>Généré par wallabag with %method%</p><p>Merci d''ouvrir <a href="https://github.com/wallabag/wallabag/issues">un ticket</a> si vous rencontrez des soucis d''affichage avec ce document sur votre support.</p></div>'
387 402
388import: 403import:
389 page_title: "Importer" 404 page_title: "Importer"
@@ -417,7 +432,7 @@ import:
417 how_to: "Choisissez le fichier de votre export Readability et cliquez sur le bouton ci-dessous pour l’importer." 432 how_to: "Choisissez le fichier de votre export Readability et cliquez sur le bouton ci-dessous pour l’importer."
418 worker: 433 worker:
419 enabled: "Les imports sont asynchrones. Une fois l’import commencé un worker externe traitera les messages un par un. Le service activé est :" 434 enabled: "Les imports sont asynchrones. Une fois l’import commencé un worker externe traitera les messages un par un. Le service activé est :"
420 download_images_warning: "Vous avez configuré le téléchagement des images pour vos articles. Combiné à l'import classique, cette opération peut être très très longue (voire échouer). Nous vous conseillons <strong>vivement</strong> d'activer les imports asynchrones." 435 download_images_warning: "Vous avez configuré le téléchagement des images pour vos articles. Combiné à l’import classique, cette opération peut être très très longue (voire échouer). Nous vous conseillons <strong>vivement</strong> d’activer les imports asynchrones."
421 firefox: 436 firefox:
422 page_title: "Import > Firefox" 437 page_title: "Import > Firefox"
423 description: "Cet outil va vous permettre d’importer tous vos marques-pages de Firefox. Ouvrez le panneau des marques-pages (Ctrl+Maj+O), puis dans « Importation et sauvegarde », choisissez « Sauvegarde… ». Vous allez récupérer un fichier .json. </p>" 438 description: "Cet outil va vous permettre d’importer tous vos marques-pages de Firefox. Ouvrez le panneau des marques-pages (Ctrl+Maj+O), puis dans « Importation et sauvegarde », choisissez « Sauvegarde… ». Vous allez récupérer un fichier .json. </p>"
@@ -486,16 +501,16 @@ developer:
486 back: "Retour" 501 back: "Retour"
487 502
488user: 503user:
489 page_title: Gestion des utilisateurs 504 page_title: "Gestion des utilisateurs"
490 new_user: Créer un nouvel utilisateur 505 new_user: "Créer un nouvel utilisateur"
491 edit_user: Éditer un utilisateur existant 506 edit_user: "Éditer un utilisateur existant"
492 description: Ici vous pouvez gérer vos utilisateurs (création, mise à jour et suppression) 507 description: "Ici vous pouvez gérer vos utilisateurs (création, mise à jour et suppression)"
493 list: 508 list:
494 actions: Actions 509 actions: "Actions"
495 edit_action: Éditer 510 edit_action: "Éditer"
496 yes: Oui 511 yes: "Oui"
497 no: Non 512 no: "Non"
498 create_new_one: Créer un nouvel utilisateur 513 create_new_one: "Créer un nouvel utilisateur"
499 form: 514 form:
500 username_label: "Nom d’utilisateur" 515 username_label: "Nom d’utilisateur"
501 name_label: "Nom" 516 name_label: "Nom"
@@ -508,11 +523,33 @@ user:
508 twofactor_label: "Double authentification" 523 twofactor_label: "Double authentification"
509 save: "Sauvegarder" 524 save: "Sauvegarder"
510 delete: "Supprimer" 525 delete: "Supprimer"
511 delete_confirm: "Voulez-vous vraiment ?" 526 delete_confirm: "Êtes-vous sûr ?"
527 back_to_list: "Revenir à la liste"
528 search:
529 placeholder: "Filtrer par nom d’utilisateur ou email"
530
531site_credential:
532 page_title: Gestion des accès aux sites
533 new_site_credential: Créer un accès à un site
534 edit_site_credential: Éditer l'accès d'un site
535 description: "Ici vous pouvez gérer les accès aux différents sites. Ces accès permettent de récupérer des contenus sur des sites qui requièrent une authentification ou un paywall"
536 list:
537 actions: Actions
538 edit_action: Éditer
539 yes: Oui
540 no: Non
541 create_new_one: Créer un nouvel accès à un site
542 form:
543 username_label: 'Identifiant'
544 host_label: 'Domaine'
545 password_label: 'Mot de passe'
546 save: "Sauvegarder"
547 delete: "Supprimer"
548 delete_confirm: "Êtes-vous sûr ?"
512 back_to_list: "Revenir à la liste" 549 back_to_list: "Revenir à la liste"
513 550
514error: 551error:
515 page_title: Une erreur est survenue 552 page_title: "Une erreur est survenue"
516 553
517flashes: 554flashes:
518 config: 555 config:
@@ -525,9 +562,10 @@ flashes:
525 tagging_rules_updated: "Règles mises à jour" 562 tagging_rules_updated: "Règles mises à jour"
526 tagging_rules_deleted: "Règle supprimée" 563 tagging_rules_deleted: "Règle supprimée"
527 rss_token_updated: "Jeton RSS mis à jour" 564 rss_token_updated: "Jeton RSS mis à jour"
528 annotations_reset: Annotations supprimées 565 annotations_reset: "Annotations supprimées"
529 tags_reset: Tags supprimés 566 tags_reset: "Tags supprimés"
530 entries_reset: Articles supprimés 567 entries_reset: "Articles supprimés"
568 archived_reset: "Articles archivés supprimés"
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: "Article déjà sauvegardé le %date%" 571 entry_already_saved: "Article déjà sauvegardé le %date%"
@@ -562,3 +600,8 @@ flashes:
562 added: 'Utilisateur "%username%" ajouté' 600 added: 'Utilisateur "%username%" ajouté'
563 updated: 'Utilisateur "%username%" mis à jour' 601 updated: 'Utilisateur "%username%" mis à jour'
564 deleted: 'Utilisateur "%username%" supprimé' 602 deleted: 'Utilisateur "%username%" supprimé'
603 site_credential:
604 notice:
605 added: 'Accès au site "%host%" ajouté'
606 updated: 'Accès au site "%host%" mis à jour'
607 deleted: 'Accès au site "%host%" supprimé'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
index 3cd3fd17..c53266ca 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
@@ -91,9 +91,10 @@ config:
91 token_reset: 'Rigenera il tuo token' 91 token_reset: 'Rigenera il tuo token'
92 rss_links: 'Collegamenti RSS' 92 rss_links: 'Collegamenti RSS'
93 rss_link: 93 rss_link:
94 unread: 'non letti' 94 unread: 'Non letti'
95 starred: 'preferiti' 95 starred: 'Preferiti'
96 archive: 'archiviati' 96 archive: 'Archiviati'
97 # all: 'All'
97 rss_limit: 'Numero di elementi nel feed' 98 rss_limit: 'Numero di elementi nel feed'
98 form_user: 99 form_user:
99 two_factor_description: "Abilitando l'autenticazione a due fattori riceverai una e-mail con un codice per ogni nuova connesione non verificata" 100 two_factor_description: "Abilitando l'autenticazione a due fattori riceverai una e-mail con un codice per ogni nuova connesione non verificata"
@@ -112,6 +113,7 @@ config:
112 annotations: Rimuovi TUTTE le annotazioni 113 annotations: Rimuovi TUTTE le annotazioni
113 tags: Rimuovi TUTTE le etichette 114 tags: Rimuovi TUTTE le etichette
114 entries: Rimuovi TUTTI gli articoli 115 entries: Rimuovi TUTTI gli articoli
116 # archived: Remove ALL archived entries
115 confirm: Sei veramente sicuro? (NON PUOI TORNARE INDIETRO) 117 confirm: Sei veramente sicuro? (NON PUOI TORNARE INDIETRO)
116 form_password: 118 form_password:
117 description: "Qui puoi cambiare la tua password. La tua nuova password dovrebbe essere composta da almeno 8 caratteri." 119 description: "Qui puoi cambiare la tua password. La tua nuova password dovrebbe essere composta da almeno 8 caratteri."
@@ -156,6 +158,7 @@ config:
156 or: "Una regola O un'altra" 158 or: "Una regola O un'altra"
157 and: "Una regola E un'altra" 159 and: "Una regola E un'altra"
158 matches: 'Verifica che un <i>oggetto</i> risulti in una <i>ricerca</i> (case-insensitive).<br />Esempio: <code>titolo contiene "football"</code>' 160 matches: 'Verifica che un <i>oggetto</i> risulti in una <i>ricerca</i> (case-insensitive).<br />Esempio: <code>titolo contiene "football"</code>'
161 # notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
159 162
160entry: 163entry:
161 page_titles: 164 page_titles:
@@ -166,6 +169,7 @@ entry:
166 filtered_tags: 'Filtrati per etichetta:' 169 filtered_tags: 'Filtrati per etichetta:'
167 filtered_search: 'Filtrati per ricerca:' 170 filtered_search: 'Filtrati per ricerca:'
168 untagged: 'Articoli non etichettati' 171 untagged: 'Articoli non etichettati'
172 all: 'Tutti gli articoli'
169 list: 173 list:
170 number_on_the_page: "{0} Non ci sono contenuti.|{1} C'è un contenuto.|]1,Inf[ Ci sono %count% contenuti." 174 number_on_the_page: "{0} Non ci sono contenuti.|{1} C'è un contenuto.|]1,Inf[ Ci sono %count% contenuti."
171 reading_time: 'tempo di lettura stimato' 175 reading_time: 'tempo di lettura stimato'
@@ -187,6 +191,8 @@ entry:
187 unread_label: 'Non letti' 191 unread_label: 'Non letti'
188 preview_picture_label: "Ha un'immagine di anteprima" 192 preview_picture_label: "Ha un'immagine di anteprima"
189 preview_picture_help: 'Immagine di anteprima' 193 preview_picture_help: 'Immagine di anteprima'
194 # is_public_label: 'Has a public link'
195 # is_public_help: 'Public link'
190 language_label: 'Lingua' 196 language_label: 'Lingua'
191 http_status_label: 'Stato HTTP' 197 http_status_label: 'Stato HTTP'
192 reading_time: 198 reading_time:
@@ -211,7 +217,7 @@ entry:
211 view_original_article: 'Contenuto originale' 217 view_original_article: 'Contenuto originale'
212 re_fetch_content: 'Ri-ottieni pagina' 218 re_fetch_content: 'Ri-ottieni pagina'
213 delete: 'Elimina' 219 delete: 'Elimina'
214 add_a_tag: 'Aggiungi un tag' 220 add_a_tag: 'Aggiungi un''etichetta'
215 share_content: 'Condividi' 221 share_content: 'Condividi'
216 share_email_label: 'E-mail' 222 share_email_label: 'E-mail'
217 public_link: 'Link pubblico' 223 public_link: 'Link pubblico'
@@ -222,7 +228,7 @@ entry:
222 label: 'Problemi?' 228 label: 'Problemi?'
223 description: 'Questo contenuto viene visualizzato male?' 229 description: 'Questo contenuto viene visualizzato male?'
224 edit_title: 'Modifica titolo' 230 edit_title: 'Modifica titolo'
225 original_article: 'originale' 231 original_article: 'Originale'
226 annotations_on_the_entry: '{0} Nessuna annotazione|{1} Una annotazione|]1,Inf[ %count% annotazioni' 232 annotations_on_the_entry: '{0} Nessuna annotazione|{1} Una annotazione|]1,Inf[ %count% annotazioni'
227 created_at: 'Data di creazione' 233 created_at: 'Data di creazione'
228 published_at: 'Data di pubblicazione' 234 published_at: 'Data di pubblicazione'
@@ -238,13 +244,15 @@ entry:
238 page_title: 'Modifica voce' 244 page_title: 'Modifica voce'
239 title_label: 'Titolo' 245 title_label: 'Titolo'
240 url_label: 'Url' 246 url_label: 'Url'
241 is_public_label: 'Pubblico'
242 save_label: 'Salva' 247 save_label: 'Salva'
243 public: 248 public:
244 shared_by_wallabag: "Questo articolo è stato condiviso da %username% con <a href='%wallabag_instance%'>wallabag</a>" 249 shared_by_wallabag: "Questo articolo è stato condiviso da %username% con <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 delete: "Vuoi veramente rimuovere quell'articolo?"
252 delete_tag: "Vuoi veramente rimuovere quell'etichetta da quell'articolo?"
245 253
246about: 254about:
247 page_title: 'About' 255 page_title: 'A proposito'
248 top_menu: 256 top_menu:
249 who_behind_wallabag: "Chi c'è dietro a wallabag" 257 who_behind_wallabag: "Chi c'è dietro a wallabag"
250 getting_help: 'Ottieni aiuto' 258 getting_help: 'Ottieni aiuto'
@@ -263,7 +271,7 @@ about:
263 bug_reports: 'Bug reports' 271 bug_reports: 'Bug reports'
264 support: '<a href="https://github.com/wallabag/wallabag/issues">su GitHub</a>' 272 support: '<a href="https://github.com/wallabag/wallabag/issues">su GitHub</a>'
265 helping: 273 helping:
266 description: 'wallabag è gratuito opensource. Puoi aiutarci:' 274 description: 'wallabag è gratuito ed OpenSource. Puoi aiutarci:'
267 by_contributing: 'per contribuire al progetto:' 275 by_contributing: 'per contribuire al progetto:'
268 by_contributing_2: 'un elenco delle attività richieste' 276 by_contributing_2: 'un elenco delle attività richieste'
269 by_paypal: 'via Paypal' 277 by_paypal: 'via Paypal'
@@ -331,7 +339,7 @@ quickstart:
331 more: 'Più…' 339 more: 'Più…'
332 intro: 340 intro:
333 title: 'Benvenuto su wallabag!' 341 title: 'Benvenuto su wallabag!'
334 paragraph_1: "Un tour in cui ti guideremo per scoprire e che ti mostrerà delle funzionalità che potrebbero interessarti." 342 paragraph_1: "Ti accompagneremo alla scoperta di wallabag e ti mostreremo delle funzionalità che potrebbero interessarti."
335 paragraph_2: 'Seguici!' 343 paragraph_2: 'Seguici!'
336 configure: 344 configure:
337 title: "Configura l'applicazione" 345 title: "Configura l'applicazione"
@@ -389,6 +397,9 @@ tag:
389 add: 'Aggiungi' 397 add: 'Aggiungi'
390 placeholder: 'Puoi aggiungere varie etichette, separate da una virgola.' 398 placeholder: 'Puoi aggiungere varie etichette, separate da una virgola.'
391 399
400# export:
401# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
392import: 403import:
393 page_title: 'Importa' 404 page_title: 'Importa'
394 page_description: "Benvenuto nell'importatore di wallabag. Seleziona il servizio da cui vuoi trasferire i contenuti." 405 page_description: "Benvenuto nell'importatore di wallabag. Seleziona il servizio da cui vuoi trasferire i contenuti."
@@ -401,20 +412,20 @@ import:
401 save_label: 'Carica file' 412 save_label: 'Carica file'
402 pocket: 413 pocket:
403 page_title: 'Importa da > Pocket' 414 page_title: 'Importa da > Pocket'
404 description: "Questo importatore copierà tutti i tuoi dati da Pocket. Pocket non ci consente di ottenere contenuti dal loro servzio, così il contenuto leggibile di ogni articolo verrà ri-ottenuto da wallabag." 415 description: "Questo importatore copierà tutti i tuoi dati da Pocket. Pocket non ci consente di ottenere contenuti dal loro servizio, così il contenuto leggibile di ogni articolo verrà ri-ottenuto da wallabag."
405 config_missing: 416 config_missing:
406 description: "Importazione da Pocket non configurata." 417 description: "Importazione da Pocket non configurata."
407 admin_message: 'Devi definire %keyurls% una pocket_consumer_key %keyurle%.' 418 admin_message: 'Devi definire %keyurls% una pocket_consumer_key %keyurle%.'
408 user_message: 'Il tuo amministratore di server deve define una API Key per Pocket.' 419 user_message: 'Il tuo amministratore del server deve definire una API Key per Pocket.'
409 authorize_message: 'Puoi importare dati dal tuo account Pocket. Devi solo cliccare sul pulsante sottostante e autorizzare la connessione a getpocket.com.' 420 authorize_message: 'Puoi importare dati dal tuo account Pocket. Devi solo cliccare sul pulsante sottostante e autorizzare la connessione a getpocket.com.'
410 connect_to_pocket: 'Connetti a Pocket and importa i dati' 421 connect_to_pocket: 'Connetti a Pocket and importa i dati'
411 wallabag_v1: 422 wallabag_v1:
412 page_title: 'Importa da > Wallabag v1' 423 page_title: 'Importa da > Wallabag v1'
413 description: 'Questo importatore copierà tutti i tuoi dati da un wallabag v1. Nella tua pagina di configurazione, clicca su "JSON export" nella sezione "Esport i tuoi dati di wallabag". Otterrai un file "wallabag-export-1-xxxx-xx-xx.json".' 424 description: 'Questo importatore copierà tutti i tuoi dati da un wallabag v1. Nella tua pagina di configurazione, clicca su "JSON export" nella sezione "Esporta i tuoi dati di wallabag". Otterrai un file "wallabag-export-1-xxxx-xx-xx.json".'
414 how_to: 'Seleziona la tua esportazione di wallabag e clicca sul pulsante sottostante caricare il file e importare i dati.' 425 how_to: 'Seleziona la tua esportazione di wallabag e clicca sul pulsante sottostante caricare il file e importare i dati.'
415 wallabag_v2: 426 wallabag_v2:
416 page_title: 'Importa da > Wallabag v2' 427 page_title: 'Importa da > Wallabag v2'
417 description: 'Questo importatore copierà tutti i tuoi dati da un wallabag v2. Vai in "Tutti i contenuti", e, nella sidebar di esportazione, clicca su "JSON". Otterrai un file "Tutti i contenuti.json".' 428 description: 'Questo importatore copierà tutti i tuoi dati da un wallabag v2. Vai in "Tutti i contenuti", e, nella barra laterale di esportazione, clicca su "JSON". Otterrai un file "Tutti i contenuti.json'
418 readability: 429 readability:
419 page_title: 'Importa da > Readability' 430 page_title: 'Importa da > Readability'
420 description: 'Questo importatore copierà tutti i tuoi articoli da Readability. Nella pagina strumenti (https://www.readability.com/tools/), clicca su "Export your data" nella sezione "Data Export". Riceverai una E-mail per scaricare un file json (che tuttavia non termina con .json).' 431 description: 'Questo importatore copierà tutti i tuoi articoli da Readability. Nella pagina strumenti (https://www.readability.com/tools/), clicca su "Export your data" nella sezione "Data Export". Riceverai una E-mail per scaricare un file json (che tuttavia non termina con .json).'
@@ -514,6 +525,28 @@ user:
514 delete: Cancella 525 delete: Cancella
515 delete_confirm: Sei sicuro? 526 delete_confirm: Sei sicuro?
516 back_to_list: Torna alla lista 527 back_to_list: Torna alla lista
528 search:
529 # placeholder: Filter by username or email
530
531site_credential:
532 # page_title: Site credentials management
533 # new_site_credential: Create a credential
534 # edit_site_credential: Edit an existing credential
535 # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc."
536 # list:
537 # actions: Actions
538 # edit_action: Edit
539 # yes: Yes
540 # no: No
541 # create_new_one: Create a new credential
542 # form:
543 # username_label: 'Username'
544 # host_label: 'Host'
545 # password_label: 'Password'
546 # save: Save
547 # delete: Delete
548 # delete_confirm: Are you sure?
549 # back_to_list: Back to list
517 550
518error: 551error:
519 page_title: Si è verificato un errore 552 page_title: Si è verificato un errore
@@ -532,6 +565,7 @@ flashes:
532 annotations_reset: Reset annotazioni 565 annotations_reset: Reset annotazioni
533 tags_reset: Reset etichette 566 tags_reset: Reset etichette
534 entries_reset: Reset articoli 567 entries_reset: Reset articoli
568 # archived_reset: Archived entries deleted
535 entry: 569 entry:
536 notice: 570 notice:
537 entry_already_saved: 'Contenuto già salvato in data %date%' 571 entry_already_saved: 'Contenuto già salvato in data %date%'
@@ -566,3 +600,8 @@ flashes:
566 added: 'Utente "%username%" aggiunto' 600 added: 'Utente "%username%" aggiunto'
567 updated: 'Utente "%username%" aggiornato' 601 updated: 'Utente "%username%" aggiornato'
568 deleted: 'Utente "%username%" eliminato' 602 deleted: 'Utente "%username%" eliminato'
603 site_credential:
604 notice:
605 # added: 'Site credential for "%host%" added'
606 # updated: 'Site credential for "%host%" updated'
607 # deleted: 'Site credential for "%host%" deleted'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
index 913e3bcb..3ae64c49 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
@@ -1,8 +1,8 @@
1security: 1security:
2 login: 2 login:
3 page_title: 'Benvenguda sus wallabag !' 3 page_title: 'Vos desirem la benvenguda a wallabag!'
4 keep_logged_in: 'Demorar connectat' 4 keep_logged_in: 'Demorar connectat'
5 forgot_password: 'Senhal doblidat ?' 5 forgot_password: 'Senhal oblidat?'
6 submit: 'Se connectar' 6 submit: 'Se connectar'
7 register: 'Crear un compte' 7 register: 'Crear un compte'
8 username: "Nom d'utilizaire" 8 username: "Nom d'utilizaire"
@@ -11,8 +11,8 @@ security:
11 resetting: 11 resetting:
12 description: "Picatz vòstra adreça de corrièl çai-jos, vos mandarem las instruccions per reïnicializar vòstre senhal." 12 description: "Picatz vòstra adreça de corrièl çai-jos, vos mandarem las instruccions per reïnicializar vòstre senhal."
13 register: 13 register:
14 page_title: 'Se crear un compte' 14 page_title: 'Crear un compte'
15 go_to_account: 'Anar sus vòstre compte' 15 go_to_account: 'Anar a vòstre compte'
16 16
17menu: 17menu:
18 left: 18 left:
@@ -22,7 +22,7 @@ menu:
22 all_articles: 'Totes los articles' 22 all_articles: 'Totes los articles'
23 config: 'Configuracion' 23 config: 'Configuracion'
24 tags: 'Etiquetas' 24 tags: 'Etiquetas'
25 internal_settings: 'Configuracion interna' 25 internal_settings: 'Configuracion intèrna'
26 import: 'Importar' 26 import: 'Importar'
27 howto: 'Ajuda' 27 howto: 'Ajuda'
28 developer: 'Gestion dels clients API' 28 developer: 'Gestion dels clients API'
@@ -32,6 +32,7 @@ menu:
32 save_link: 'Enregistrar un novèl article' 32 save_link: 'Enregistrar un novèl article'
33 back_to_unread: 'Tornar als articles pas legits' 33 back_to_unread: 'Tornar als articles pas legits'
34 users_management: 'Gestion dels utilizaires' 34 users_management: 'Gestion dels utilizaires'
35 site_credentials: 'Identificants del site'
35 top: 36 top:
36 add_new_entry: 'Enregistrar un novèl article' 37 add_new_entry: 'Enregistrar un novèl article'
37 search: 'Cercar' 38 search: 'Cercar'
@@ -46,7 +47,7 @@ footer:
46 social: 'Social' 47 social: 'Social'
47 powered_by: 'propulsat per' 48 powered_by: 'propulsat per'
48 about: 'A prepaus' 49 about: 'A prepaus'
49 stats: "Dempuèi %user_creation% avètz legit %nb_archives% articles. Es a l'entorn de %per_day% per jorn !" 50 stats: "Dempuèi %user_creation% avètz legit %nb_archives% articles. Es a l'entorn de %per_day% per jorn!"
50 51
51config: 52config:
52 page_title: 'Configuracion' 53 page_title: 'Configuracion'
@@ -65,17 +66,18 @@ config:
65 language_label: 'Lenga' 66 language_label: 'Lenga'
66 reading_speed: 67 reading_speed:
67 label: 'Velocitat de lectura' 68 label: 'Velocitat de lectura'
68 help_message: 'Podètz utilizar una aisina en linha per estimar vòstra velocitat de lectura :' 69 help_message: 'Podètz utilizar una aisina en linha per estimar vòstra velocitat de lectura:'
69 100_word: "Legissi a l'entorn de 100 mots per minuta" 70 100_word: "Legissi a l'entorn de 100 mots per minuta"
70 200_word: "Legissi a l'entorn de 200 mots per minuta" 71 200_word: "Legissi a l'entorn de 200 mots per minuta"
71 300_word: "Legissi a l'entorn de 300 mots per minuta" 72 300_word: "Legissi a l'entorn de 300 mots per minuta"
72 400_word: "Legissi a l'entorn de 400 mots per minuta" 73 400_word: "Legissi a l'entorn de 400 mots per minuta"
73 action_mark_as_read: 74 action_mark_as_read:
74 label: 'Ont volètz èsser menat aprèp aver marcat un article coma legit ?' 75 label: 'Ont volètz èsser menat aprèp aver marcat un article coma legit?'
75 redirect_homepage: "A la pagina d'acuèlh" 76 redirect_homepage: "A la pagina dacuèlh"
76 redirect_current_page: 'A la pagina actuala' 77 redirect_current_page: 'A la pagina actuala'
77 pocket_consumer_key_label: Clau d'autentificacion Pocket per importar las donadas 78 pocket_consumer_key_label: Clau dautentificacion Pocket per importar las donadas
78 android_configuration: Configuratz vòstra aplicacion Android 79 android_configuration: Configuratz vòstra aplicacion Android
80 android_instruction: "Tocatz aquí per garnir las informacions de l'aplicacion Android"
79 help_theme: "wallabag es personalizable. Podètz causir vòstre tèma preferit aquí." 81 help_theme: "wallabag es personalizable. Podètz causir vòstre tèma preferit aquí."
80 help_items_per_page: "Podètz cambiar lo nombre d'articles afichats per pagina." 82 help_items_per_page: "Podètz cambiar lo nombre d'articles afichats per pagina."
81 help_reading_speed: "wallabag calcula lo temps de lectura per cada article. Podètz lo definir aquí, gràcias a aquesta lista, se sètz un legeire rapid o lent. wallabag tornarà calcular lo temps de lectura per cada article." 83 help_reading_speed: "wallabag calcula lo temps de lectura per cada article. Podètz lo definir aquí, gràcias a aquesta lista, se sètz un legeire rapid o lent. wallabag tornarà calcular lo temps de lectura per cada article."
@@ -87,22 +89,23 @@ config:
87 no_token: 'Pas cap de geton generat' 89 no_token: 'Pas cap de geton generat'
88 token_create: 'Creatz vòstre geton' 90 token_create: 'Creatz vòstre geton'
89 token_reset: 'Reïnicializatz vòstre geton' 91 token_reset: 'Reïnicializatz vòstre geton'
90 rss_links: 'URL de vòstres fluxes RSS' 92 rss_links: 'URLs de vòstres fluxes RSS'
91 rss_link: 93 rss_link:
92 unread: 'pas legits' 94 unread: 'Pas legits'
93 starred: 'favorits' 95 starred: 'Favorits'
94 archive: 'legits' 96 archive: 'Legits'
97 all: 'Totes'
95 rss_limit: "Nombre d'articles dins un flux RSS" 98 rss_limit: "Nombre d'articles dins un flux RSS"
96 form_user: 99 form_user:
97 two_factor_description: "Activar l'autentificacion doble-factor vòl dire que recebretz un còdi per corrièl per cada novèla connexion pas aprovada." 100 two_factor_description: "Activar l'autentificacion en dos temps vòl dire que recebretz un còdi per corrièl per cada novèla connexion pas aprovada."
98 name_label: 'Nom' 101 name_label: 'Nom'
99 email_label: 'Adreça de corrièl' 102 email_label: 'Adreça de corrièl'
100 twoFactorAuthentication_label: 'Dobla autentificacion' 103 twoFactorAuthentication_label: 'Dobla autentificacion'
101 help_twoFactorAuthentication: "S'avètz activat 2FA, cada còp que volètz vos connectar a wallabag, recebretz un còdi per corrièl." 104 help_twoFactorAuthentication: "S'avètz activat l'autentificacion en dos temps, cada còp que volètz vos connectar a wallabag, recebretz un còdi per corrièl."
102 delete: 105 delete:
103 title: Suprimir mon compte (Mèfi zòna perilhosa) 106 title: Suprimir mon compte (Mèfi zòna perilhosa)
104 description: Se confirmatz la supression de vòstre compte, TOTES vòstres articles, TOTAS vòstras etiquetas, TOTAS vòstras anotacions e vòstre compte seràn suprimits per totjorn. E aquò es IRREVERSIBLE. Puèi seretz desconnectat. 107 description: Se confirmatz la supression de vòstre compte, TOTES vòstres articles, TOTAS vòstras etiquetas, TOTAS vòstras anotacions e vòstre compte seràn suprimits per totjorn. E aquò es IRREVERSIBLE. Puèi seretz desconnectat.
105 confirm: Sètz vertadièrament segur ? (ES IRREVERSIBLE) 108 confirm: Sètz vertadièrament segur?(AQUÒ ES IRREVERSIBLE)
106 button: Suprimir mon compte 109 button: Suprimir mon compte
107 reset: 110 reset:
108 title: Zòna de reïnicializacion (Mèfi zòna perilhosa) 111 title: Zòna de reïnicializacion (Mèfi zòna perilhosa)
@@ -110,7 +113,8 @@ config:
110 annotations: Levar TOTAS las anotacions 113 annotations: Levar TOTAS las anotacions
111 tags: Levar TOTAS las etiquetas 114 tags: Levar TOTAS las etiquetas
112 entries: Levar TOTES los articles 115 entries: Levar TOTES los articles
113 confirm: Sètz vertadièrament segur ? (ES IRREVERSIBLE) 116 archived: Levar TOTES los articles archivats
117 confirm: Sètz vertadièrament segur ? (AQUÒ ES IRREVERSIBLE)
114 form_password: 118 form_password:
115 description: "Podètz cambiar vòstre senhal aquí. Vòstre senhal deu èsser long d'almens 8 caractèrs." 119 description: "Podètz cambiar vòstre senhal aquí. Vòstre senhal deu èsser long d'almens 8 caractèrs."
116 old_password_label: 'Senhal actual' 120 old_password_label: 'Senhal actual'
@@ -125,12 +129,12 @@ config:
125 tags_label: 'Etiquetas' 129 tags_label: 'Etiquetas'
126 faq: 130 faq:
127 title: 'FAQ' 131 title: 'FAQ'
128 tagging_rules_definition_title: "Qué significa las règlas d'etiquetas automaticas ?" 132 tagging_rules_definition_title: "Qué significa las règlas d'etiquetas automaticas?"
129 tagging_rules_definition_description: "Son de règlas utilizadas per wallabad per classar automaticament vòstres novèls articles.<br />Cada còp qu'un novèl article es apondut, totas las règlas d'etiquetas automaticas seràn utilizadas per ajustar d'etiquetas qu'avètz configuradas, en vos esparnhant l'esfòrç de classificar vòstres articles manualament." 133 tagging_rules_definition_description: "Son de règlas utilizadas per wallabad per classar automaticament vòstres novèls articles.<br />Cada còp qu'un novèl article es apondut, totas las règlas d'etiquetas automaticas seràn utilizadas per ajustar d'etiquetas qu'avètz configuradas, en vos esparnhant l'esfòrç de classificar vòstres articles manualament."
130 how_to_use_them_title: 'Cossí las utilizar ?' 134 how_to_use_them_title: 'Cossí las utilizar?'
131 how_to_use_them_description: "Imaginem que volètz atribuir als novèls article l'etiqueta « <i>lectura corta</i> » quand lo temps per legir es inferior a 3 minutas.<br />Dins aquel cas, deuriatz metre « readingTime &lt;= 3 » dins lo camp <i>Règla</i> e « <i>lectura corta</i> » dins lo camp <i>Etiqueta</i>.<br />Mai d'una etiquetas pòdon èsser apondudas simultanèament ne las separant amb de virgulas : « <i>lectura corta, per ligir</i> »<br />De règlas complèxas pòdon èsser creadas en emplegant d'operators predefinits : se « <i>readingTime &gt;= 5 AND domainName = \"github.com\"</i> » alara atribuir las etiquetas « <i>lectura longa, github </i> »" 135 how_to_use_them_description: "Imaginem que volètz atribuir als novèls article l'etiqueta « <i>lectura corta</i> » quand lo temps per legir es inferior a 3 minutas.<br />Dins aquel cas, deuriatz metre « readingTime &lt;= 3 » dins lo camp <i>Règla</i> e « <i>lectura corta</i> » dins lo camp <i>Etiqueta</i>.<br />Mai d'una etiquetas pòdon èsser apondudas simultanèament ne las separant amb de virgulas : « <i>lectura corta, per ligir</i> »<br />De règlas complèxas pòdon èsser creadas n'emplegant d'operators predefinits : se « <i>readingTime &gt;= 5 AND domainName = \"github.com\"</i> » alara atribuir las etiquetas « <i>lectura longa, github </i> »"
132 variables_available_title: 'Quinas variablas e operators pòdi utilizar per escriure de règlas ?' 136 variables_available_title: 'Quinas variablas e operators pòdi utilizar per escriure de règlas?'
133 variables_available_description: "Las variablas e operators seguents pòdon èsser utilizats per escriure de règlas d'etiquetas automaticas :" 137 variables_available_description: "Las variablas e operators seguents pòdon èsser utilizats per escriure de règlas d'etiquetas automaticas : "
134 meaning: 'Significacion' 138 meaning: 'Significacion'
135 variable_description: 139 variable_description:
136 label: 'Variabla' 140 label: 'Variabla'
@@ -153,7 +157,8 @@ config:
153 not_equal_to: 'Diferent de…' 157 not_equal_to: 'Diferent de…'
154 or: "Una règla O l'autra" 158 or: "Una règla O l'autra"
155 and: "Una règla E l'autra" 159 and: "Una règla E l'autra"
156 matches: 'Teste se un <i>subjècte</i> correspond a una <i>recerca</i> (non sensibla a la cassa).<br />Exemple : <code>title matches \"football\"</code>' 160 matches: 'Teste se un <i>subjècte</i> correspond a una <i>recèrca</i> (non sensibla a la cassa).<br />Exemple : <code>title matches \"football\"</code>'
161 notmatches: 'Teste se <i>subjècte</i> correspond pas a una <i>recèrca</i> (sensibla a la cassa).<br />Example : <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -161,14 +166,15 @@ entry:
161 starred: 'Articles favorits' 166 starred: 'Articles favorits'
162 archived: 'Articles legits' 167 archived: 'Articles legits'
163 filtered: 'Articles filtrats' 168 filtered: 'Articles filtrats'
164 filtered_tags: 'Articles filtrats per etiquetas :' 169 filtered_tags: 'Articles filtrats per etiquetas:'
165 filtered_search: 'Articles filtrats per recèrca :' 170 filtered_search: 'Articles filtrats per recèrca:'
166 untagged: 'Articles sens etiqueta' 171 untagged: 'Articles sens etiqueta'
172 all: 'Totes los articles'
167 list: 173 list:
168 number_on_the_page: "{0} I a pas cap d'article.|{1} I a un article.|]1,Inf[ I a %count% articles." 174 number_on_the_page: "{0} I a pas cap d'article.|{1} I a un article.|]1,Inf[ I a %count% articles."
169 reading_time: 'durada de lectura' 175 reading_time: 'durada de lectura'
170 reading_time_minutes: 'durada de lectura : %readingTime% min' 176 reading_time_minutes: 'durada de lectura:%readingTime% min'
171 reading_time_less_one_minute: 'durada de lectura : &lt; 1 min' 177 reading_time_less_one_minute: 'durada de lectura:&lt; 1 min'
172 number_of_tags: '{1}e una autra etiqueta|]1,Inf[e %count% autras etiquetas' 178 number_of_tags: '{1}e una autra etiqueta|]1,Inf[e %count% autras etiquetas'
173 reading_time_minutes_short: '%readingTime% min' 179 reading_time_minutes_short: '%readingTime% min'
174 reading_time_less_one_minute_short: '&lt; 1 min' 180 reading_time_less_one_minute_short: '&lt; 1 min'
@@ -183,8 +189,10 @@ entry:
183 archived_label: 'Legits' 189 archived_label: 'Legits'
184 starred_label: 'Favorits' 190 starred_label: 'Favorits'
185 unread_label: 'Pas legits' 191 unread_label: 'Pas legits'
186 preview_picture_label: 'A una fotò' 192 preview_picture_label: 'A un imatge'
187 preview_picture_help: 'Fotò' 193 preview_picture_help: 'Imatge'
194 is_public_label: 'Ten un ligam public'
195 is_public_help: 'Ligam public'
188 language_label: 'Lenga' 196 language_label: 'Lenga'
189 http_status_label: 'Estatut HTTP' 197 http_status_label: 'Estatut HTTP'
190 reading_time: 198 reading_time:
@@ -205,7 +213,7 @@ entry:
205 back_to_homepage: 'Tornar' 213 back_to_homepage: 'Tornar'
206 set_as_read: 'Marcar coma legit' 214 set_as_read: 'Marcar coma legit'
207 set_as_unread: 'Marcar coma pas legit' 215 set_as_unread: 'Marcar coma pas legit'
208 set_as_starred: 'Metre en favori' 216 set_as_starred: 'Metre en favorit'
209 view_original_article: 'Article original' 217 view_original_article: 'Article original'
210 re_fetch_content: 'Tornar cargar lo contengut' 218 re_fetch_content: 'Tornar cargar lo contengut'
211 delete: 'Suprimir' 219 delete: 'Suprimir'
@@ -217,27 +225,31 @@ entry:
217 export: 'Exportar' 225 export: 'Exportar'
218 print: 'Imprimir' 226 print: 'Imprimir'
219 problem: 227 problem:
220 label: 'Un problèma ?' 228 label: 'Un problèma?'
221 description: "Marca mal la presentacion d'aqueste article ?" 229 description: "Marca mal la presentacion d'aqueste article?"
222 edit_title: 'Modificar lo títol' 230 edit_title: 'Modificar lo títol'
223 original_article: 'original' 231 original_article: 'original'
224 annotations_on_the_entry: "{0} Pas cap d'anotacion|{1} Una anotacion|]1,Inf[ %count% anotacions" 232 annotations_on_the_entry: "{0} Pas cap d'anotacion|{1} Una anotacion|]1,Inf[ %count% anotacions"
225 created_at: 'Data de creacion' 233 created_at: 'Data de creacion'
234 published_at: 'Data de publicacion'
235 published_by: 'Publicat per'
226 new: 236 new:
227 page_title: 'Enregistrar un novèl article' 237 page_title: 'Enregistrar un novèl article'
228 placeholder: 'http://website.com' 238 placeholder: 'http://website.com'
229 form_new: 239 form_new:
230 url_label: Url 240 url_label: Url
231 search: 241 search:
232 placeholder: 'Qué cercatz ?' 242 placeholder: 'Qué cercatz?'
233 edit: 243 edit:
234 page_title: 'Modificar un article' 244 page_title: 'Modificar un article'
235 title_label: 'Títol' 245 title_label: 'Títol'
236 url_label: 'Url' 246 url_label: 'Url'
237 is_public_label: 'Public'
238 save_label: 'Enregistrar' 247 save_label: 'Enregistrar'
239 public: 248 public:
240 shared_by_wallabag: "Aqueste article es estat partejat per <a href='%wallabag_instance%'>wallabag</a>" 249 shared_by_wallabag: "Aqueste article es estat partejat per <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 delete: "Sètz segur de voler suprimir aqueste article ?"
252 delete_tag: "Sètz segur de voler levar aquesta etiqueta de l'article ?"
241 253
242about: 254about:
243 page_title: 'A prepaus' 255 page_title: 'A prepaus'
@@ -259,27 +271,27 @@ about:
259 bug_reports: 'Rapòrt de bugs' 271 bug_reports: 'Rapòrt de bugs'
260 support: "<a href=\"https://github.com/wallabag/wallabag/issues\">sur GitHub</a>" 272 support: "<a href=\"https://github.com/wallabag/wallabag/issues\">sur GitHub</a>"
261 helping: 273 helping:
262 description: 'wallabag es a gratuit e opensource. Nos podètz ajudar :' 274 description: 'wallabag es a gratuit e opensource. Nos podètz ajudar:'
263 by_contributing: 'en ajudant lo projècte :' 275 by_contributing: 'en participant lo projècte:'
264 by_contributing_2: 'un bilhet recensa totes nòstres besonhs' 276 by_contributing_2: 'un bilhet recensa totes nòstres besonhs'
265 by_paypal: 'via Paypal' 277 by_paypal: 'via Paypal'
266 contributors: 278 contributors:
267 description: "Mercés als contributors de l'aplicacion web de wallabag" 279 description: "Mercés als contributors de l'aplicacion web de wallabag"
268 third_party: 280 third_party:
269 description: 'Aquí la lista de las dependéncias utilizadas dins wallabag (e lor licéncia) :' 281 description: 'Aquí la lista de las dependéncias utilizadas dins wallabag (e lor licéncia):'
270 package: 'Dependéncia' 282 package: 'Dependéncia'
271 license: 'Licéncia' 283 license: 'Licéncia'
272 284
273howto: 285howto:
274 page_title: 'Ajuda' 286 page_title: 'Ajuda'
275 page_description: "I a mai d'un biais d'enregistrar un article :" 287 page_description: "I a mai d'un biais d'enregistrar un article:"
276 tab_menu: 288 tab_menu:
277 add_link: "Ajustar un ligam" 289 add_link: "Ajustar un ligam"
278 shortcuts: "Utilizar d'acorchis" 290 shortcuts: "Utilizar d'acorchis"
279 top_menu: 291 top_menu:
280 browser_addons: 'Extensions de navigator' 292 browser_addons: 'Extensions de navigator'
281 mobile_apps: 'Aplicacions mobil' 293 mobile_apps: 'Aplicacions mobil'
282 bookmarklet: 'Bookmarklet' 294 bookmarklet: 'Marcapaginas'
283 form: 295 form:
284 description: 'Gràcias a aqueste formulari' 296 description: 'Gràcias a aqueste formulari'
285 browser_addons: 297 browser_addons:
@@ -293,7 +305,7 @@ howto:
293 ios: 'sus iTunes Store' 305 ios: 'sus iTunes Store'
294 windows: 'sus Microsoft Store' 306 windows: 'sus Microsoft Store'
295 bookmarklet: 307 bookmarklet:
296 description: 'Lisatz-depausatz aqueste ligam dins vòstra barra de favorits :' 308 description: 'Lisatz-depausatz aqueste ligam dins vòstra barra de favorits:'
297 shortcuts: 309 shortcuts:
298 page_description: Aquí son los acorchis disponibles dins wallabag. 310 page_description: Aquí son los acorchis disponibles dins wallabag.
299 shortcut: Acorchis 311 shortcut: Acorchis
@@ -307,7 +319,7 @@ howto:
307 go_config: Anar a la config 319 go_config: Anar a la config
308 go_import: Anar per importar 320 go_import: Anar per importar
309 go_developers: Anar al canton desvolopaires 321 go_developers: Anar al canton desvolopaires
310 go_howto: Anar a l'ajuda (aquesta quita pagina !) 322 go_howto: Anar a l'ajuda (aquesta quita pagina!)
311 go_logout: Desconnexion 323 go_logout: Desconnexion
312 list_title: Acorchis disponibles dins las paginas de lista 324 list_title: Acorchis disponibles dins las paginas de lista
313 search: Afichar lo formulari de recèrca 325 search: Afichar lo formulari de recèrca
@@ -326,9 +338,9 @@ quickstart:
326 page_title: 'Per ben començar' 338 page_title: 'Per ben començar'
327 more: 'Mai…' 339 more: 'Mai…'
328 intro: 340 intro:
329 title: 'Benvenguda sus wallabag !' 341 title: 'Benvenguda sus wallabag!'
330 paragraph_1: "Anem vos guidar per far lo torn de la proprietat e vos presentar unas fonccionalitats que vos poirián interessar per vos apropriar aquesta aisina." 342 paragraph_1: "Anem vos guidar per far lo torn de la proprietat e vos presentar unas foncionalitats que vos poirián interessar per vos apropriar aquesta aisina."
331 paragraph_2: 'Seguètz-nos !' 343 paragraph_2: 'Seguètz-nos!'
332 configure: 344 configure:
333 title: "Configuratz l'aplicacion" 345 title: "Configuratz l'aplicacion"
334 description: "Per fin d'aver una aplicacion que vos va ben, anatz veire la configuracion de wallabag." 346 description: "Per fin d'aver una aplicacion que vos va ben, anatz veire la configuracion de wallabag."
@@ -337,20 +349,20 @@ quickstart:
337 tagging_rules: 'Escrivètz de règlas per classar automaticament vòstres articles' 349 tagging_rules: 'Escrivètz de règlas per classar automaticament vòstres articles'
338 admin: 350 admin:
339 title: 'Administracion' 351 title: 'Administracion'
340 description: "En qualitat d'adminitrastor sus wallabag, avètz de privilègis que vos permeton de :" 352 description: "En qualitat d'adminitrastor sus wallabag, avètz de privilègis que vos permeton de:"
341 new_user: 'Crear un novèl utilizaire' 353 new_user: 'Crear un novèl utilizaire'
342 analytics: 'Configurar las estadisticas' 354 analytics: 'Configurar las estadisticas'
343 sharing: 'Activar de paramètres de partatge' 355 sharing: 'Activar de paramètres de partatge'
344 export: 'Configurar los expòrt' 356 export: 'Configurar los expòrts'
345 import: 'Configurar los impòrt' 357 import: 'Configurar los impòrts'
346 first_steps: 358 first_steps:
347 title: 'Primièrs passes' 359 title: 'Primièrs passes'
348 description: "Ara wallabag es ben configurat, es lo moment d'archivar lo web. Podètz clicar sul signe + a man drecha amont per ajustar un ligam." 360 description: "Ara wallabag es ben configurat, es lo moment d'archivar lo web. Podètz clicar sul signe + a man drecha amont per ajustar un ligam."
349 new_article: 'Ajustatz vòstre primièr article' 361 new_article: 'Ajustatz vòstre primièr article'
350 unread_articles: 'E racaptatz-lo !' 362 unread_articles: 'E recaptatz-lo!'
351 migrate: 363 migrate:
352 title: 'Migrar dempuèi un servici existent' 364 title: 'Migrar dempuèi un servici existent'
353 description: "Sètz un ancian utilizaire d'un servici existent ? Vos ajudarem a trapar vòstras donadas sus wallabag." 365 description: "Sètz un ancian utilizaire d'un servici existent ? Vos ajudarem a trapar vòstras donadas sus wallabag."
354 pocket: 'Migrar dempuèi Pocket' 366 pocket: 'Migrar dempuèi Pocket'
355 wallabag_v1: 'Migrar dempuèi wallabag v1' 367 wallabag_v1: 'Migrar dempuèi wallabag v1'
356 wallabag_v2: 'Migrar dempuèi wallabag v2' 368 wallabag_v2: 'Migrar dempuèi wallabag v2'
@@ -358,17 +370,17 @@ quickstart:
358 instapaper: 'Migrar dempuèi Instapaper' 370 instapaper: 'Migrar dempuèi Instapaper'
359 developer: 371 developer:
360 title: 'Pels desvolopaires' 372 title: 'Pels desvolopaires'
361 description: 'Avèm tanben pensat als desvolopaires : Docker, API, traduccions, etc.' 373 description: 'Avèm tanben pensat als desvolopaires:Docker, API, traduccions, etc.'
362 create_application: 'Crear vòstra aplicacion tèrça' 374 create_application: 'Crear vòstra aplicacion tèrça'
363 use_docker: 'Utilizar Docker per installar wallabag' 375 use_docker: 'Utilizar Docker per installar wallabag'
364 docs: 376 docs:
365 title: 'Documentacion complèta' 377 title: 'Documentacion complèta'
366 description: "I a un fum de fonccionalitats dins wallabag. Esitetz pas a legir lo manual per las conéisser e aprendre a las utilizar." 378 description: "I a un fum de foncionalitats dins wallabag. Esitetz pas a legir lo manual per las conéisser e aprendre a las utilizar."
367 annotate: 'Anotar vòstre article' 379 annotate: 'Anotar vòstre article'
368 export: 'Convertissètz vòstres articles en ePub o en PDF' 380 export: 'Convertissètz vòstres articles en ePub o en PDF'
369 search_filters: "Aprenètz a utilizar lo motor de recèrca e los filtres per retrobar l'article que vos interèssa" 381 search_filters: "Aprenètz a utilizar lo motor de recèrca e los filtres per retrobar l'article que vos interèssa"
370 fetching_errors: "Qué far se mon article es pas recuperat coma cal ?" 382 fetching_errors: "Qué far se mon article es pas recuperat coma cal?"
371 all_docs: "E encara plen de causas mai !" 383 all_docs: "E encara plen de causas mai!"
372 support: 384 support:
373 title: 'Assisténcia' 385 title: 'Assisténcia'
374 description: 'Perque avètz benlèu besonh de nos pausar una question, sèm disponibles per vosautres.' 386 description: 'Perque avètz benlèu besonh de nos pausar una question, sèm disponibles per vosautres.'
@@ -380,19 +392,22 @@ tag:
380 page_title: 'Etiquetas' 392 page_title: 'Etiquetas'
381 list: 393 list:
382 number_on_the_page: "{0} I a pas cap d'etiquetas.|{1} I a una etiqueta.|]1,Inf[ I a %count% etiquetas." 394 number_on_the_page: "{0} I a pas cap d'etiquetas.|{1} I a una etiqueta.|]1,Inf[ I a %count% etiquetas."
383 see_untagged_entries: "Afichar las entradas sens pas cap d'etiquetas" 395 see_untagged_entries: "Afichar las entradas sens etiquetas"
384 new: 396 new:
385 add: 'Ajustar' 397 add: 'Ajustar'
386 placeholder: "Podètz ajustar mai qu'una etiqueta, separadas per de virgula." 398 placeholder: "Podètz ajustar mai qu'una etiqueta, separadas per de virgula."
387 399
400# export:
401# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
388import: 403import:
389 page_title: 'Importar' 404 page_title: 'Importar'
390 page_description: "Benvenguda sus l'aisina de migracion de wallabag. Causissètz çai-jos lo servici dempuèi lo qual volètz migrar." 405 page_description: "Benvenguda sus l'aisina de migracion de wallabag. Causissètz çai-jos lo servici dempuèi lo qual volètz migrar."
391 action: 406 action:
392 import_contents: 'Importar los contenguts' 407 import_contents: 'Importar lo contengut'
393 form: 408 form:
394 mark_as_read_title: 'Tot marcar coma legit ?' 409 mark_as_read_title: 'O marcar tot coma legit?'
395 mark_as_read_label: 'Marcar tot los contenguts importats coma legits' 410 mark_as_read_label: 'Marcar tot lo contengut importats coma legit'
396 file_label: 'Fichièr' 411 file_label: 'Fichièr'
397 save_label: 'Importar lo fichièr' 412 save_label: 'Importar lo fichièr'
398 pocket: 413 pocket:
@@ -406,25 +421,25 @@ import:
406 connect_to_pocket: 'Se connectar a Pocket e importar las donadas' 421 connect_to_pocket: 'Se connectar a Pocket e importar las donadas'
407 wallabag_v1: 422 wallabag_v1:
408 page_title: 'Importar > Wallabag v1' 423 page_title: 'Importar > Wallabag v1'
409 description: 'Aquesta aisina importarà totas vòstras donadas de wallabag v1. Sus vòstre pagina de configuracion de wallabag v1, clicatz sus \"Export JSON\" dins la seccion \"Exportar vòstras donadas de wallabag\". Traparatz un fichièr \"wallabag-export-1-xxxx-xx-xx.json\".' 424 description: 'Aquesta aisina importarà totas vòstras donadas de wallabag v1. Sus vòstre pagina de configuracion de wallabag v1, clicatz sus \"Export JSON\" dins la seccion \"Exportar vòstras donadas de wallabag\". Traparetz un fichièr \"wallabag-export-1-xxxx-xx-xx.json\".'
410 how_to: "Causissètz lo fichièr de vòstra exportacion wallabag v1 e clicatz sul boton çai-jos per l'importar." 425 how_to: "Causissètz lo fichièr de vòstra exportacion wallabag v1 e clicatz sul boton çai-jos per l'importar."
411 wallabag_v2: 426 wallabag_v2:
412 page_title: 'Importar > Wallabag v2' 427 page_title: 'Importar > Wallabag v2'
413 description: "Aquesta aisina importarà totas vòstras donadas d'una instància mai de wallabag v2. Anatz dins totes vòstres articles, puèi, sus la barra laterala, clicatz sus \"JSON\". Traparatz un fichièr \"All articles.json\"." 428 description: "Aquesta aisina importarà totas vòstras donadas d'una instància mai de wallabag v2. Anatz dins totes vòstres articles, puèi, sus la barra laterala, clicatz sus \"JSON\". Traparetz un fichièr \"All articles.json\"."
414 readability: 429 readability:
415 page_title: 'Importar > Readability' 430 page_title: 'Importar > Readability'
416 description: "Aquesta aisina importarà totas vòstres articles de Readability. Sus la pagina de l'aisina (https://www.readability.com/tools/), clicatz sus \"Export your data\" dins la seccion \"Data Export\". Recebretz un corrièl per telecargar un json (qu'acaba pas amb un .json de fach)." 431 description: "Aquesta aisina importarà totas vòstres articles de Readability. Sus la pagina de l'aisina (https://www.readability.com/tools/), clicatz sus \"Export your data\" dins la seccion \"Data Export\". Recebretz un corrièl per telecargar un json (qu'acaba pas amb un .json de fach)."
417 how_to: "Mercés de seleccionar vòstre Readability fichièr e de clicar sul boton dejós per lo telecargar e l'importar." 432 how_to: "Mercés de seleccionar vòstre Readability fichièr e de clicar sul boton dejós per lo telecargar e l'importar."
418 worker: 433 worker:
419 enabled: "L'importacion se fa de manièra asincròna. Un còp l'importacion lançada, una aisina extèrna s'ocuparà dels messatges un per un. Lo servici actual es : " 434 enabled: "L'importacion se fa de manièra asincròna. Un còp l'importacion lançada, una aisina extèrna s'ocuparà dels articles un per un. Lo servici actual es : "
420 download_images_warning: "Avètz activat lo telecargament de los imatges de vòstres articles. Combinat amb l'importacion classica, aquò pòt tardar un long moment (o benlèu fracassar). <strong>Recomandem fòrtament</strong> l'activacion de l'importacion asincròna per evitar las errors." 435 download_images_warning: "Avètz activat lo telecargament de los imatges de vòstres articles. Combinat amb l'importacion classica, aquò pòt tardar un long moment (o benlèu fracassar). <strong>Recomandem fòrtament</strong> l'activacion de l'importacion asincròna per evitar las errors."
421 firefox: 436 firefox:
422 page_title: 'Importar > Firefox' 437 page_title: 'Importar > Firefox'
423 description: "Aquesta aisina importarà totas vòstres favorits de Firefox. Just go to your bookmarks (Ctrl+Maj+O), then into \"Import and backup\", choose \"Backup...\". You will obtain a .json file." 438 description: "Aquesta aisina importarà totas vòstres favorits de Firefox. Anatz simplament dins vòstres marcapaginas (Ctrl+Maj+O), puèi dins \"Impòrt e salvagarda\", causissètz \"Salvagardar...\". Auretz un fichièr .json."
424 how_to: "Mercés de causir lo fichièr de salvagarda e de clicar sul boton dejós per l'importar. Notatz que lo tractament pòt durar un moment ja que totes los articles an d'èsser recuperats." 439 how_to: "Mercés de causir lo fichièr de salvagarda e de clicar sul boton dejós per l'importar. Notatz que lo tractament pòt durar un moment ja que totes los articles an d'èsser recuperats."
425 chrome: 440 chrome:
426 page_title: 'Importar > Chrome' 441 page_title: 'Importar > Chrome'
427 description: "Aquesta aisina importarà totas vòstres favorits de Chrome. L'emplaçament del fichièr depend de vòstre sistèma operatiu : <ul><li>Sus Linux, anatz al dorsièr <code>~/.config/chromium/Default/</code></li><li>Sus Windows, deu èsser dins <code>%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default</code></li><li>sus OS X, deu èsser dins <code>~/Library/Application Support/Google/Chrome/Default/Bookmarks</code></li></ul>Un còp enlà, copiatz lo fichièr de favorits dins un endrech que volètz.<em><br>Notatz que s'avètz Chromium al lòc de Chrome, vos cal cambiar lo camin segon aquesta situacion.</em></p>" 442 description: "Aquesta aisina importarà totas vòstres favorits de Chrome. L'emplaçament del fichièr depend de vòstre sistèma operatiu : <ul><li>Sus Linux, anatz al dorsièr <code>~/.config/chromium/Default/</code></li><li>Sus Windows, deu èsser dins <code>%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default</code></li><li>sus OS X, deu èsser dins <code>~/Library/Application Support/Google/Chrome/Default/Bookmarks</code></li></ul>Un còp enlà, copiatz lo fichièr de favorits dins un endrech que volètz.<em><br>Notatz que s'avètz Chromium al lòc de Chrome, vos cal cambiar lo camin segon aquesta situacion.</em></p>"
428 how_to: "Mercés de causir lo fichièr de salvagarda e de clicar sul boton dejós per l'importar. Notatz que lo tractament pòt durar un moment ja que totes los articles an d'èsser recuperats." 443 how_to: "Mercés de causir lo fichièr de salvagarda e de clicar sul boton dejós per l'importar. Notatz que lo tractament pòt durar un moment ja que totes los articles an d'èsser recuperats."
429 instapaper: 444 instapaper:
430 page_title: 'Importar > Instapaper' 445 page_title: 'Importar > Instapaper'
@@ -437,7 +452,7 @@ import:
437 452
438developer: 453developer:
439 page_title: 'Gestion dels clients API' 454 page_title: 'Gestion dels clients API'
440 welcome_message: "Benvenguda sus l'API de wallabag" 455 welcome_message: "Vos desirem la benvenguda sus l'API de wallabag"
441 documentation: 'Documentacion' 456 documentation: 'Documentacion'
442 how_to_first_app: 'Cossí crear vòstra primièra aplicacion' 457 how_to_first_app: 'Cossí crear vòstra primièra aplicacion'
443 full_documentation: "Veire la documentacion completa de l'API" 458 full_documentation: "Veire la documentacion completa de l'API"
@@ -448,17 +463,17 @@ developer:
448 existing_clients: 463 existing_clients:
449 title: 'Los clients existents' 464 title: 'Los clients existents'
450 field_id: 'ID Client' 465 field_id: 'ID Client'
451 field_secret: 'Clé secreta' 466 field_secret: 'Clau secrèta'
452 field_uris: 'URLs de redireccion' 467 field_uris: 'URLs de redireccion'
453 field_grant_types: 'Tipe de privilègi acordat' 468 field_grant_types: 'Tipe de privilègi acordat'
454 no_client: 'Pas cap de client pel moment.' 469 no_client: 'Pas cap de client pel moment.'
455 remove: 470 remove:
456 warn_message_1: 'Avètz la possibilitat de supriimr un client. Aquesta accion es IRREVERSIBLA !' 471 warn_message_1: 'Avètz la possibilitat de suprimir un client. Aquesta accion es IRREVERSIBLA!'
457 warn_message_2: "Se suprimissètz un client, totas las aplicacions que l'emplegan foncionaràn pas mai amb vòstre compte wallabag." 472 warn_message_2: "Se suprimissètz un client, totas las aplicacions que l'emplegan foncionaràn pas mai amb vòstre compte wallabag."
458 action: 'Suprimir aqueste client' 473 action: 'Suprimir aqueste client'
459 client: 474 client:
460 page_title: 'Gestion dels clients API > Novèl client' 475 page_title: 'Gestion dels clients API > Novèl client'
461 page_description: "Anatz crear un novèl client. Mercés de cumplir l'url de redireccion cap a vòstra aplicacion." 476 page_description: "Anatz crear un novèl client. Mercés de garnir l'url de redireccion cap a vòstra aplicacion."
462 form: 477 form:
463 name_label: "Nom del client" 478 name_label: "Nom del client"
464 redirect_uris_label: 'URLs de redireccion' 479 redirect_uris_label: 'URLs de redireccion'
@@ -469,7 +484,7 @@ developer:
469 page_description: 'Vaquí los paramètres de vòstre client.' 484 page_description: 'Vaquí los paramètres de vòstre client.'
470 field_name: 'Nom del client' 485 field_name: 'Nom del client'
471 field_id: 'ID Client' 486 field_id: 'ID Client'
472 field_secret: 'Clau secreta' 487 field_secret: 'Clau secrèta'
473 back: 'Retour' 488 back: 'Retour'
474 read_howto: 'Legir "cossí crear ma primièra aplicacion"' 489 read_howto: 'Legir "cossí crear ma primièra aplicacion"'
475 howto: 490 howto:
@@ -477,10 +492,10 @@ developer:
477 description: 492 description:
478 paragraph_1: "Las comandas seguentas utilizan la <a href=\"https://github.com/jkbrzt/httpie\">bibliotèca HTTPie</a>. Asseguratz-vos que siasqueòu installadas abans de l'utilizar." 493 paragraph_1: "Las comandas seguentas utilizan la <a href=\"https://github.com/jkbrzt/httpie\">bibliotèca HTTPie</a>. Asseguratz-vos que siasqueòu installadas abans de l'utilizar."
479 paragraph_2: "Vos cal un geton per escambiar entre vòstra aplicacion e l'API de wallabar." 494 paragraph_2: "Vos cal un geton per escambiar entre vòstra aplicacion e l'API de wallabar."
480 paragraph_3: 'Per crear un geton, vos cal crear <a href=\"%link%\">crear un novèl client</a>.' 495 paragraph_3: 'Per crear un geton, vos cal <a href=\"%link%\">crear un novèl client</a>.'
481 paragraph_4: 'Ara creatz un geton (remplaçar client_id, client_secret, username e password amb las bonas valors) :' 496 paragraph_4: 'Ara creatz un geton (remplaçar client_id, client_secret, username e password amb las bonas valors) :'
482 paragraph_5: "L'API vos tornarà una responsa coma aquò :" 497 paragraph_5: "L'API vos tornarà una responsa coma aquò:"
483 paragraph_6: "L'access_token deu èsser emplegat per far una requèsta a l'API. Per exemple :" 498 paragraph_6: "L'access_token deu èsser emplegat per far una requèsta a l'API. Per exemple:"
484 paragraph_7: "Aquesta requèsta tornarà totes los articles de l'utilizaire." 499 paragraph_7: "Aquesta requèsta tornarà totes los articles de l'utilizaire."
485 paragraph_8: "Se volètz totas las adreças d'accès de l'API, donatz un còp d’uèlh <a href=\"%link%\">a la documentacion de l'API</a>." 500 paragraph_8: "Se volètz totas las adreças d'accès de l'API, donatz un còp d’uèlh <a href=\"%link%\">a la documentacion de l'API</a>."
486 back: 'Retorn' 501 back: 'Retorn'
@@ -508,7 +523,29 @@ user:
508 twofactor_label: 'Autentificacion doble-factor' 523 twofactor_label: 'Autentificacion doble-factor'
509 save: 'Enregistrar' 524 save: 'Enregistrar'
510 delete: 'Suprimir' 525 delete: 'Suprimir'
511 delete_confirm: 'Sètz segur ?' 526 delete_confirm: 'Sètz segur ?'
527 back_to_list: 'Tornar a la lista'
528 search:
529 placeholder: "Filtrar per nom d'utilizaire o corrièl"
530
531site_credential:
532 page_title: Gestion dels identificants
533 new_site_credential: Crear un identificant
534 edit_site_credential: Modificar un identificant
535 description: "Aquí podètz gerir vòstres identificants pels sites que los demandan (ne crear, ne modifiar, ne suprimir) coma los sites a peatge, etc."
536 list:
537 actions: 'Accions'
538 edit_action: 'Modificar'
539 yes: 'Òc'
540 no: 'Non'
541 create_new_one: Crear un novèl identificant
542 form:
543 username_label: "Nom d'utilizaire"
544 host_label: 'Òste'
545 password_label: 'Senhal'
546 save: 'Enregistrar'
547 delete: 'Suprimir'
548 delete_confirm: 'Sètz segur ?'
512 back_to_list: 'Tornar a la lista' 549 back_to_list: 'Tornar a la lista'
513 550
514error: 551error:
@@ -519,7 +556,7 @@ flashes:
519 notice: 556 notice:
520 config_saved: 'Los paramètres son ben estats meses a jorn.' 557 config_saved: 'Los paramètres son ben estats meses a jorn.'
521 password_updated: 'Vòstre senhal es ben estat mes a jorn' 558 password_updated: 'Vòstre senhal es ben estat mes a jorn'
522 password_not_updated_demo: "En demostration, podètz pas cambiar lo senhal d'aqueste utilizaire." 559 password_not_updated_demo: "En demostracion, podètz pas cambiar lo senhal d'aqueste utilizaire."
523 user_updated: 'Vòstres informacions personnelas son ben estadas mesas a jorn' 560 user_updated: 'Vòstres informacions personnelas son ben estadas mesas a jorn'
524 rss_updated: 'La configuracion dels fluxes RSS es ben estada mesa a jorn' 561 rss_updated: 'La configuracion dels fluxes RSS es ben estada mesa a jorn'
525 tagging_rules_updated: 'Règlas misa a jorn' 562 tagging_rules_updated: 'Règlas misa a jorn'
@@ -528,9 +565,10 @@ flashes:
528 annotations_reset: Anotacions levadas 565 annotations_reset: Anotacions levadas
529 tags_reset: Etiquetas levadas 566 tags_reset: Etiquetas levadas
530 entries_reset: Articles levats 567 entries_reset: Articles levats
568 archived_reset: Articles archivat suprimits
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: 'Article ja salvargardat lo %date%' 571 entry_already_saved: 'Article ja salvagardat lo %date%'
534 entry_saved: 'Article enregistrat' 572 entry_saved: 'Article enregistrat'
535 entry_saved_failed: 'Article salvat mai fracàs de la recuperacion del contengut' 573 entry_saved_failed: 'Article salvat mai fracàs de la recuperacion del contengut'
536 entry_updated: 'Article mes a jorn' 574 entry_updated: 'Article mes a jorn'
@@ -547,9 +585,9 @@ flashes:
547 import: 585 import:
548 notice: 586 notice:
549 failed: "L'importacion a fracassat, mercés de tornar ensajar." 587 failed: "L'importacion a fracassat, mercés de tornar ensajar."
550 failed_on_file: "Errorr pendent du tractament de l'import. Mercés de verificar vòstre fichièr." 588 failed_on_file: "Error en tractar l'impòrt. Mercés de verificar vòstre fichièr."
551 summary: "Rapòrt d'import: %imported% importats, %skipped% ja presents." 589 summary: "Rapòrt d'impòrt: %imported% importats, %skipped% ja presents."
552 summary_with_queue: "Rapòrt d'import : %queued% en espèra de tractament." 590 summary_with_queue: "Rapòrt d'impòrt:%queued% en espèra de tractament."
553 error: 591 error:
554 redis_enabled_not_installed: "Redis es capable d'importar de manièra asincròna mai sembla que <u>podèm pas nos conectar amb el</u>. Mercés de verificar la configuracion de Redis." 592 redis_enabled_not_installed: "Redis es capable d'importar de manièra asincròna mai sembla que <u>podèm pas nos conectar amb el</u>. Mercés de verificar la configuracion de Redis."
555 rabbit_enabled_not_installed: "RabbitMQ es capable d'importar de manièra asincròna mai sembla que <u>podèm pas nos conectar amb el</u>. Mercés de verificar la configuracion de RabbitMQ." 593 rabbit_enabled_not_installed: "RabbitMQ es capable d'importar de manièra asincròna mai sembla que <u>podèm pas nos conectar amb el</u>. Mercés de verificar la configuracion de RabbitMQ."
@@ -562,3 +600,8 @@ flashes:
562 added: 'Utilizaire "%username%" ajustat' 600 added: 'Utilizaire "%username%" ajustat'
563 updated: 'Utilizaire "%username%" mes a jorn' 601 updated: 'Utilizaire "%username%" mes a jorn'
564 deleted: 'Utilizaire "%username%" suprimit' 602 deleted: 'Utilizaire "%username%" suprimit'
603 site_credential:
604 notice:
605 added: 'Identificant per "%host%" ajustat'
606 updated: 'Identificant per "%host%" mes a jorn'
607 deleted: 'Identificant per "%host%" suprimit'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
index b990a6b9..e642c530 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
@@ -32,6 +32,7 @@ menu:
32 save_link: 'Zapisz link' 32 save_link: 'Zapisz link'
33 back_to_unread: 'Powrót do nieprzeczytanych artykułów' 33 back_to_unread: 'Powrót do nieprzeczytanych artykułów'
34 users_management: 'Zarządzanie użytkownikami' 34 users_management: 'Zarządzanie użytkownikami'
35 site_credentials: 'Poświadczenia strony'
35 top: 36 top:
36 add_new_entry: 'Dodaj nowy wpis' 37 add_new_entry: 'Dodaj nowy wpis'
37 search: 'Szukaj' 38 search: 'Szukaj'
@@ -76,6 +77,7 @@ config:
76 redirect_current_page: 'do bieżącej strony' 77 redirect_current_page: 'do bieżącej strony'
77 pocket_consumer_key_label: 'Klucz klienta Pocket do importu zawartości' 78 pocket_consumer_key_label: 'Klucz klienta Pocket do importu zawartości'
78 android_configuration: Skonfiguruj swoją androidową aplikację 79 android_configuration: Skonfiguruj swoją androidową aplikację
80 android_instruction: "Dotknij tutaj, aby wstępnie uzupełnij androidową aplikację"
79 help_theme: "Dopasuj wallabag do swoich potrzeb. Tutaj możesz wybrać preferowany przez ciebie motyw." 81 help_theme: "Dopasuj wallabag do swoich potrzeb. Tutaj możesz wybrać preferowany przez ciebie motyw."
80 help_items_per_page: "Możesz zmienić ilość artykułów wyświetlanych na każdej stronie." 82 help_items_per_page: "Możesz zmienić ilość artykułów wyświetlanych na każdej stronie."
81 help_reading_speed: "wallabag oblicza czas czytania każdego artykułu. Dzięki tej liście możesz określić swoje tempo. Wallabag przeliczy ponownie czas potrzebny, na przeczytanie każdego z artykułów." 83 help_reading_speed: "wallabag oblicza czas czytania każdego artykułu. Dzięki tej liście możesz określić swoje tempo. Wallabag przeliczy ponownie czas potrzebny, na przeczytanie każdego z artykułów."
@@ -89,9 +91,10 @@ config:
89 token_reset: 'Zresetuj swojego tokena' 91 token_reset: 'Zresetuj swojego tokena'
90 rss_links: 'RSS links' 92 rss_links: 'RSS links'
91 rss_link: 93 rss_link:
92 unread: 'nieprzeczytane' 94 unread: 'Nieprzeczytane'
93 starred: 'oznaczone gwiazdką' 95 starred: 'Oznaczone gwiazdką'
94 archive: 'archiwum' 96 archive: 'Archiwum'
97 all: 'Wszystkie'
95 rss_limit: 'Link do RSS' 98 rss_limit: 'Link do RSS'
96 form_user: 99 form_user:
97 two_factor_description: "Włączenie autoryzacji dwuetapowej oznacza, że będziesz otrzymywał maile z kodem przy każdym nowym, niezaufanym połączeniu" 100 two_factor_description: "Włączenie autoryzacji dwuetapowej oznacza, że będziesz otrzymywał maile z kodem przy każdym nowym, niezaufanym połączeniu"
@@ -110,6 +113,7 @@ config:
110 annotations: Usuń WSZYSTKIE adnotacje 113 annotations: Usuń WSZYSTKIE adnotacje
111 tags: Usuń WSZYSTKIE tagi 114 tags: Usuń WSZYSTKIE tagi
112 entries: usuń WSZYTSTKIE wpisy 115 entries: usuń WSZYTSTKIE wpisy
116 archived: usuń WSZYSTKIE zarchiwizowane wpisy
113 confirm: Jesteś pewien? (tej operacji NIE MOŻNA cofnąć) 117 confirm: Jesteś pewien? (tej operacji NIE MOŻNA cofnąć)
114 form_password: 118 form_password:
115 description: "Tutaj możesz zmienić swoje hasło. Twoje nowe hasło powinno mieć conajmniej 8 znaków." 119 description: "Tutaj możesz zmienić swoje hasło. Twoje nowe hasło powinno mieć conajmniej 8 znaków."
@@ -154,6 +158,7 @@ config:
154 or: 'Jedna reguła LUB inna' 158 or: 'Jedna reguła LUB inna'
155 and: 'Jedna reguła I inna' 159 and: 'Jedna reguła I inna'
156 matches: 'Sprawdź czy <i>temat</i> pasuje <i>szukaj</i> (duże lub małe litery).<br />Przykład: <code>tytuł zawiera "piłka nożna"</code>' 160 matches: 'Sprawdź czy <i>temat</i> pasuje <i>szukaj</i> (duże lub małe litery).<br />Przykład: <code>tytuł zawiera "piłka nożna"</code>'
161 notmatches: 'Sprawdź czy <i>temat</i> nie zawiera <i>szukaj</i> (duże lub małe litery).<br />Przykład: <code>tytuł nie zawiera "piłka nożna"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 filtered_tags: 'Filtrowane po tagach:' 169 filtered_tags: 'Filtrowane po tagach:'
165 filtered_search: 'Filtrowanie po wyszukiwaniu:' 170 filtered_search: 'Filtrowanie po wyszukiwaniu:'
166 untagged: 'Odtaguj wpisy' 171 untagged: 'Odtaguj wpisy'
172 all: 'Wszystkie przedmioty'
167 list: 173 list:
168 number_on_the_page: '{0} Nie ma wpisów.|{1} Jest jeden wpis.|]1,Inf[ Są %count% wpisy.' 174 number_on_the_page: '{0} Nie ma wpisów.|{1} Jest jeden wpis.|]1,Inf[ Są %count% wpisy.'
169 reading_time: 'szacunkowy czas czytania' 175 reading_time: 'szacunkowy czas czytania'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'Nieprzeczytane' 191 unread_label: 'Nieprzeczytane'
186 preview_picture_label: 'Posiada podgląd obrazu' 192 preview_picture_label: 'Posiada podgląd obrazu'
187 preview_picture_help: 'Podgląd obrazu' 193 preview_picture_help: 'Podgląd obrazu'
194 is_public_label: 'Posiada publiczny link'
195 is_public_help: 'Publiczny link'
188 language_label: 'Język' 196 language_label: 'Język'
189 http_status_label: 'Status HTTP' 197 http_status_label: 'Status HTTP'
190 reading_time: 198 reading_time:
@@ -223,6 +231,8 @@ entry:
223 original_article: 'oryginalny' 231 original_article: 'oryginalny'
224 annotations_on_the_entry: '{0} Nie ma adnotacji |{1} Jedna adnotacja |]1,Inf[ %count% adnotacji' 232 annotations_on_the_entry: '{0} Nie ma adnotacji |{1} Jedna adnotacja |]1,Inf[ %count% adnotacji'
225 created_at: 'Czas stworzenia' 233 created_at: 'Czas stworzenia'
234 published_at: 'Data publikacji'
235 published_by: 'Opublikowane przez'
226 new: 236 new:
227 page_title: 'Zapisz nowy wpis' 237 page_title: 'Zapisz nowy wpis'
228 placeholder: 'http://website.com' 238 placeholder: 'http://website.com'
@@ -234,10 +244,12 @@ entry:
234 page_title: 'Edytuj wpis' 244 page_title: 'Edytuj wpis'
235 title_label: 'Tytuł' 245 title_label: 'Tytuł'
236 url_label: 'Adres URL' 246 url_label: 'Adres URL'
237 is_public_label: 'Publiczny'
238 save_label: 'Zapisz' 247 save_label: 'Zapisz'
239 public: 248 public:
240 shared_by_wallabag: "Ten artykuł został udostępniony przez <a href='%wallabag_instance%'>wallabag</a>" 249 shared_by_wallabag: "Ten artykuł został udostępniony przez <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 delete: "Czy jesteś pewien, że chcesz usunąć ten artykuł?"
252 delete_tag: "Czy jesteś pewien, że chcesz usunąć ten tag, z tego artykułu?"
241 253
242about: 254about:
243 page_title: 'O nas' 255 page_title: 'O nas'
@@ -385,6 +397,9 @@ tag:
385 add: 'Dodaj' 397 add: 'Dodaj'
386 placeholder: 'Możesz dodać kilka tagów, oddzielając je przecinkami.' 398 placeholder: 'Możesz dodać kilka tagów, oddzielając je przecinkami.'
387 399
400export:
401 footer_template: '<div style="text-align:center;"><p>Stworzone przez wallabag z %method%</p><p>Proszę zgłoś <a href="https://github.com/wallabag/wallabag/issues">sprawę</a>, jeżeli masz problem z wyświetleniem tego e-booka na swoim urządzeniu.</p></div>'
402
388import: 403import:
389 page_title: 'Import' 404 page_title: 'Import'
390 page_description: 'Witaj w importerze Wallabag. Wybierz swoją poprzednią usługę, z której chcesz migrować.' 405 page_description: 'Witaj w importerze Wallabag. Wybierz swoją poprzednią usługę, z której chcesz migrować.'
@@ -510,6 +525,28 @@ user:
510 delete: Usuń 525 delete: Usuń
511 delete_confirm: Jesteś pewien? 526 delete_confirm: Jesteś pewien?
512 back_to_list: Powrót do listy 527 back_to_list: Powrót do listy
528 search:
529 placeholder: Filtruj po nazwie użytkownika lub adresie e-mail
530
531site_credential:
532 page_title: Zarządzanie poświadczeniami strony
533 new_site_credential: Stwórz nowe poświadczenie
534 edit_site_credential: Edytuj istniejące poświadczenie
535 description: "Tutaj możesz zarządzać wszystkim poświadczeniami wymaganymi przez strony (stwórz, edytuj i usuń ), takie jak paywall, autentykacja, itp."
536 list:
537 actions: Akcje
538 edit_action: Edytuj
539 yes: Tak
540 no: Nie
541 create_new_one: Stwórz nowe poświadczenie
542 form:
543 username_label: 'Nazwa użytkownika'
544 host_label: 'Host'
545 password_label: 'Hasło'
546 save: Zapisz
547 delete: Usuń
548 delete_confirm: Jesteś pewien?
549 back_to_list: Powrót do listy
513 550
514error: 551error:
515 page_title: Wystąpił błąd 552 page_title: Wystąpił błąd
@@ -528,6 +565,7 @@ flashes:
528 annotations_reset: Zresetuj adnotacje 565 annotations_reset: Zresetuj adnotacje
529 tags_reset: Zresetuj tagi 566 tags_reset: Zresetuj tagi
530 entries_reset: Zresetuj wpisy 567 entries_reset: Zresetuj wpisy
568 archived_reset: Zarchiwizowane wpisy usunięte
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: 'Wpis już został dodany %date%' 571 entry_already_saved: 'Wpis już został dodany %date%'
@@ -562,3 +600,8 @@ flashes:
562 added: 'Użytkownik "%username%" dodany' 600 added: 'Użytkownik "%username%" dodany'
563 updated: 'Użytkownik "%username%" zaktualizowany' 601 updated: 'Użytkownik "%username%" zaktualizowany'
564 deleted: 'Użytkownik "%username%" usunięty' 602 deleted: 'Użytkownik "%username%" usunięty'
603 site_credential:
604 notice:
605 added: 'Poświadczenie dla "%host%" dodane'
606 updated: 'Poświadczenie dla "%host%" zaktualizowane'
607 deleted: 'Poświadczenie dla "%host%" usuniętę'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
index 3b1f9cb6..9b3fea6b 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
@@ -32,6 +32,7 @@ menu:
32 save_link: 'Salvar um link' 32 save_link: 'Salvar um link'
33 back_to_unread: 'Voltar para os artigos não lidos' 33 back_to_unread: 'Voltar para os artigos não lidos'
34 users_management: 'Gestão de Usuários' 34 users_management: 'Gestão de Usuários'
35 # site_credentials: 'Site credentials'
35 top: 36 top:
36 add_new_entry: 'Adicionar uma nova entrada' 37 add_new_entry: 'Adicionar uma nova entrada'
37 search: 'Pesquisa' 38 search: 'Pesquisa'
@@ -76,6 +77,7 @@ config:
76 # redirect_current_page: 'To the current page' 77 # redirect_current_page: 'To the current page'
77 pocket_consumer_key_label: 'Chave do consumidor do Pocket para importar conteúdo' 78 pocket_consumer_key_label: 'Chave do consumidor do Pocket para importar conteúdo'
78 # android_configuration: Configure your Android application 79 # android_configuration: Configure your Android application
80 # android_instruction: "Touch here to prefill your Android application"
79 # help_theme: "wallabag is customizable. You can choose your prefered theme here." 81 # help_theme: "wallabag is customizable. You can choose your prefered theme here."
80 # help_items_per_page: "You can change the number of articles displayed on each page." 82 # help_items_per_page: "You can change the number of articles displayed on each page."
81 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article." 83 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article."
@@ -89,9 +91,10 @@ config:
89 token_reset: 'Gerar novamente seu token' 91 token_reset: 'Gerar novamente seu token'
90 rss_links: 'Links RSS' 92 rss_links: 'Links RSS'
91 rss_link: 93 rss_link:
92 unread: 'não lido' 94 unread: 'Não lido'
93 starred: 'destacado' 95 starred: 'Destacado'
94 archive: 'arquivado' 96 archive: 'Arquivado'
97 # all: 'All'
95 rss_limit: 'Número de itens no feed' 98 rss_limit: 'Número de itens no feed'
96 form_user: 99 form_user:
97 two_factor_description: 'Habilitar autenticação de dois passos significa que você receberá um e-mail com um código a cada nova conexão desconhecida.' 100 two_factor_description: 'Habilitar autenticação de dois passos significa que você receberá um e-mail com um código a cada nova conexão desconhecida.'
@@ -110,6 +113,7 @@ config:
110 # annotations: Remove ALL annotations 113 # annotations: Remove ALL annotations
111 # tags: Remove ALL tags 114 # tags: Remove ALL tags
112 # entries: Remove ALL entries 115 # entries: Remove ALL entries
116 # archived: Remove ALL archived entries
113 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE) 117 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
114 form_password: 118 form_password:
115 # description: "You can change your password here. Your new password should by at least 8 characters long." 119 # description: "You can change your password here. Your new password should by at least 8 characters long."
@@ -154,6 +158,7 @@ config:
154 or: 'Uma regra OU outra' 158 or: 'Uma regra OU outra'
155 and: 'Uma regra E outra' 159 and: 'Uma regra E outra'
156 matches: 'Testa que um <i>assunto</i> corresponde a uma <i>pesquisa</i> (maiúscula ou minúscula).<br />Exemplo: <code>título corresponde a "futebol"</code>' 160 matches: 'Testa que um <i>assunto</i> corresponde a uma <i>pesquisa</i> (maiúscula ou minúscula).<br />Exemplo: <code>título corresponde a "futebol"</code>'
161 # notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 filtered_tags: 'Filtrar por tags:' 169 filtered_tags: 'Filtrar por tags:'
165 # filtered_search: 'Filtered by search:' 170 # filtered_search: 'Filtered by search:'
166 untagged: 'Entradas sem tags' 171 untagged: 'Entradas sem tags'
172 # all: 'All entries'
167 list: 173 list:
168 number_on_the_page: '{0} Não existem entradas.|{1} Existe uma entrada.|]1,Inf[ Existem %count% entradas.' 174 number_on_the_page: '{0} Não existem entradas.|{1} Existe uma entrada.|]1,Inf[ Existem %count% entradas.'
169 reading_time: 'tempo estimado de leitura' 175 reading_time: 'tempo estimado de leitura'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'Não Lido' 191 unread_label: 'Não Lido'
186 preview_picture_label: 'Possui uma imagem de preview' 192 preview_picture_label: 'Possui uma imagem de preview'
187 preview_picture_help: 'Imagem de preview' 193 preview_picture_help: 'Imagem de preview'
194 # is_public_label: 'Has a public link'
195 # is_public_help: 'Public link'
188 language_label: 'Idioma' 196 language_label: 'Idioma'
189 # http_status_label: 'HTTP status' 197 # http_status_label: 'HTTP status'
190 reading_time: 198 reading_time:
@@ -223,6 +231,8 @@ entry:
223 original_article: 'original' 231 original_article: 'original'
224 annotations_on_the_entry: '{0} Sem anotações|{1} Uma anotação|]1,Inf[ %nbAnnotations% anotações' 232 annotations_on_the_entry: '{0} Sem anotações|{1} Uma anotação|]1,Inf[ %nbAnnotations% anotações'
225 created_at: 'Data de criação' 233 created_at: 'Data de criação'
234 # published_at: 'Publication date'
235 # published_by: 'Published by'
226 new: 236 new:
227 page_title: 'Salvar nova entrada' 237 page_title: 'Salvar nova entrada'
228 placeholder: 'http://website.com' 238 placeholder: 'http://website.com'
@@ -234,10 +244,12 @@ entry:
234 page_title: 'Editar uma entrada' 244 page_title: 'Editar uma entrada'
235 title_label: 'Título' 245 title_label: 'Título'
236 url_label: 'Url' 246 url_label: 'Url'
237 is_public_label: 'Público'
238 save_label: 'Salvar' 247 save_label: 'Salvar'
239 public: 248 public:
240 shared_by_wallabag: "Este artigo foi compartilhado pelo <a href='%wallabag_instance%'>wallabag</a>" 249 shared_by_wallabag: "Este artigo foi compartilhado pelo <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 # delete: "Are you sure you want to remove that article?"
252 # delete_tag: "Are you sure you want to remove that tag from that article?"
241 253
242about: 254about:
243 page_title: 'Sobre' 255 page_title: 'Sobre'
@@ -385,6 +397,9 @@ tag:
385 # add: 'Add' 397 # add: 'Add'
386 # placeholder: 'You can add several tags, separated by a comma.' 398 # placeholder: 'You can add several tags, separated by a comma.'
387 399
400# export:
401# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
388import: 403import:
389 page_title: 'Importar' 404 page_title: 'Importar'
390 page_description: 'Bem-vindo ao importador do wallabag. Por favo selecione o serviço do qual deseja migrar.' 405 page_description: 'Bem-vindo ao importador do wallabag. Por favo selecione o serviço do qual deseja migrar.'
@@ -510,6 +525,28 @@ user:
510 delete: 'Apagar' 525 delete: 'Apagar'
511 delete_confirm: 'Tem certeza?' 526 delete_confirm: 'Tem certeza?'
512 back_to_list: 'Voltar para a lista' 527 back_to_list: 'Voltar para a lista'
528 search:
529 # placeholder: Filter by username or email
530
531site_credential:
532 # page_title: Site credentials management
533 # new_site_credential: Create a credential
534 # edit_site_credential: Edit an existing credential
535 # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc."
536 list:
537 actions: 'Ações'
538 edit_action: 'Editar'
539 yes: 'Sim'
540 no: 'Não'
541 # create_new_one: Create a new credential
542 form:
543 # username_label: 'Username'
544 # host_label: 'Host'
545 # password_label: 'Password'
546 save: 'Salvar'
547 delete: 'Apagar'
548 delete_confirm: 'Tem certeza?'
549 back_to_list: 'Voltar para a lista'
513 550
514error: 551error:
515 # page_title: An error occurred 552 # page_title: An error occurred
@@ -528,6 +565,7 @@ flashes:
528 # annotations_reset: Annotations reset 565 # annotations_reset: Annotations reset
529 # tags_reset: Tags reset 566 # tags_reset: Tags reset
530 # entries_reset: Entries reset 567 # entries_reset: Entries reset
568 # archived_reset: Archived entries deleted
531 entry: 569 entry:
532 notice: 570 notice:
533 entry_already_saved: 'Entrada já foi salva em %date%' 571 entry_already_saved: 'Entrada já foi salva em %date%'
@@ -562,3 +600,8 @@ flashes:
562 added: 'Usuário "%username%" adicionado' 600 added: 'Usuário "%username%" adicionado'
563 updated: 'Usuário "%username%" atualizado' 601 updated: 'Usuário "%username%" atualizado'
564 deleted: 'Usuário "%username%" removido' 602 deleted: 'Usuário "%username%" removido'
603 site_credential:
604 notice:
605 # added: 'Site credential for "%host%" added'
606 # updated: 'Site credential for "%host%" updated'
607 # deleted: 'Site credential for "%host%" deleted'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
index 728eed58..673ca183 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
@@ -32,6 +32,7 @@ menu:
32 # save_link: 'Save a link' 32 # save_link: 'Save a link'
33 back_to_unread: 'Înapoi la articolele necitite' 33 back_to_unread: 'Înapoi la articolele necitite'
34 # users_management: 'Users management' 34 # users_management: 'Users management'
35 # site_credentials: 'Site credentials'
35 top: 36 top:
36 add_new_entry: 'Introdu un nou articol' 37 add_new_entry: 'Introdu un nou articol'
37 search: 'Căutare' 38 search: 'Căutare'
@@ -76,6 +77,7 @@ config:
76 # redirect_current_page: 'To the current page' 77 # redirect_current_page: 'To the current page'
77 pocket_consumer_key_label: Cheie consumator pentru importarea contentului din Pocket 78 pocket_consumer_key_label: Cheie consumator pentru importarea contentului din Pocket
78 # android_configuration: Configure your Android application 79 # android_configuration: Configure your Android application
80 # android_instruction: "Touch here to prefill your Android application"
79 # help_theme: "wallabag is customizable. You can choose your prefered theme here." 81 # help_theme: "wallabag is customizable. You can choose your prefered theme here."
80 # help_items_per_page: "You can change the number of articles displayed on each page." 82 # help_items_per_page: "You can change the number of articles displayed on each page."
81 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article." 83 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article."
@@ -89,9 +91,10 @@ config:
89 token_reset: 'Resetează-ți token-ul' 91 token_reset: 'Resetează-ți token-ul'
90 rss_links: 'Link-uri RSS' 92 rss_links: 'Link-uri RSS'
91 rss_link: 93 rss_link:
92 unread: 'unread' 94 unread: 'Unread'
93 starred: 'starred' 95 starred: 'Starred'
94 archive: 'archived' 96 archive: 'Archived'
97 # all: 'All'
95 rss_limit: 'Limită RSS' 98 rss_limit: 'Limită RSS'
96 form_user: 99 form_user:
97 # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connexion" 100 # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connexion"
@@ -110,6 +113,7 @@ config:
110 # annotations: Remove ALL annotations 113 # annotations: Remove ALL annotations
111 # tags: Remove ALL tags 114 # tags: Remove ALL tags
112 # entries: Remove ALL entries 115 # entries: Remove ALL entries
116 # archived: Remove ALL archived entries
113 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE) 117 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
114 form_password: 118 form_password:
115 # description: "You can change your password here. Your new password should by at least 8 characters long." 119 # description: "You can change your password here. Your new password should by at least 8 characters long."
@@ -154,6 +158,7 @@ config:
154 # or: 'One rule OR another' 158 # or: 'One rule OR another'
155 # and: 'One rule AND another' 159 # and: 'One rule AND another'
156 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>' 160 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
161 # notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 # filtered_tags: 'Filtered by tags:' 169 # filtered_tags: 'Filtered by tags:'
165 # filtered_search: 'Filtered by search:' 170 # filtered_search: 'Filtered by search:'
166 # untagged: 'Untagged entries' 171 # untagged: 'Untagged entries'
172 # all: 'All entries'
167 list: 173 list:
168 # number_on_the_page: '{0} There is no entry.|{1} There is one entry.|]1,Inf[ There are %count% entries.' 174 # number_on_the_page: '{0} There is no entry.|{1} There is one entry.|]1,Inf[ There are %count% entries.'
169 reading_time: 'timp estimat de citire' 175 reading_time: 'timp estimat de citire'
@@ -185,6 +191,8 @@ entry:
185 unread_label: 'Necitite' 191 unread_label: 'Necitite'
186 preview_picture_label: 'Are o imagine de previzualizare' 192 preview_picture_label: 'Are o imagine de previzualizare'
187 preview_picture_help: 'Previzualizare imagine' 193 preview_picture_help: 'Previzualizare imagine'
194 # is_public_label: 'Has a public link'
195 # is_public_help: 'Public link'
188 language_label: 'Limbă' 196 language_label: 'Limbă'
189 # http_status_label: 'HTTP status' 197 # http_status_label: 'HTTP status'
190 reading_time: 198 reading_time:
@@ -223,6 +231,8 @@ entry:
223 original_article: 'original' 231 original_article: 'original'
224 # annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations' 232 # annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations'
225 created_at: 'Data creării' 233 created_at: 'Data creării'
234 # published_at: 'Publication date'
235 # published_by: 'Published by'
226 new: 236 new:
227 page_title: 'Salvează un nou articol' 237 page_title: 'Salvează un nou articol'
228 placeholder: 'http://website.com' 238 placeholder: 'http://website.com'
@@ -234,10 +244,12 @@ entry:
234 # page_title: 'Edit an entry' 244 # page_title: 'Edit an entry'
235 # title_label: 'Title' 245 # title_label: 'Title'
236 url_label: 'Url' 246 url_label: 'Url'
237 # is_public_label: 'Public'
238 save_label: 'Salvează' 247 save_label: 'Salvează'
239 public: 248 public:
240 # shared_by_wallabag: "This article has been shared by <a href='%wallabag_instance%'>wallabag</a>" 249 # shared_by_wallabag: "This article has been shared by %username% with <a href='%wallabag_instance%'>wallabag</a>"
250 confirm:
251 # delete: "Are you sure you want to remove that article?"
252 # delete_tag: "Are you sure you want to remove that tag from that article?"
241 253
242about: 254about:
243 page_title: 'Despre' 255 page_title: 'Despre'
@@ -385,6 +397,9 @@ tag:
385 # add: 'Add' 397 # add: 'Add'
386 # placeholder: 'You can add several tags, separated by a comma.' 398 # placeholder: 'You can add several tags, separated by a comma.'
387 399
400# export:
401# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
402
388import: 403import:
389 # page_title: 'Import' 404 # page_title: 'Import'
390 # page_description: 'Welcome to wallabag importer. Please select your previous service that you want to migrate.' 405 # page_description: 'Welcome to wallabag importer. Please select your previous service that you want to migrate.'
@@ -510,6 +525,28 @@ user:
510 # delete: Delete 525 # delete: Delete
511 # delete_confirm: Are you sure? 526 # delete_confirm: Are you sure?
512 # back_to_list: Back to list 527 # back_to_list: Back to list
528 search:
529 # placeholder: Filter by username or email
530
531site_credential:
532 # page_title: Site credentials management
533 # new_site_credential: Create a credential
534 # edit_site_credential: Edit an existing credential
535 # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc."
536 # list:
537 # actions: Actions
538 # edit_action: Edit
539 # yes: Yes
540 # no: No
541 # create_new_one: Create a new credential
542 # form:
543 # username_label: 'Username'
544 # host_label: 'Host'
545 # password_label: 'Password'
546 # save: Save
547 # delete: Delete
548 # delete_confirm: Are you sure?
549 # back_to_list: Back to list
513 550
514error: 551error:
515 # page_title: An error occurred 552 # page_title: An error occurred
@@ -528,6 +565,7 @@ flashes:
528 # annotations_reset: Annotations reset 565 # annotations_reset: Annotations reset
529 # tags_reset: Tags reset 566 # tags_reset: Tags reset
530 # entries_reset: Entries reset 567 # entries_reset: Entries reset
568 # archived_reset: Archived entries deleted
531 entry: 569 entry:
532 notice: 570 notice:
533 # entry_already_saved: 'Entry already saved on %date%' 571 # entry_already_saved: 'Entry already saved on %date%'
@@ -562,3 +600,8 @@ flashes:
562 # added: 'User "%username%" added' 600 # added: 'User "%username%" added'
563 # updated: 'User "%username%" updated' 601 # updated: 'User "%username%" updated'
564 # deleted: 'User "%username%" deleted' 602 # deleted: 'User "%username%" deleted'
603 site_credential:
604 notice:
605 # added: 'Site credential for "%host%" added'
606 # updated: 'Site credential for "%host%" updated'
607 # deleted: 'Site credential for "%host%" deleted'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml
new file mode 100644
index 00000000..eceecabf
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml
@@ -0,0 +1,564 @@
1security:
2 login:
3 page_title: 'Приветствую в wallabag!'
4 keep_logged_in: 'Запомнить меня'
5 forgot_password: 'Забыли пароль?'
6 submit: 'Войти'
7 register: 'Регистрация'
8 username: 'Логин'
9 password: 'Пароль'
10 cancel: 'Отменить'
11 resetting:
12 description: "Введите Ваш email ниже и мы отправим Вам инструкцию для сброса пароля."
13 register:
14 page_title: 'Создать новый аккаунт'
15 go_to_account: 'Перейти в аккаунт'
16
17menu:
18 left:
19 unread: 'Непрочитанные'
20 starred: 'Помеченные'
21 archive: 'Архивные'
22 all_articles: 'Все записи'
23 config: 'Настройки'
24 tags: 'Теги'
25 internal_settings: 'Внутренние настройки'
26 import: 'Импорт'
27 howto: 'How to'
28 developer: 'Настройки клиентского API'
29 logout: 'Выход'
30 about: 'о wallabag'
31 search: 'Поиск'
32 save_link: 'Сохранить ссылку'
33 back_to_unread: 'Назад к непрочитанным записям'
34 users_management: 'Управление пользователями'
35 top:
36 add_new_entry: 'Добавить новую запись'
37 search: 'Поиск'
38 filter_entries: 'Фильтр записей'
39 export: 'Экспорт'
40 search_form:
41 input_label: 'Введите текст для поиска'
42
43footer:
44 wallabag:
45 elsewhere: 'Возьмите wallabag с собой'
46 social: 'Соц. сети'
47 powered_by: 'создано с помощью'
48 about: 'О wallabag'
49 stats: "Поскольку %user_creation% вы читаете %nb_archives% записей, это около %per_day% в день!"
50
51config:
52 page_title: 'Настройки'
53 tab_menu:
54 settings: 'Настройки'
55 rss: 'RSS'
56 user_info: 'Информация о пользователе'
57 password: 'Пароль'
58 rules: 'Правила настройки простановки тегов'
59 new_user: 'Добавить пользователя'
60 form:
61 save: 'Сохранить'
62 form_settings:
63 theme_label: 'Тема'
64 items_per_page_label: 'Записей на странице'
65 language_label: 'Язык'
66 reading_speed:
67 label: 'Скорость чтения'
68 help_message: 'Вы можете использовать онлайн-инструменты для оценки скорости чтения:'
69 100_word: 'Я читаю ~100 слов в минуту'
70 200_word: 'Я читаю ~200 слов в минуту'
71 300_word: 'Я читаю ~300 слов в минуту'
72 400_word: 'Я читаю ~400 слов в минуту'
73 action_mark_as_read:
74 label: 'Куда Вы хотите быть перенаправлены, после пометки записи, как прочитанная?'
75 redirect_homepage: 'На домашнюю страницу'
76 redirect_current_page: 'На текущую страницу'
77 pocket_consumer_key_label: "Ключ от Pocket для импорта контента"
78 android_configuration: "Настройте Ваше Android приложение"
79 help_theme: "wallabag настраиваемый, здесь Вы можете выбрать тему."
80 help_items_per_page: "Вы можете выбрать количество отображаемых записей на странице."
81 help_reading_speed: "wallabag посчитает сколько времени занимает чтение каждой записи. Вы можете определить здесь, как быстро вы читаете. wallabag пересчитает время чтения для каждой записи."
82 help_language: "Вы можете изменить язык интерфейса wallabag."
83 help_pocket_consumer_key: "Обязательно для импорта из Pocket. Вы можете создать это в Вашем аккаунте на Pocket."
84 form_rss:
85 description: 'RSS фид созданный с помощью wallabag позволяет читать Ваши записи через Ваш любимый RSS агрегатор. Для начала Вам потребуется создать ключ.'
86 token_label: 'RSS ключ'
87 no_token: 'Ключ не задан'
88 token_create: 'Создать ключ'
89 token_reset: 'Пересоздать ключ'
90 rss_links: 'ссылка на RSS'
91 rss_link:
92 unread: 'непрочитанные'
93 starred: 'помеченные'
94 archive: 'архивные'
95 rss_limit: 'Количество записей в фиде'
96 form_user:
97 two_factor_description: "Включить двухфакторную аутентификацию, Вы получите сообщение на указанный email с кодом, при каждом новом непроверенном подключении."
98 name_label: 'Имя'
99 email_label: 'Email'
100 twoFactorAuthentication_label: 'Двухфакторная аутентификация'
101 help_twoFactorAuthentication: "Если Вы включите двухфакторную аутентификацию, то Вы будете получать код на указанный ранее email, каждый раз при входе в wallabag."
102 delete:
103 title: "Удалить мой аккаунт (или опасная зона)"
104 description: "Если Вы удалите ваш аккаунт, ВСЕ ваши записи, теги и другие данные, будут БЕЗВОЗВРАТНО удалены (операция не может быть отменена после). Затем Вы выйдете из системы."
105 confirm: "Вы точно уверены? (Данные будут БЕЗВОЗВРАТНО удалены, эти действия необратимы)"
106 button: "Удалить мой аккаунт"
107 reset:
108 title: "Сброс данных (или опасная зона)"
109 description: "По нажатию на кнопки ниже, Вы сможете удалить часть данных из Вашего аккаунта. Будьте осторожны, эти действия необратимы."
110 annotations: "Удалить все аннотации"
111 tags: "Удалить все теги"
112 entries: "Удалить все записи"
113 confirm: "Вы уверены? (Данные будут БЕЗВОЗВРАТНО удалены, эти действия необратимы)"
114 form_password:
115 description: "Здесь Вы можете поменять своя пароль. Ваш пароль должен быть длиннее 8 символов."
116 old_password_label: 'Текущий пароль'
117 new_password_label: 'Новый пароль'
118 repeat_new_password_label: 'Повторите новый пароль'
119 form_rules:
120 if_label: 'Если'
121 then_tag_as_label: 'тогда тег'
122 delete_rule_label: 'удалить'
123 edit_rule_label: 'изменить'
124 rule_label: 'Правило'
125 tags_label: 'теги'
126 faq:
127 title: 'FAQ'
128 tagging_rules_definition_title: 'Что значит "правило тегирования"?'
129 tagging_rules_definition_description: 'Правила, по которым Wallabag автоматически добавит теги для новых записей.<br />Каждый раз, при добавлении новых записей, будут проставляться теги к записям, согласно настроенным правилам тегирования, это избавит Вас от необходимости проставлять теги для каждой записи вручную.'
130 how_to_use_them_title: 'Как мне их использовать?'
131 how_to_use_them_description: 'Предположим, вы хотите пометить новые записи как "<i>короткая</i>", когда на чтение уйдет меньше 3 минут.<br />В этом случае, установите " readingTime &lt;= 3 " в поле <i>Правила</i> и "<i>короткая</i>" в поле <i>Теги</i>.<br />Несколько тегов могут добавляться одновременно, разделяя их запятой: "<i>короткая, прочитать обязательно</i>" <br />Сложные правила могут быть записаны с использованием предопределенных операторов: если "<i>readingTime &gt;= 5 AND domainName = \"github.com\"</i> " тогда тег будет "<i>долго читать, github </i>"'
132 variables_available_title: 'Какие переменные и операторы я могу использовать для написания правил?'
133 variables_available_description: 'Следующие переменные и операторы могут использоваться для создания правил тегов:'
134 meaning: 'Смысл'
135 variable_description:
136 label: 'Переменные'
137 title: 'Название записи'
138 url: 'Ссылка на запись'
139 isArchived: 'Независимо от того, архивная запись или нет'
140 isStarred: 'Независимо от того, помечена запись или нет'
141 content: "Содержимое записи"
142 language: "Язык записи"
143 mimetype: "Mime-type записи"
144 readingTime: "Оценочное время чтения записи в минутах"
145 domainName: 'Домен, с которого была скачана запись'
146 operator_description:
147 label: 'Оператор'
148 less_than: 'Меньше чем...'
149 strictly_less_than: 'Строго меньше чем...'
150 greater_than: 'Больше чем...'
151 strictly_greater_than: 'Строго больше чем...'
152 equal_to: 'Равно...'
153 not_equal_to: 'Не равно...'
154 or: 'Одно правило ИЛИ другое'
155 and: 'Одно правило И другое'
156 matches: 'Тесты, в которых <i> тема </i> соответствует <i> поиску </i> (без учета регистра). Пример: <code> title matches "футбол" </code>'
157
158entry:
159 page_titles:
160 unread: 'Непрочитанные записи'
161 starred: 'Помеченные записи'
162 archived: 'Архивные записи'
163 filtered: 'Отфильтрованные записи'
164 filtered_tags: 'Отфильтрованные по тегу:'
165 filtered_search: 'Отфильтрованные по поиску:'
166 untagged: 'Записи без тегов'
167 list:
168 number_on_the_page: '{0} Записей не обнаружено.|{1} Одна запись.|]1,Inf[ Найдено %count% записей.'
169 reading_time: 'расчетное время чтения'
170 reading_time_minutes: 'расчетное время чтения: %readingTime% мин'
171 reading_time_less_one_minute: 'расчетное время чтения: &lt; 1 мин'
172 number_of_tags: '{1}и один другой тег|]1,Inf[и %count% другие теги'
173 reading_time_minutes_short: '%readingTime% мин'
174 reading_time_less_one_minute_short: '&lt; 1 мин'
175 original_article: 'оригинал'
176 toogle_as_read: 'Пометить, как прочитанное'
177 toogle_as_star: 'Пометить звездочкой'
178 delete: 'Удалить'
179 export_title: 'Экспорт'
180 filters:
181 title: 'Фильтры'
182 status_label: 'Статус'
183 archived_label: 'Архивная'
184 starred_label: 'Помеченная звездочкой'
185 unread_label: 'Непрочитанная'
186 preview_picture_label: 'Есть картинка предварительного просмотра'
187 preview_picture_help: 'Картинка предварительного просмотра'
188 language_label: 'Язык'
189 http_status_label: 'статус HTTP'
190 reading_time:
191 label: 'Время чтения в минутах'
192 from: 'с'
193 to: 'по'
194 domain_label: 'Доменное имя'
195 created_at:
196 label: 'Дата создания'
197 from: 'с'
198 to: 'по'
199 action:
200 clear: 'Очистить'
201 filter: 'Выбрать'
202 view:
203 left_menu:
204 back_to_top: 'Наверх'
205 back_to_homepage: 'Назад'
206 set_as_read: 'Пометить, как прочитанное'
207 set_as_unread: 'Пометить, как не прочитанное'
208 set_as_starred: 'Пометить звездочкой'
209 view_original_article: 'Оригинальное название'
210 re_fetch_content: 'Перезагрузить содержимое'
211 delete: 'Удалить'
212 add_a_tag: 'Добавить тег'
213 share_content: 'Поделиться'
214 share_email_label: 'Email'
215 public_link: 'публичная ссылка'
216 delete_public_link: 'удалить публичную ссылку'
217 export: 'Экспорт'
218 print: 'Печатать'
219 problem:
220 label: 'Проблемы?'
221 description: 'Что-то не так с записью?'
222 edit_title: 'Изменить название'
223 original_article: 'оригинал'
224 annotations_on_the_entry: '{0} Нет аннотации|{1} Одна аннотация|]1,Inf[ %count% аннотаций'
225 created_at: 'Дата создания'
226 new:
227 page_title: 'Сохранить новую запись'
228 placeholder: 'http://website.com'
229 form_new:
230 url_label: Ссылка
231 search:
232 placeholder: 'Что Вы ищете?'
233 edit:
234 page_title: 'Изменить запись'
235 title_label: 'Название'
236 url_label: 'Ссылка'
237 is_public_label: 'Публичная'
238 save_label: 'Сохранить'
239 public:
240 shared_by_wallabag: "Запись была опубликована <a href='%wallabag_instance%'>wallabag</a>"
241
242about:
243 page_title: 'О'
244 top_menu:
245 who_behind_wallabag: 'Кто стоит за wallabag'
246 getting_help: 'Помощь'
247 helping: 'Помочь wallabag'
248 contributors: 'Авторы'
249 third_party: 'Сторонние библиотеки'
250 who_behind_wallabag:
251 developped_by: 'Разработано'
252 website: 'веб-сайт'
253 many_contributors: 'и другие разработчики ♥ <a href="https://github.com/wallabag/wallabag/graphs/contributors">на Github</a>'
254 project_website: 'Веб-сайт проекта'
255 license: 'Лицензия'
256 version: 'Версия'
257 getting_help:
258 documentation: 'Документация'
259 bug_reports: 'Отчеты об ошибках'
260 support: '<a href="https://github.com/wallabag/wallabag/issues">на GitHub</a>'
261 helping:
262 description: 'wallabag является бесплатным и открытым исходным кодом. Вы можете помочь нам:'
263 by_contributing: 'путем внесения вклада в проект:'
264 by_contributing_2: 'с решением наших проблем'
265 by_paypal: 'с помощью Paypal'
266 contributors:
267 description: 'Спасибо вам за вклад в веб-приложение wallabag'
268 third_party:
269 description: 'Ниже приведен список сторонних библиотек, используемых в wallabag (с их лицензиями):'
270 package: 'Пакет'
271 license: 'Лицензия'
272
273howto:
274 page_title: 'How to'
275 tab_menu:
276 add_link: "Добавить ссылку"
277 shortcuts: "Использовать горячии клавиши"
278 page_description: 'Несколько способов сохранить запись:'
279 top_menu:
280 browser_addons: 'Посмотреть дополнения'
281 mobile_apps: 'Мобильные приложения'
282 bookmarklet: 'Закладка'
283 form:
284 description: 'Спасибо за эту форму'
285 browser_addons:
286 firefox: 'Аддоны для Firefox'
287 chrome: 'Аддоны для Chrome'
288 opera: 'Аддоны для Opera'
289 mobile_apps:
290 android:
291 via_f_droid: 'в F-Droid'
292 via_google_play: 'в Google Play'
293 ios: 'в iTunes Store'
294 windows: 'в Microsoft Store'
295 bookmarklet:
296 description: 'Drag & drop эту ссылку в ваши закладки:'
297 shortcuts:
298 page_description: "Здесь доступны горячие клавиши wallabag."
299 shortcut: "Горячие клавиши"
300 action: "Действия"
301 all_pages_title: "Горячие клавиши на всех страницах"
302 go_unread: "Перейти в непрочитанные"
303 go_starred: "Перейти в помеченные звездочкой"
304 go_archive: "Перейти в архивные"
305 go_all: "Перейти ко всем записям"
306 go_tags: "Перейти в теги"
307 go_config: "Перейти в настройки"
308 go_import: "Перейти в импорт"
309 go_developers: "Перейти к разработчикам"
310 go_howto: "Перейти в howto (эта страница!)"
311 go_logout: "Выйти"
312 list_title: "Горячие клавиши, доступные на страницу списков"
313 search: "Отобразить форму поиска"
314 article_title: "Горячие клавиши доступные во время просмотра записи"
315 open_original: "Открыть оригинальную ссылку на запись"
316 toggle_favorite: "Переключить пометку звездочкой для записи"
317 toggle_archive: "Переключить пометку о прочтении для записи"
318 delete: "Удалить запись"
319 material_title: "Горячие клавиши доступные только в Material теме"
320 add_link: "Добавить новую запись"
321 hide_form: "Скрыть текущую форму (поисковую или новой ссылки)"
322 arrows_navigation: "Навигация по статьям"
323 open_article: "Отобразить выбранную запись"
324
325quickstart:
326 page_title: 'Быстрый старт'
327 more: 'Еще…'
328 intro:
329 title: 'Приветствую в wallabag!'
330 paragraph_1: "Мы будем сопровождать вас при посещении wallabag и покажем вам некоторые функции, которые могут вас заинтересовать."
331 paragraph_2: 'Следите за нами!'
332 configure:
333 title: 'Настроить приложение'
334 description: 'Чтобы иметь приложение, которое вам подходит, ознакомьтесь с конфигурацией wallabag.'
335 language: 'Выбрать язык и дизайн'
336 rss: 'Включить RSS фид'
337 tagging_rules: 'Создать правило для автоматической установки тегов'
338 admin:
339 title: 'Администрирование'
340 description: 'Как администратор, у Вас есть привилегии на wallabag. Вы можете:'
341 new_user: 'Создать нового пользователя'
342 analytics: 'Настроить аналитику'
343 sharing: 'Включить сервисы для публикации записей'
344 export: 'Настроить экспорт'
345 import: 'Настроить импорт'
346 first_steps:
347 title: 'Первый шаг'
348 description: "Теперь wallabag хорошо настроен, пришло время архивировать Интернет. Вы можете нажать на верхний правый знак +, чтобы добавить ссылку."
349 new_article: 'Сохраните свою первую запись'
350 unread_articles: 'И добавьте к ней тег!'
351 migrate:
352 title: 'Перейти с существующего сервиса'
353 description: "Вы используете другой сервис? Мы поможем перенести данные на wallabag."
354 pocket: 'Перейти с Pocket'
355 wallabag_v1: 'Перейти с wallabag 1 версии'
356 wallabag_v2: 'Перейти с wallabag 2 версии'
357 readability: 'Перейти с Readability'
358 instapaper: 'Перейти с Instapaper'
359 developer:
360 title: 'Для разработчиков'
361 description: 'Мы также подумали о разработчиках: Docker, API, переводы, и т.д.'
362 create_application: 'Создать ваше стороннее приложение'
363 use_docker: 'Использовать Docker для установки wallabag'
364 docs:
365 title: 'Полная документация'
366 description: "В wallabag есть так много особенностей. Не стесняйтесь читать руководство, чтобы узнать эти особенности и как их использовать."
367 annotate: 'Комментирование своей статьи'
368 export: 'Конвертировать ваши статьи в ePUB или PDF'
369 search_filters: 'Посмотрите, как Вы можете искать записи с помощью поисковой системы и фильтров'
370 fetching_errors: 'Что я могу сделать, если во время извлечения записей произошла ошибка?'
371 all_docs: 'И много других записей!'
372 support:
373 title: 'Поддержка'
374 description: 'Если Вам нужна помощь, мы здесь чтобы помочь Вам.'
375 github: 'На GitHub'
376 email: 'По email'
377 gitter: 'На Gitter'
378
379tag:
380 page_title: 'Теги'
381 list:
382 number_on_the_page: '{0} Теги не найдены.|{1} Найден один тег.|]1,Inf[ Найдено %count% тегов.'
383 see_untagged_entries: 'Просмотреть записи без тегов'
384 new:
385 add: 'Добавить'
386 placeholder: 'Вы можете добавить несколько тегов, разделенных запятой.'
387
388import:
389 page_title: 'Импорт'
390 page_description: 'Добро пожаловать в импортер wallabag. Выберите сервис, из которого вы хотите перенести данные.'
391 action:
392 import_contents: 'Импортировать данные'
393 form:
394 mark_as_read_title: 'Отметить все, как прочитанное?'
395 mark_as_read_label: 'Пометить все импортированные записи, как прочитанные'
396 file_label: 'Файл'
397 save_label: 'Загрузить файл'
398 pocket:
399 page_title: 'Импорт в Pocket'
400 description: "Функция импрота добавит все ваши данные в формате Pocket. Pocket не позволяет нам извлекать контент из своего сервиса, поэтому содержимое записей будет повторно перезакачана."
401 config_missing:
402 description: "Импорт из Pocket не настроен."
403 admin_message: 'Вам нужно добавить %keyurls% pocket_consumer_key%keyurle%.'
404 user_message: 'Администратор сервера должен добавить ключ для API Pocket.'
405 authorize_message: 'Вы можете импортировать свои данные из своего аккаунта на Pocket. Вам просто нужно нажать на кнопку ниже и разрешить приложению подключение к getpocket.com.'
406 connect_to_pocket: 'Подключиться к Pocket и импортировать данные'
407 wallabag_v1:
408 page_title: 'Импорт > Wallabag v1'
409 description: 'Функция импорта добавит все ваши записи из wallabag v1. На странице конфигурации нажмите "Экспорт JSON" в разделе "Экспорт данных wallabag". У вас появится файл "wallabag-export-1-xxxx-xx-xx.json".'
410 how_to: 'Выберите свой файл с настройками экспорта wallabag и нажмите кнопку ниже, чтобы загрузить файл и начать импорт.'
411 wallabag_v2:
412 page_title: 'Импорт > Wallabag v2'
413 description: 'Функция импорта добавит все ваши записи wallabag v2. Перейдите ко всем статьям, затем на боковой панели экспорта нажмите "JSON". У вас появится файл со всеми записями "All articles.json".'
414 readability:
415 page_title: 'Импорт > Readability'
416 description: 'Функция импорта добавит все ваши записи для чтения. На странице инструментов (https://www.readability.com/tools/) нажмите "Экспорт ваших данных" в разделе "Экспорт данных". Вы получите электронное письмо для загрузки json (что не заканчивается только .json файлом).'
417 how_to: 'Выберите экспорт Readability и нажмите кнопку ниже, чтобы загрузить файл и начать импорт.'
418 worker:
419 enabled: "Импорт производится асинхронно. После запуска задачи импорта внешний процесс будет обрабатывать задания по одному за раз. Текущая процедура:"
420 download_images_warning: "Вы включили загрузку изображений для своих записей. В сочетании с обычным импортом может потребоваться много времени, что может привести к ошибке. Мы <strong>настоятельно рекомендуем</strong> включить асинхронный импорт, чтобы избежать ошибок."
421 firefox:
422 page_title: 'Импорт > Firefox'
423 description: "Функция импорта добавит все ваши закладки из Firefox. Просто зайдите в закладки (Ctrl + Maj + O), затем в \"Импорт и резервное копирование\", выберите \"Резервное копирование ...\". Вы получите файл .json."
424 how_to: "Выберите файл резервной копии закладок и нажмите кнопку ниже, чтобы импортировать его. Обратите внимание, что процесс может занять много времени, поскольку все статьи должны быть загружены."
425 chrome:
426 page_title: 'Импорт > Chrome'
427 description: "Функция импорта добавит все ваши закладки из Chrome. Расположение фйла зависит от операционной системы: <ul><li>На Linux, перейдите в папку <code>~/.config/chromium/Default/</code></li><li>На Windows, должно находиться в <code>%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default</code></li><li>На OS X, должно находиться в <code>~/Library/Application Support/Google/Chrome/Default/Bookmarks</code></li></ul>После того, как перейдете в папку, скопируйте файл <code>Bookmarks</code>. <em><br>Примечание, если у вас есть Chromium вместо Chrome, вам потребуется скорректировать пути соответственно.</em></p>"
428 how_to: "Выберите файл резервной копии закладок и нажмите кнопку ниже, чтобы импортировать его. Обратите внимание, что процесс может занять много времени, поскольку все статьи должны быть загружены."
429 instapaper:
430 page_title: 'Импорт > Instapaper'
431 description: 'Функция импорта добавит все ваши закладки из Instapaper. На странице настроек (https://www.instapaper.com/user), нажмите на "Скачать .CSV файл" в разделе "Экспорт". CSV файл будет загружен (наподобии "instapaper-export.csv").'
432 how_to: 'Выберите файл Instapaper и нажмите кнопку ниже, чтобы импортировать его.'
433 pinboard:
434 page_title: "Импорт > Pinboard"
435 description: 'Функция импорта добавит все ваши закладки из Pinboard. На странице резервирования (https://pinboard.in/settings/backup), нажмите на "JSON" в разделе "Закладки". JSON файл будет загружен (наподобии "pinboard_export").'
436 how_to: 'Выберите файл Pinboard и нажмите кнопку ниже, чтобы импортировать его.'
437
438developer:
439 page_title: 'Управление клиентским API'
440 welcome_message: 'Добро пожаловать в API wallabag'
441 documentation: 'Документация'
442 how_to_first_app: 'Как создать мое первое приложение'
443 full_documentation: 'Посмотреть полную документацию по API'
444 list_methods: 'Список методов API'
445 clients:
446 title: 'Клиенты'
447 create_new: 'Создать нового клиента'
448 existing_clients:
449 title: 'Текущие клиенты'
450 field_id: 'ID клиента'
451 field_secret: 'secret-код клиента'
452 field_uris: 'Ссылки перенаправлений'
453 field_grant_types: 'Разрешенные типы доступа'
454 no_client: 'Нет клиентов.'
455 remove:
456 warn_message_1: 'Вы имеете возможность удалить клиента %name%. Данные клиента будут удалены БЕЗВОЗВРАТНО!'
457 warn_message_2: "Если вы удалите его, каждое приложение настроенное через этого клиента не сможет авторизоваться в вашем wallabag."
458 action: 'Удалить клиента %name%'
459 client:
460 page_title: 'Управление клиентским API > Новый клиент'
461 page_description: 'Вы находитесь в разделе создания нового клиента. Пожалуйста заполните поля ниже для перенаправления к вашему приложению.'
462 form:
463 name_label: 'Название клиента'
464 redirect_uris_label: 'Ссылка перенаправления (опционально)'
465 save_label: 'Создать нового клиента'
466 action_back: 'Назад'
467 client_parameter:
468 page_title: 'Управление клиентским API > Параметры клиента'
469 page_description: 'Здесь ваши параметры клиента.'
470 field_name: 'Название клиента'
471 field_id: 'ID клиента'
472 field_secret: 'Secret-код клиента'
473 back: 'Назад'
474 read_howto: 'Прочитать "как создать мое первое приложение"'
475 howto:
476 page_title: 'Управление клиентским API > Как создать мое первое приложение'
477 description:
478 paragraph_1: 'Следующие комманды используют библиотеку <a href="https://github.com/jkbrzt/httpie">HTTPie</a>. Убедитесь, что она у вас установлена, прежде чем использовать ее.'
479 paragraph_2: 'Вам нужен ключ для того, чтобы ваше стороннее приложение и wallabag API могли обмениваться данными.'
480 paragraph_3: 'Для создания ключа, вам необходимо <a href="%link%">создать нового клиента</a>.'
481 paragraph_4: 'Теперь создать ключ (замените client_id, client_secret, username and password на ваши значения):'
482 paragraph_5: 'API вернет рабочую ссылку, наподобии:'
483 paragraph_6: 'Ключ доступа access_token используется для вызова конечной точки API. Пример:'
484 paragraph_7: 'Этот вызов вернет все записи для вашего пользователя.'
485 paragraph_8: 'Если Вы хотите увидеть все конечные точки API, то смотрите <a href="%link%">в нашу документацию по API</a>.'
486 back: 'Назад'
487
488user:
489 page_title: "Управление пользователями"
490 new_user: "Создать нового пользователя"
491 edit_user: "Изменить существующего пользователя"
492 description: "Здесь Вы можете управлять всеми пользователями (создание, изменение и удаление)"
493 list:
494 actions: "Действия"
495 edit_action: "Изменить"
496 yes: "Да"
497 no: "Нет"
498 create_new_one: "Создать нового пользователя"
499 form:
500 username_label: 'Логин'
501 name_label: 'Имя'
502 password_label: 'Пароль'
503 repeat_new_password_label: 'Повторите новый пароль'
504 plain_password_label: '????'
505 email_label: 'Email'
506 enabled_label: 'Включить'
507 last_login_label: 'Последний вход'
508 twofactor_label: "Двухфакторная аутентификация"
509 save: "Сохранить"
510 delete: "Удалить"
511 delete_confirm: "Вы уверены?"
512 back_to_list: "Назад к списку"
513
514error:
515 page_title: "Произошла ошибка"
516
517flashes:
518 config:
519 notice:
520 config_saved: 'Настройки сохранены.'
521 password_updated: 'Пароль обновлен'
522 password_not_updated_demo: "В режиме демонстрации нельзя изменять пароль для этого пользователя."
523 user_updated: 'Информация обновлена'
524 rss_updated: 'RSS информация обновлена'
525 tagging_rules_updated: 'Правила тегировния обновлены'
526 tagging_rules_deleted: 'Правила тегировния удалены'
527 rss_token_updated: 'RSS ключ обновлен'
528 annotations_reset: "Аннотации сброшены"
529 tags_reset: "Теги сброшены"
530 entries_reset: "Записи сброшены"
531 entry:
532 notice:
533 entry_already_saved: 'Запись была сохранена ранее %date%'
534 entry_saved: 'Запись сохранена'
535 entry_saved_failed: 'Запись сохранена, но содержимое не удалось получить'
536 entry_updated: 'Запись обновлена'
537 entry_reloaded: 'Запись перезагружена'
538 entry_reloaded_failed: 'Запись перезагружена, но содержимое не удалось получить'
539 entry_archived: 'Запись перемещена в архив'
540 entry_unarchived: 'Запись перемещена из архива'
541 entry_starred: 'Запись помечена звездочкой'
542 entry_unstarred: 'Пометка звездочкой у записи убрана'
543 entry_deleted: 'Запись удалена'
544 tag:
545 notice:
546 tag_added: 'Тег добавлен'
547 import:
548 notice:
549 failed: 'Во время импорта произошла ошибка, повторите попытку.'
550 failed_on_file: 'Ошибка при обработке данных для импорта. Пожалуйста проверьте свой файл импорта.'
551 summary: 'Статистика импорта: %imported% импортировано, %skipped% уже сохранено.'
552 summary_with_queue: 'Статистика импорта: %queued% в очереди.'
553 error:
554 redis_enabled_not_installed: "Redis включен, для приема асинхронного импорта, но похоже, что <u>мы не можем к нему подключиться</u>. Пожалуйста проверьте настройки Redis."
555 rabbit_enabled_not_installed: "RabbitMQ включен, для приема асинхронного импорта, но похоже, что <u>мы не можем к нему подключиться</u>. Пожалуйста проверьте настройки RabbitMQ."
556 developer:
557 notice:
558 client_created: 'Новый клиент %name% добавлен.'
559 client_deleted: 'Клиент %name% удален'
560 user:
561 notice:
562 added: 'Пользователь "%username%" добавлен'
563 updated: 'Пользователь "%username%" обновлен'
564 deleted: 'Пользователь "%username%" удален' \ No newline at end of file
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
index d3180f42..563bc50b 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
@@ -32,6 +32,7 @@ menu:
32 # save_link: 'Save a link' 32 # save_link: 'Save a link'
33 back_to_unread: 'Okunmayan makalelere geri dön' 33 back_to_unread: 'Okunmayan makalelere geri dön'
34 # users_management: 'Users management' 34 # users_management: 'Users management'
35 # site_credentials: 'Site credentials'
35 top: 36 top:
36 add_new_entry: 'Yeni bir makale ekle' 37 add_new_entry: 'Yeni bir makale ekle'
37 search: 'Ara' 38 search: 'Ara'
@@ -76,6 +77,7 @@ config:
76 # redirect_current_page: 'To the current page' 77 # redirect_current_page: 'To the current page'
77 # pocket_consumer_key_label: Consumer key for Pocket to import contents 78 # pocket_consumer_key_label: Consumer key for Pocket to import contents
78 # android_configuration: Configure your Android application 79 # android_configuration: Configure your Android application
80 # android_instruction: "Touch here to prefill your Android application"
79 # help_theme: "wallabag is customizable. You can choose your prefered theme here." 81 # help_theme: "wallabag is customizable. You can choose your prefered theme here."
80 # help_items_per_page: "You can change the number of articles displayed on each page." 82 # help_items_per_page: "You can change the number of articles displayed on each page."
81 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article." 83 # help_reading_speed: "wallabag calculates a reading time for each article. You can define here, thanks to this list, if you are a fast or a slow reader. wallabag will recalculate the reading time for each article."
@@ -89,9 +91,10 @@ config:
89 token_reset: 'Belirteci (token) sıfırla' 91 token_reset: 'Belirteci (token) sıfırla'
90 rss_links: 'RSS akış bağlantıları' 92 rss_links: 'RSS akış bağlantıları'
91 rss_link: 93 rss_link:
92 unread: 'okunmayan' 94 unread: 'Okunmayan'
93 starred: 'favoriler' 95 starred: 'Favoriler'
94 archive: 'arşiv' 96 archive: 'Arşiv'
97 # all: 'All'
95 rss_limit: 'RSS içeriğinden talep edilecek makale limiti' 98 rss_limit: 'RSS içeriğinden talep edilecek makale limiti'
96 form_user: 99 form_user:
97 two_factor_description: "İki adımlı doğrulamayı aktifleştirdiğinizde, her yeni güvenilmeyen bağlantılarda size e-posta ile bir kod alacaksınız." 100 two_factor_description: "İki adımlı doğrulamayı aktifleştirdiğinizde, her yeni güvenilmeyen bağlantılarda size e-posta ile bir kod alacaksınız."
@@ -110,6 +113,7 @@ config:
110 # annotations: Remove ALL annotations 113 # annotations: Remove ALL annotations
111 # tags: Remove ALL tags 114 # tags: Remove ALL tags
112 # entries: Remove ALL entries 115 # entries: Remove ALL entries
116 # archived: Remove ALL archived entries
113 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE) 117 # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
114 form_password: 118 form_password:
115 # description: "You can change your password here. Your new password should by at least 8 characters long." 119 # description: "You can change your password here. Your new password should by at least 8 characters long."
@@ -154,6 +158,7 @@ config:
154 or: 'Bir kural veya birbaşkası' 158 or: 'Bir kural veya birbaşkası'
155 and: 'Bir kural ve diğeri' 159 and: 'Bir kural ve diğeri'
156 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>' 160 # matches: 'Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
161 # notmatches: 'Tests that a <i>subject</i> is not matches a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
157 162
158entry: 163entry:
159 page_titles: 164 page_titles:
@@ -164,6 +169,7 @@ entry:
164 # filtered_tags: 'Filtered by tags:' 169 # filtered_tags: 'Filtered by tags:'
165 # filtered_search: 'Filtered by search:' 170 # filtered_search: 'Filtered by search:'
166 # untagged: 'Untagged entries' 171 # untagged: 'Untagged entries'
172 # all: 'All entries'
167 list: 173 list:
168 number_on_the_page: '{0} Herhangi bir makale yok.|{1} Burada bir adet makale var.|]1,Inf[ Burada %count% adet makale var.' 174 number_on_the_page: '{0} Herhangi bir makale yok.|{1} Burada bir adet makale var.|]1,Inf[ Burada %count% adet makale var.'
169 reading_time: 'tahmini okuma süresi' 175 reading_time: 'tahmini okuma süresi'
@@ -223,6 +229,8 @@ entry:
223 original_article: 'orijinal' 229 original_article: 'orijinal'
224 # annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations' 230 # annotations_on_the_entry: '{0} No annotations|{1} One annotation|]1,Inf[ %count% annotations'
225 created_at: 'Oluşturulma tarihi' 231 created_at: 'Oluşturulma tarihi'
232 # published_at: 'Publication date'
233 # published_by: 'Published by'
226 new: 234 new:
227 page_title: 'Yeni makaleyi kaydet' 235 page_title: 'Yeni makaleyi kaydet'
228 placeholder: 'http://website.com' 236 placeholder: 'http://website.com'
@@ -234,10 +242,12 @@ entry:
234 page_title: 'Makaleyi düzenle' 242 page_title: 'Makaleyi düzenle'
235 title_label: 'Başlık' 243 title_label: 'Başlık'
236 url_label: 'Url' 244 url_label: 'Url'
237 is_public_label: 'Herkes tarafından erişime açık olsun mu?'
238 save_label: 'Kaydet' 245 save_label: 'Kaydet'
239 public: 246 public:
240 # shared_by_wallabag: "This article has been shared by <a href='%wallabag_instance%'>wallabag</a>" 247 # shared_by_wallabag: "This article has been shared by %username% with <a href='%wallabag_instance%'>wallabag</a>"
248 confirm:
249 # delete: "Are you sure you want to remove that article?"
250 # delete_tag: "Are you sure you want to remove that tag from that article?"
241 251
242about: 252about:
243 page_title: 'Hakkımızda' 253 page_title: 'Hakkımızda'
@@ -385,6 +395,9 @@ tag:
385 # add: 'Add' 395 # add: 'Add'
386 # placeholder: 'You can add several tags, separated by a comma.' 396 # placeholder: 'You can add several tags, separated by a comma.'
387 397
398# export:
399# footer_template: '<div style="text-align:center;"><p>Produced by wallabag with %method%</p><p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p></div>'
400
388import: 401import:
389 page_title: 'İçe Aktar' 402 page_title: 'İçe Aktar'
390 page_description: 'wallabag içe aktarma aracına hoşgeldiniz. Lütfen içe aktarmak istediğiiz önceki servisinizi seçin.' 403 page_description: 'wallabag içe aktarma aracına hoşgeldiniz. Lütfen içe aktarmak istediğiiz önceki servisinizi seçin.'
@@ -510,6 +523,8 @@ user:
510 # delete: Delete 523 # delete: Delete
511 # delete_confirm: Are you sure? 524 # delete_confirm: Are you sure?
512 # back_to_list: Back to list 525 # back_to_list: Back to list
526 search:
527 # placeholder: Filter by username or email
513 528
514error: 529error:
515 # page_title: An error occurred 530 # page_title: An error occurred
@@ -528,6 +543,7 @@ flashes:
528 # annotations_reset: Annotations reset 543 # annotations_reset: Annotations reset
529 # tags_reset: Tags reset 544 # tags_reset: Tags reset
530 # entries_reset: Entries reset 545 # entries_reset: Entries reset
546 # archived_reset: Archived entries deleted
531 entry: 547 entry:
532 notice: 548 notice:
533 entry_already_saved: 'Entry already saved on %date%' 549 entry_already_saved: 'Entry already saved on %date%'
@@ -562,3 +578,8 @@ flashes:
562 # added: 'User "%username%" added' 578 # added: 'User "%username%" added'
563 # updated: 'User "%username%" updated' 579 # updated: 'User "%username%" updated'
564 # deleted: 'User "%username%" deleted' 580 # deleted: 'User "%username%" deleted'
581 site_credential:
582 notice:
583 # added: 'Site credential for "%host%" added'
584 # updated: 'Site credential for "%host%" updated'
585 # deleted: 'Site credential for "%host%" deleted'
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..87f00f10 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml
@@ -1,6 +1,7 @@
1validator: 1validator:
2 password_must_match: 'Cal que los dos senhals siasquen los meteisses' 2 password_must_match: 'Cal que los dos senhals correspondan'
3 password_too_short: 'Lo senhal deu aver almens 8 caractèrs' 3 password_too_short: 'Lo senhal deu aver almens 8 caractèrs'
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: 'Aquesta citacion es tròpa longa. Cal que faga {{ limit }} caractèrs o mens.'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml
index ffcd5e7f..e4165c14 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: 'Cytat jest zbyt długi. powinien mieć {{ limit }} znaków lub mniej.'
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.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.ru.yml
new file mode 100644
index 00000000..567b8e6e
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/translations/validators.ru.yml
@@ -0,0 +1,6 @@
1validator:
2 password_must_match: 'Пароли должны совпадать.'
3 password_too_short: 'Пароль должен быть длиннее 8 символов'
4 password_wrong_value: 'Неправильный текущий пароль'
5 item_per_page_too_high: 'Это, безусловно, убьет приложение'
6 rss_limit_too_high: 'Это, безусловно, убьет приложение'
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/baggy/Config/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig
index 3548f590..bcc57dac 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig
@@ -82,7 +82,7 @@
82 <fieldset class="w500p inline"> 82 <fieldset class="w500p inline">
83 <div class="row"> 83 <div class="row">
84 <h3>{{ 'config.form_settings.android_configuration'|trans }}</h3> 84 <h3>{{ 'config.form_settings.android_configuration'|trans }}</h3>
85 <a href="wallabag://{{ app.user.username }}@{{ wallabag_url }}" >Touch here to prefill your Android application</a> 85 <a href="wallabag://{{ app.user.username }}@{{ wallabag_url }}">{{ 'config.form_settings.android_instruction' | trans }}</a>
86 <br/> 86 <br/>
87 <img id="androidQrcode" /> 87 <img id="androidQrcode" />
88 <script> 88 <script>
@@ -106,7 +106,7 @@
106 106
107 <fieldset class="w500p inline"> 107 <fieldset class="w500p inline">
108 <div class="row"> 108 <div class="row">
109 <label>Rss token</label> 109 <label>{{ 'config.form_rss.token_label'|trans }}</label>
110 {% if rss.token %} 110 {% if rss.token %}
111 {{ rss.token }} 111 {{ rss.token }}
112 {% else %} 112 {% else %}
@@ -128,9 +128,10 @@
128 <div class="row"> 128 <div class="row">
129 <label>{{ 'config.form_rss.rss_links'|trans }}</label> 129 <label>{{ 'config.form_rss.rss_links'|trans }}</label>
130 <ul> 130 <ul>
131 <li><a href="{{ path('unread_rss', {'username': rss.username, 'token': rss.token}) }}">unread</a></li> 131 <li><a href="{{ path('unread_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.unread'|trans }}</a></li>
132 <li><a href="{{ path('starred_rss', {'username': rss.username, 'token': rss.token}) }}">fav</a></li> 132 <li><a href="{{ path('starred_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.starred'|trans }}</a></li>
133 <li><a href="{{ path('archive_rss', {'username': rss.username, 'token': rss.token}) }}">archives</a></li> 133 <li><a href="{{ path('archive_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.archive'|trans }}</a></li>
134 <li><a href="{{ path('all_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.all'|trans }}</a></li>
134 </ul> 135 </ul>
135 </div> 136 </div>
136 </fieldset> 137 </fieldset>
@@ -200,6 +201,11 @@
200 </a> 201 </a>
201 </li> 202 </li>
202 <li> 203 <li>
204 <a href="{{ path('config_reset', { type: 'archived'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
205 {{ 'config.reset.archived'|trans }}
206 </a>
207 </li>
208 <li>
203 <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red"> 209 <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
204 {{ 'config.reset.entries'|trans }} 210 {{ 'config.reset.entries'|trans }}
205 </a> 211 </a>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entries.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entries.html.twig
index 859b166b..12cead48 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entries.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entries.html.twig
@@ -1,5 +1,12 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %} 1{% extends "WallabagCoreBundle::layout.html.twig" %}
2 2
3{% block head %}
4 {{ parent() }}
5 {% if tag is defined and app.user.config.rssToken %}
6 <link rel="alternate" type="application/rss+xml" href="{{ path('tag_rss', {'username': app.user.username, 'token': app.user.config.rssToken, 'slug': tag.slug}) }}" />
7 {% endif %}
8{% endblock %}
9
3{% block title %} 10{% block title %}
4 {% set filter = '' %} 11 {% set filter = '' %}
5 {% if tag is defined %} 12 {% if tag is defined %}
@@ -12,14 +19,17 @@
12{% endblock %} 19{% endblock %}
13 20
14{% block content %} 21{% block content %}
15 22 {% set currentRoute = app.request.attributes.get('_route') %}
16 {% set listMode = app.user.config.listMode %} 23 {% set listMode = app.user.config.listMode %}
17 <div class="results"> 24 <div class="results">
18 <div class="nb-results">{{ 'entry.list.number_on_the_page'|transchoice(entries.count) }}</div> 25 <div class="nb-results">{{ 'entry.list.number_on_the_page'|transchoice(entries.count) }}</div>
19 <div class="pagination"> 26 <div class="pagination">
20 <a href="{{ path('switch_view_mode') }}"><i class="listMode-btn material-icons md-36">{% if listMode == 0 %}list{% else %}view_module{% endif %}</i></a> 27 <a href="{{ path('switch_view_mode') }}"><i class="listMode-btn material-icons md-24">{% if listMode == 0 %}list{% else %}view_module{% endif %}</i></a>
21 <i class="btn-clickable download-btn material-icons md-36 js-export-action">file_download</i> 28 {% if app.user.config.rssToken %}
22 <i class="btn-clickable filter-btn material-icons md-36 js-filters-action">filter_list</i> 29 {% include "@WallabagCore/themes/common/Entry/_rss_link.html.twig" %}
30 {% endif %}
31 <i class="btn-clickable download-btn material-icons md-24 js-export-action">file_download</i>
32 <i class="btn-clickable filter-btn material-icons md-24 js-filters-action">filter_list</i>
23 {% if entries.getNbPages > 1 %} 33 {% if entries.getNbPages > 1 %}
24 {{ pagerfanta(entries, 'twitter_bootstrap_translated', {'proximity': 1}) }} 34 {{ pagerfanta(entries, 'twitter_bootstrap_translated', {'proximity': 1}) }}
25 {% endif %} 35 {% endif %}
@@ -47,10 +57,10 @@
47 </div> 57 </div>
48 58
49 <ul class="tools links"> 59 <ul class="tools links">
50 <li><a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool icon-check icon {% if entry.isArchived == 0 %}archive-off{% else %}archive{% endif %}" href="{{ path('archive_entry', { 'id': entry.id }) }}"><span>{{ 'entry.list.toogle_as_read'|trans }}</span></a></li> 60 <li><a href="{{ entry.url|e }}" target="_blank" title="{{ 'entry.list.original_article'|trans }} : {{ entry.title|e }}"><span>{{ entry.domainName|removeWww }}</span></a></li>
51 <li><a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool icon-star icon {% if entry.isStarred == 0 %}fav-off{% else %}fav{% endif %}" href="{{ path('star_entry', { 'id': entry.id }) }}"><span>{{ 'entry.list.toogle_as_star'|trans }}</span></a></li> 61 <li><a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool icon {% if entry.isArchived == 0 %}archive-off{% else %}archive{% endif %}" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons md-24 vertical-align-middle">check</i><span>{{ 'entry.list.toogle_as_read'|trans }}</span></a></li>
52 <li><a title="{{ 'entry.list.delete'|trans }}" class="tool delete icon-trash icon" href="{{ path('delete_entry', { 'id': entry.id }) }}"><span>{{ 'entry.list.delete'|trans }}</span></a></li> 62 <li><a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool icon {% if entry.isStarred == 0 %}fav-off{% else %}fav{% endif %}" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons md-24 vertical-align-middle">star_rate</i><span>{{ 'entry.list.toogle_as_star'|trans }}</span></a></li>
53 <li><a href="{{ entry.url|e }}" target="_blank" title="{{ 'entry.list.original_article'|trans }} : {{ entry.title|e }}" class="tool link icon-link icon"><span>{{ entry.domainName|removeWww }}</span></a></li> 63 <li><a title="{{ 'entry.list.delete'|trans }}" class="tool icon" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" href="{{ path('delete_entry', { 'id': entry.id }) }}"><i class="material-icons md-24 vertical-align-middle">delete</i><span>{{ 'entry.list.delete'|trans }}</span></a></li>
54 </ul> 64 </ul>
55 {% if (entry.previewPicture is null or listMode == 1) %} 65 {% if (entry.previewPicture is null or listMode == 1) %}
56 <ul class="card-entry-tags"> 66 <ul class="card-entry-tags">
@@ -76,8 +86,7 @@
76 86
77 <!-- Export --> 87 <!-- Export -->
78 <aside id="download-form"> 88 <aside id="download-form">
79 {% set currentRoute = app.request.attributes.get('_route') %} 89 {% set currentTag = null %}
80 {% set currentTag = '' %}
81 {% if tag is defined %} 90 {% if tag is defined %}
82 {% set currentTag = tag %} 91 {% set currentTag = tag %}
83 {% endif %} 92 {% endif %}
@@ -127,6 +136,11 @@
127 {{ form_widget(form.previewPicture) }} 136 {{ form_widget(form.previewPicture) }}
128 {{ form_label(form.previewPicture) }} 137 {{ form_label(form.previewPicture) }}
129 </div> 138 </div>
139
140 <div class="input-field">
141 {{ form_widget(form.isPublic) }}
142 {{ form_label(form.isPublic) }}
143 </div>
130 </div> 144 </div>
131 145
132 <div id="filter-language" class="filter-group"> 146 <div id="filter-language" class="filter-group">
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entry.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entry.html.twig
index d6b83fd2..f8723189 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entry.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entry.html.twig
@@ -22,7 +22,7 @@
22 <li><a title="{{ markAsReadLabel|trans }}" class="tool icon icon-check {% if entry.isArchived == 0 %}archive-off{% else %}archive{% endif %} markasread" href="{{ path('archive_entry', { 'id': entry.id }) }}"><span>{{ markAsReadLabel|trans }}</span></a></li> 22 <li><a title="{{ markAsReadLabel|trans }}" class="tool icon icon-check {% if entry.isArchived == 0 %}archive-off{% else %}archive{% endif %} markasread" href="{{ path('archive_entry', { 'id': entry.id }) }}"><span>{{ markAsReadLabel|trans }}</span></a></li>
23 <li><a title="{{ 'entry.view.left_menu.set_as_starred'|trans }}" class="tool icon icon-star {% if entry.isStarred == 0 %}fav-off{% else %}fav{% endif %} favorite" href="{{ path('star_entry', { 'id': entry.id }) }}"><span>{{ 'entry.view.left_menu.set_as_starred'|trans }}</span></a></li> 23 <li><a title="{{ 'entry.view.left_menu.set_as_starred'|trans }}" class="tool icon icon-star {% if entry.isStarred == 0 %}fav-off{% else %}fav{% endif %} favorite" href="{{ path('star_entry', { 'id': entry.id }) }}"><span>{{ 'entry.view.left_menu.set_as_starred'|trans }}</span></a></li>
24 <li><a id="nav-btn-add-tag" class="tool icon icon-price-tags" title="{{ 'entry.view.left_menu.add_a_tag'|trans }}"><span>{{ 'entry.view.left_menu.add_a_tag'|trans }}</span></a></li> 24 <li><a id="nav-btn-add-tag" class="tool icon icon-price-tags" title="{{ 'entry.view.left_menu.add_a_tag'|trans }}"><span>{{ 'entry.view.left_menu.add_a_tag'|trans }}</span></a></li>
25 <li><a title="{{ 'entry.view.left_menu.delete'|trans }}" class="tool delete icon icon-trash" href="{{ path('delete_entry', { 'id': entry.id }) }}"><span>{{ 'entry.view.left_menu.delete'|trans }}</span></a></li> 25 <li><a title="{{ 'entry.view.left_menu.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" class="tool delete icon icon-trash" href="{{ path('delete_entry', { 'id': entry.id }) }}"><span>{{ 'entry.view.left_menu.delete'|trans }}</span></a></li>
26 {% if craue_setting('share_public') %} 26 {% if craue_setting('share_public') %}
27 <li><a href="{{ path('share', {'id': entry.id }) }}" target="_blank" class="tool icon icon-eye" title="{{ 'entry.view.left_menu.public_link'|trans }}"><span>{{ 'entry.view.left_menu.public_link'|trans }}</span></a></li> 27 <li><a href="{{ path('share', {'id': entry.id }) }}" target="_blank" class="tool icon icon-eye" title="{{ 'entry.view.left_menu.public_link'|trans }}"><span>{{ 'entry.view.left_menu.public_link'|trans }}</span></a></li>
28 <li><a href="{{ path('delete_share', {'id': entry.id }) }}" class="tool icon icon-no-eye" title="{{ 'entry.view.left_menu.delete_public_link'|trans }}"><span>{{ 'entry.view.left_menu.delete_public_link'|trans }}</span></a></li> 28 <li><a href="{{ path('delete_share', {'id': entry.id }) }}" class="tool icon icon-no-eye" title="{{ 'entry.view.left_menu.delete_public_link'|trans }}"><span>{{ 'entry.view.left_menu.delete_public_link'|trans }}</span></a></li>
@@ -30,6 +30,7 @@
30 {% if craue_setting('share_twitter') %}<li><a href="https://twitter.com/home?status={{entry.title|url_encode}}%20{{ entry.url|url_encode }}%20via%20@wallabagapp" target="_blank" class="tool twitter icon icon-twitter" title="Tweet"><span>Tweet</span></a></li>{% endif %} 30 {% if craue_setting('share_twitter') %}<li><a href="https://twitter.com/home?status={{entry.title|url_encode}}%20{{ entry.url|url_encode }}%20via%20@wallabagapp" target="_blank" class="tool twitter icon icon-twitter" title="Tweet"><span>Tweet</span></a></li>{% endif %}
31 {% if craue_setting('share_mail') %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email icon icon-mail" title="Email"><span>Email</span></a></li>{% endif %} 31 {% if craue_setting('share_mail') %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email icon icon-mail" title="Email"><span>Email</span></a></li>{% endif %}
32 {% if craue_setting('share_shaarli') %}<li><a href="{{ craue_setting('shaarli_url') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}&amp;tags={{ entry.tags|join(',')|url_encode }}" target="_blank" class="tool icon-image icon-image--shaarli" title="shaarli"><span>shaarli</span></a></li>{% endif %} 32 {% if craue_setting('share_shaarli') %}<li><a href="{{ craue_setting('shaarli_url') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}&amp;tags={{ entry.tags|join(',')|url_encode }}" target="_blank" class="tool icon-image icon-image--shaarli" title="shaarli"><span>shaarli</span></a></li>{% endif %}
33 {% if craue_setting('share_scuttle') %}<li><a href="{{ craue_setting('scuttle_url') }}/bookmarks.php?action=add&amp;address={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}&amp;tags={{ entry.tags|join(',')|url_encode }}" target="_blank" class="tool icon-image icon-image--scuttle" title="scuttle"><span>scuttle</span></a></li>{% endif %}
33 {% if craue_setting('share_diaspora') %}<li><a href="{{ craue_setting('diaspora_url') }}/bookmarklet?url={{ entry.url|url_encode }}&title={{ entry.title|url_encode }}&notes=&v=1&noui=1&jump=doclose" target="_blank" class="tool diaspora icon-image icon-image--diaspora" title="diaspora"><span>diaspora</span></a></li>{% endif %} 34 {% if craue_setting('share_diaspora') %}<li><a href="{{ craue_setting('diaspora_url') }}/bookmarklet?url={{ entry.url|url_encode }}&title={{ entry.title|url_encode }}&notes=&v=1&noui=1&jump=doclose" target="_blank" class="tool diaspora icon-image icon-image--diaspora" title="diaspora"><span>diaspora</span></a></li>{% endif %}
34 {% if craue_setting('share_unmark') %}<li><a href="{{ craue_setting('unmark_url') }}/mark/add?url={{ entry.url|url_encode }}&amp;title={{entry.title|url_encode}}&amp;v=6" target="_blank" class="tool unmark icon-image icon-image--unmark" title="unmark"><span>unmark.it</span></a></li>{% endif %} 35 {% if craue_setting('share_unmark') %}<li><a href="{{ craue_setting('unmark_url') }}/mark/add?url={{ entry.url|url_encode }}&amp;title={{entry.title|url_encode}}&amp;v=6" target="_blank" class="tool unmark icon-image icon-image--unmark" title="unmark"><span>unmark.it</span></a></li>{% endif %}
35 {% if craue_setting('carrot') %}<li><a href="https://secure.carrot.org/GiveAndGetBack.do?url={{ entry.url|url_encode }}&title={{ entry.title|url_encode }}" class="tool carrot icon-image icon-image--carrot" target="_blank" title="carrot"><span>Carrot</span></a></li>{% endif %} 36 {% if craue_setting('carrot') %}<li><a href="https://secure.carrot.org/GiveAndGetBack.do?url={{ entry.url|url_encode }}&title={{ entry.title|url_encode }}" class="tool carrot icon-image icon-image--carrot" target="_blank" title="carrot"><span>Carrot</span></a></li>{% endif %}
@@ -43,9 +44,23 @@
43 44
44 <div id="article-informations"> 45 <div id="article-informations">
45 <i class="tool icon icon-calendar" title="{{ 'entry.view.created_at'|trans }}"> 46 <i class="tool icon icon-calendar" title="{{ 'entry.view.created_at'|trans }}">
46 {{ entry.createdAt|date('Y-m-d') }} 47 {{ entry.createdAt|date('Y-m-d H:i') }}
47 </i> 48 </i>
48 49
50 {% if entry.publishedAt is not null %}
51 <i class="tool icon icon-pencil2" title="{{ 'entry.view.published_at'|trans }}">
52 {{ entry.publishedAt|date('Y-m-d H:i') }}
53 </i>
54 {% endif %}
55
56 {% if entry.publishedBy is not empty %}
57 <i class="tool icon icon-users" title="{{ 'entry.view.published_by'|trans }}">
58 {% for author in entry.publishedBy %}
59 {{ author }}{% if not loop.last %}, {% endif %}
60 {% endfor %}
61 </i>
62 {% endif %}
63
49 <i class="tool icon icon-time"> 64 <i class="tool icon icon-time">
50 {% set readingTime = entry.readingTime / app.user.config.readingSpeed %} 65 {% set readingTime = entry.readingTime / app.user.config.readingSpeed %}
51 {% if readingTime > 0 %} 66 {% if readingTime > 0 %}
@@ -59,10 +74,16 @@
59 <aside class="tags"> 74 <aside class="tags">
60 <div class="card-entry-tags"> 75 <div class="card-entry-tags">
61 {% for tag in entry.tags %} 76 {% for tag in entry.tags %}
62 <span class="label-outline"><i class="material-icons">label_outline</i> <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a> <a href="{{ path('remove_tag', { 'entry': entry.id, 'tag': tag.id }) }}" class="nostyle"><i>✘</i></a></span> 77 <span class="label-outline">
78 <i class="material-icons">label_outline</i>
79 <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a>
80 <a href="{{ path('remove_tag', { 'entry': entry.id, 'tag': tag.id }) }}" onclick="return confirm('{{ 'entry.confirm.delete_tag'|trans|escape('js') }}')" class="nostyle">
81 <i>✘</i>
82 </a>
83 </span>
63 {% endfor %} 84 {% endfor %}
64 </div> 85 </div>
65 <div class="input-field nav-panel-add-tag" style="display: none"> 86 <div class="input-field baggy-add-tag" style="display: none">
66 {{ render(controller( "WallabagCoreBundle:Tag:addTagForm", { 'id': entry.id } )) }} 87 {{ render(controller( "WallabagCoreBundle:Tag:addTagForm", { 'id': entry.id } )) }}
67 </div> 88 </div>
68 </aside> 89 </aside>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/edit.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/edit.html.twig
new file mode 100644
index 00000000..882be430
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/edit.html.twig
@@ -0,0 +1,60 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{{ 'site_credential.page_title'|trans }}{% endblock %}
4
5{% block content %}
6
7 <div class="row">
8 <div class="col s12">
9 <div class="card-panel">
10 <div class="row">
11 <div class="input-field col s12">
12 <h4>{{ 'site_credential.edit_site_credential'|trans }}</h4>
13
14 <div id="set6" class="col s12">
15 {{ form_start(edit_form) }}
16 {{ form_errors(edit_form) }}
17
18 <div class="row">
19 <div class="input-field col s12">
20 {{ form_label(edit_form.host) }}
21 {{ form_errors(edit_form.host) }}
22 {{ form_widget(edit_form.host) }}
23 </div>
24 </div>
25
26 <div class="row">
27 <div class="input-field col s12">
28 {{ form_label(edit_form.username) }}
29 {{ form_errors(edit_form.username) }}
30 {{ form_widget(edit_form.username) }}
31 </div>
32 </div>
33
34 <div class="row">
35 <div class="input-field col s12">
36 {{ form_label(edit_form.password) }}
37 {{ form_errors(edit_form.password) }}
38 {{ form_widget(edit_form.password) }}
39 </div>
40 </div>
41
42 <br/>
43
44 {{ form_widget(edit_form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
45 {{ form_widget(edit_form._token) }}
46 </form>
47 <p>
48 {{ form_start(delete_form) }}
49 <button onclick="return confirm('{{ 'site_credential.form.delete_confirm'|trans|escape('js') }}')" type="submit" class="btn waves-effect waves-light red">{{ 'site_credential.form.delete'|trans }}</button>
50 {{ form_end(delete_form) }}
51 </p>
52 <p><a class="waves-effect waves-light btn blue-grey" href="{{ path('site_credentials_index') }}">{{ 'site_credential.form.back_to_list'|trans }}</a></p>
53 </div>
54 </div>
55 </div>
56 </div>
57 </div>
58 </div>
59
60{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/index.html.twig
new file mode 100644
index 00000000..324854ad
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/index.html.twig
@@ -0,0 +1,42 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{{ 'site_credential.page_title'|trans }}{% endblock %}
4
5{% block content %}
6
7 <div class="row">
8 <div class="col s12">
9 <div class="card-panel">
10 <div class="row">
11 <div class="input-field col s12">
12 <p class="help">{{ 'site_credential.description'|trans|raw }}</p>
13
14 <table class="bordered">
15 <thead>
16 <tr>
17 <th>{{ 'site_credential.form.host_label'|trans }}</th>
18 <th>{{ 'site_credential.list.actions'|trans }}</th>
19 </tr>
20 </thead>
21 <tbody>
22 {% for credential in credentials %}
23 <tr>
24 <td>{{ credential.host }}</td>
25 <td>
26 <a href="{{ path('site_credentials_edit', { 'id': credential.id }) }}">{{ 'site_credential.list.edit_action'|trans }}</a>
27 </td>
28 </tr>
29 {% endfor %}
30 </tbody>
31 </table>
32 <br />
33 <p>
34 <a href="{{ path('site_credentials_new') }}" class="waves-effect waves-light btn">{{ 'site_credential.list.create_new_one'|trans }}</a>
35 </p>
36 </div>
37 </div>
38 </div>
39 </div>
40 </div>
41
42{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/new.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/new.html.twig
new file mode 100644
index 00000000..3c008cde
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/SiteCredential/new.html.twig
@@ -0,0 +1,53 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{{ 'site_credential.page_title'|trans }}{% endblock %}
4
5{% block content %}
6
7 <div class="row">
8 <div class="col s12">
9 <div class="card-panel">
10 <div class="row">
11 <div class="input-field col s12">
12 <h4>{{ 'site_credential.new_site_credential'|trans }}</h4>
13
14 <div id="set6" class="col s12">
15 {{ form_start(form) }}
16 {{ form_errors(form) }}
17
18 <div class="row">
19 <div class="input-field col s12">
20 {{ form_label(form.host) }}
21 {{ form_errors(form.host) }}
22 {{ form_widget(form.host) }}
23 </div>
24 </div>
25
26 <div class="row">
27 <div class="input-field col s12">
28 {{ form_label(form.username) }}
29 {{ form_errors(form.username) }}
30 {{ form_widget(form.username) }}
31 </div>
32 </div>
33
34 <div class="row">
35 <div class="input-field col s12">
36 {{ form_label(form.password) }}
37 {{ form_errors(form.password) }}
38 {{ form_widget(form.password) }}
39 </div>
40 </div>
41
42 {{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
43 {{ form_rest(form) }}
44 </form>
45 <p><a class="waves-effect waves-light btn blue-grey" href="{{ path('site_credentials_index') }}">{{ 'site_credential.form.back_to_list'|trans }}</a></p>
46 </div>
47 </div>
48 </div>
49 </div>
50 </div>
51 </div>
52
53{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Tag/tags.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Tag/tags.html.twig
index 1e2c6b42..070d5629 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Tag/tags.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Tag/tags.html.twig
@@ -9,7 +9,12 @@
9 9
10 <ul> 10 <ul>
11 {% for tag in tags %} 11 {% for tag in tags %}
12 <li id="tag-{{ tag.id|e }}"><a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{tag.label}} ({{ tag.nbEntries | length }})</a></li> 12 <li id="tag-{{ tag.id|e }}">
13 <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{tag.label}} ({{ tag.nbEntries }})</a>
14 <a rel="alternate" type="application/rss+xml" href="{{ path('tag_rss', {'username': app.user.username, 'token': app.user.config.rssToken, 'slug': tag.slug}) }}" class="right">
15 <i class="material-icons md-24">rss_feed</i>
16 </a>
17 </li>
13 {% endfor %} 18 {% endfor %}
14 </ul> 19 </ul>
15 20
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/layout.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/layout.html.twig
index 07ff8e14..17fa13bb 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/layout.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/layout.html.twig
@@ -2,12 +2,14 @@
2 2
3{% block css %} 3{% block css %}
4 {{ parent() }} 4 {{ parent() }}
5 <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/themes/baggy/css/style.min.css') }}" media="screen,projection,print"/> 5 {% if not app.debug %}
6 <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/baggy.css') }}">
7 {% endif %}
6{% endblock %} 8{% endblock %}
7 9
8{% block scripts %} 10{% block scripts %}
9 {{ parent() }} 11 {{ parent() }}
10 <script src="{{ asset('bundles/wallabagcore/themes/baggy/js/baggy.min.js') }}"></script> 12 <script src="{{ asset('bundles/wallabagcore/baggy' ~ (app.debug ? '.dev' : '') ~ '.js') }}"></script>
11{% endblock %} 13{% endblock %}
12 14
13{% block header %} 15{% block header %}
@@ -36,6 +38,9 @@
36 {{ render(controller("WallabagCoreBundle:Entry:searchForm", {'currentRoute': app.request.attributes.get('_route')})) }} 38 {{ render(controller("WallabagCoreBundle:Entry:searchForm", {'currentRoute': app.request.attributes.get('_route')})) }}
37 </div> 39 </div>
38 </li> 40 </li>
41 {% if craue_setting('restricted_access') %}
42 <li class="menu site_credentials"><a href="{{ path('site_credentials_index') }}">{{ 'menu.left.site_credentials'|trans }}</a></li>
43 {% endif %}
39 <li class="menu config"><a href="{{ path('config') }}">{{ 'menu.left.config'|trans }}</a></li> 44 <li class="menu config"><a href="{{ path('config') }}">{{ 'menu.left.config'|trans }}</a></li>
40 {% if is_granted('ROLE_SUPER_ADMIN') %} 45 {% if is_granted('ROLE_SUPER_ADMIN') %}
41 <li class="menu users"><a href="{{ path('user_index') }}">{{ 'menu.left.users_management'|trans }}</a></li> 46 <li class="menu users"><a href="{{ path('user_index') }}">{{ 'menu.left.users_management'|trans }}</a></li>
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/CoreBundle/Resources/views/themes/common/Entry/_rss_link.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_rss_link.html.twig
new file mode 100644
index 00000000..2bf9b2bd
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_rss_link.html.twig
@@ -0,0 +1,6 @@
1{% if tag is defined %}
2 <a rel="alternate" type="application/rss+xml" href="{{ path('tag_rss', {'username': app.user.username, 'token': app.user.config.rssToken, 'slug': tag.slug}) }}" class="right"><i class="material-icons md-24">rss_feed</i></a>
3{% elseif currentRoute in ['unread', 'starred', 'archive', 'all'] %}
4 <a rel="alternate" type="application/rss+xml" href="{{ path(currentRoute ~ '_rss', {'username': app.user.username, 'token': app.user.config.rssToken}) }}" class="right"><i class="material-icons">rss_feed</i></a>
5{% endif %}
6
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig
index 654c1d2d..5c17e9f7 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig
@@ -5,7 +5,7 @@
5{% elseif currentRoute == 'archive' %} 5{% elseif currentRoute == 'archive' %}
6 {{ 'entry.page_titles.archived'|trans }} 6 {{ 'entry.page_titles.archived'|trans }}
7{% elseif currentRoute == 'all' %} 7{% elseif currentRoute == 'all' %}
8 {{ 'entry.page_titles.filtered'|trans }} 8 {{ isFiltered ? 'entry.page_titles.filtered'|trans : 'entry.page_titles.all'|trans }}
9{% elseif currentRoute == 'search' %} 9{% elseif currentRoute == 'search' %}
10 {{ 'entry.page_titles.filtered_search'|trans }} {{ filter }} 10 {{ 'entry.page_titles.filtered_search'|trans }} {{ filter }}
11{% elseif currentRoute == 'tag_entries' %} 11{% elseif currentRoute == 'tag_entries' %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/entries.xml.twig b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/entries.xml.twig
index 12e8c79f..d70aa5dc 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/entries.xml.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/entries.xml.twig
@@ -1,8 +1,8 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"> 2<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
3 <channel> 3 <channel>
4 <title>wallabag {{type}} feed</title> 4 <title>wallabag - {{ type }} feed</title>
5 <link>{{ url(type) }}</link> 5 <link>{{ url_html }}</link>
6 <link rel="self" href="{{ app.request.uri }}"/> 6 <link rel="self" href="{{ app.request.uri }}"/>
7 {% if entries.hasPreviousPage -%} 7 {% if entries.hasPreviousPage -%}
8 <link rel="previous" href="{{ url }}?page={{ entries.previousPage }}"/> 8 <link rel="previous" href="{{ url }}?page={{ entries.previousPage }}"/>
@@ -13,7 +13,7 @@
13 <link rel="last" href="{{ url }}?page={{ entries.nbPages }}"/> 13 <link rel="last" href="{{ url }}?page={{ entries.nbPages }}"/>
14 <pubDate>{{ "now"|date('D, d M Y H:i:s') }}</pubDate> 14 <pubDate>{{ "now"|date('D, d M Y H:i:s') }}</pubDate>
15 <generator>wallabag</generator> 15 <generator>wallabag</generator>
16 <description>wallabag {{type}} elements</description> 16 <description>wallabag {{ type }} elements</description>
17 17
18 {% for entry in entries %} 18 {% for entry in entries %}
19 19
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/share.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/share.html.twig
index 623cf1c4..a67807f9 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/share.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/share.html.twig
@@ -1,32 +1,6 @@
1<html> 1<html>
2 <head> 2 <head>
3 <title>{{ entry.title|e|raw }}</title> 3 <title>{{ entry.title|e|raw }}</title>
4 <style>
5 body {
6 margin: 10px;
7 font-family: 'Roboto',Verdana,Geneva,sans-serif;
8 font-size: 16px;
9 color: #000;
10 }
11 header {
12 text-align: center;
13 }
14
15 header h1 {
16 font-size: 1.3em;
17 }
18
19 a,
20 a:hover,
21 a:visited {
22 color: #000;
23 }
24
25 article {
26 margin: 0 auto;
27 width: 600px;
28 }
29 </style>
30 <meta property="og:title" content="{{ entry.title|e|raw }}" /> 4 <meta property="og:title" content="{{ entry.title|e|raw }}" />
31 <meta property="og:type" content="article" /> 5 <meta property="og:type" content="article" />
32 <meta property="og:url" content="{{ app.request.uri }}" /> 6 <meta property="og:url" content="{{ app.request.uri }}" />
@@ -40,12 +14,22 @@
40 <meta name="twitter:site" content="@wallabagapp" /> 14 <meta name="twitter:site" content="@wallabagapp" />
41 <meta name="twitter:title" content="{{ entry.title|e|raw }}" /> 15 <meta name="twitter:title" content="{{ entry.title|e|raw }}" />
42 <meta name="twitter:description" content="{{ entry.content|striptags|slice(0, 300)|raw }}&hellip;" /> 16 <meta name="twitter:description" content="{{ entry.content|striptags|slice(0, 300)|raw }}&hellip;" />
17 {% if app.debug %}
18 <script src="{{ asset('bundles/wallabagcore/public.dev.js') }}"></script>
19 {% else %}
20 <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/public.css') }}">
21 {% endif %}
22
43 </head> 23 </head>
44 <body> 24 <body>
45 <header> 25 <header>
46 <h1>{{ entry.title|e|raw }}</h1> 26 <h1>{{ entry.title|e|raw }}</h1>
47 <div><a href="{{ entry.url|e }}" target="_blank" title="{{ 'entry.view.original_article'|trans }} : {{ entry.title|e|raw }}" class="tool">{{ entry.domainName|removeWww }}</a></div> 27 <div><a href="{{ entry.url|e }}" target="_blank" title="{{ 'entry.view.original_article'|trans }} : {{ entry.title|e|raw }}" class="tool">{{ entry.domainName|removeWww }}</a></div>
48 <div>{{ "entry.public.shared_by_wallabag"|trans({'%wallabag_instance%': url('homepage')})|raw }}</div> 28 <div>{{ "entry.public.shared_by_wallabag"|trans({'%wallabag_instance%': url('homepage'), '%username%': entry.user.username})|raw }}.</div>
29
30 {% if entry.previewPicture is not null %}
31 <div><img class="preview" src="{{ entry.previewPicture }}" alt="{{ entry.title|striptags|e('html_attr') }}" /></div>
32 {% endif %}
49 </header> 33 </header>
50 <article> 34 <article>
51 {{ entry.content | raw }} 35 {{ entry.content | raw }}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
index 5d411fdd..a8143315 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
@@ -25,9 +25,9 @@
25 25
26 <div class="row"> 26 <div class="row">
27 <div class="input-field col s11"> 27 <div class="input-field col s11">
28 {{ form_label(form.config.theme) }}
29 {{ form_errors(form.config.theme) }} 28 {{ form_errors(form.config.theme) }}
30 {{ form_widget(form.config.theme) }} 29 {{ form_widget(form.config.theme) }}
30 {{ form_label(form.config.theme) }}
31 </div> 31 </div>
32 <div class="input-field col s1"> 32 <div class="input-field col s1">
33 <a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_theme'|trans }}"> 33 <a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_theme'|trans }}">
@@ -38,9 +38,9 @@
38 38
39 <div class="row"> 39 <div class="row">
40 <div class="input-field col s11"> 40 <div class="input-field col s11">
41 {{ form_label(form.config.items_per_page) }}
42 {{ form_errors(form.config.items_per_page) }} 41 {{ form_errors(form.config.items_per_page) }}
43 {{ form_widget(form.config.items_per_page) }} 42 {{ form_widget(form.config.items_per_page) }}
43 {{ form_label(form.config.items_per_page) }}
44 </div> 44 </div>
45 <div class="input-field col s1"> 45 <div class="input-field col s1">
46 <a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_items_per_page'|trans }}"> 46 <a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_items_per_page'|trans }}">
@@ -51,9 +51,9 @@
51 51
52 <div class="row"> 52 <div class="row">
53 <div class="input-field col s11"> 53 <div class="input-field col s11">
54 {{ form_label(form.config.reading_speed) }}
55 {{ form_errors(form.config.reading_speed) }} 54 {{ form_errors(form.config.reading_speed) }}
56 {{ form_widget(form.config.reading_speed) }} 55 {{ form_widget(form.config.reading_speed) }}
56 {{ form_label(form.config.reading_speed) }}
57 <p> 57 <p>
58 {{ 'config.form_settings.reading_speed.help_message'|trans }} 58 {{ 'config.form_settings.reading_speed.help_message'|trans }}
59 <a href="http://www.myreadspeed.com/calculate/">myreadspeed</a> 59 <a href="http://www.myreadspeed.com/calculate/">myreadspeed</a>
@@ -66,19 +66,19 @@
66 </div> 66 </div>
67 </div> 67 </div>
68 68
69 <div class="row"> 69 <div class="row">
70 <div class="input-field col s12"> 70 <div class="input-field col s12">
71 {{ form_label(form.config.action_mark_as_read) }} 71 {{ form_label(form.config.action_mark_as_read) }}
72 {{ form_errors(form.config.action_mark_as_read) }} 72 {{ form_errors(form.config.action_mark_as_read) }}
73 {{ form_widget(form.config.action_mark_as_read) }} 73 {{ form_widget(form.config.action_mark_as_read) }}
74 </div>
74 </div> 75 </div>
75 </div>
76 76
77 <div class="row"> 77 <div class="row">
78 <div class="input-field col s11"> 78 <div class="input-field col s11">
79 {{ form_label(form.config.language) }}
80 {{ form_errors(form.config.language) }} 79 {{ form_errors(form.config.language) }}
81 {{ form_widget(form.config.language) }} 80 {{ form_widget(form.config.language) }}
81 {{ form_label(form.config.language) }}
82 </div> 82 </div>
83 <div class="input-field col s1"> 83 <div class="input-field col s1">
84 <a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_language'|trans }}"> 84 <a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_language'|trans }}">
@@ -89,9 +89,9 @@
89 89
90 <div class="row"> 90 <div class="row">
91 <div class="input-field col s11"> 91 <div class="input-field col s11">
92 {{ form_label(form.config.pocket_consumer_key) }}
93 {{ form_errors(form.config.pocket_consumer_key) }} 92 {{ form_errors(form.config.pocket_consumer_key) }}
94 {{ form_widget(form.config.pocket_consumer_key) }} 93 {{ form_widget(form.config.pocket_consumer_key) }}
94 {{ form_label(form.config.pocket_consumer_key) }}
95 <p> 95 <p>
96 &raquo; 96 &raquo;
97 <a href="https://getpocket.com/developer/docs/authentication">https://getpocket.com/developer/docs/authentication</a> 97 <a href="https://getpocket.com/developer/docs/authentication">https://getpocket.com/developer/docs/authentication</a>
@@ -107,7 +107,7 @@
107 <div class="row"> 107 <div class="row">
108 <div class="input-field col s12"> 108 <div class="input-field col s12">
109 <h5>{{ 'config.form_settings.android_configuration'|trans }}</h5> 109 <h5>{{ 'config.form_settings.android_configuration'|trans }}</h5>
110 <a href="wallabag://{{ app.user.username }}@{{ wallabag_url }}" class="waves-effect waves-light btn hide-on-large-only">Touch here to prefill your Android application</a> 110 <a href="wallabag://{{ app.user.username }}@{{ wallabag_url }}" class="waves-effect waves-light btn hide-on-large-only">{{ 'config.form_settings.android_instruction' | trans }}</a>
111 <img id="androidQrcode" class="hide-on-med-and-down" /> 111 <img id="androidQrcode" class="hide-on-med-and-down" />
112 </div> 112 </div>
113 <script> 113 <script>
@@ -132,8 +132,8 @@
132 </div> 132 </div>
133 133
134 <div class="row"> 134 <div class="row">
135 <div class="input-field col s12"> 135 <div class="col s12">
136 <label>{{ 'config.form_rss.token_label'|trans }}</label> 136 <h6 class="grey-text">{{ 'config.form_rss.token_label'|trans }}</h6>
137 <div> 137 <div>
138 {% if rss.token %} 138 {% if rss.token %}
139 {{ rss.token }} 139 {{ rss.token }}
@@ -151,12 +151,13 @@
151 </div> 151 </div>
152 {% if rss.token %} 152 {% if rss.token %}
153 <div class="row"> 153 <div class="row">
154 <div class="input-field col s12"> 154 <div class="col s12">
155 <label>{{ 'config.form_rss.rss_links'|trans }}</label> 155 <h6 class="grey-text">{{ 'config.form_rss.rss_links'|trans }}</h6>
156 <ul> 156 <ul>
157 <li><a href="{{ path('unread_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.unread'|trans }}</a></li> 157 <li><a href="{{ path('unread_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.unread'|trans }}</a></li>
158 <li><a href="{{ path('starred_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.starred'|trans }}</a></li> 158 <li><a href="{{ path('starred_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.starred'|trans }}</a></li>
159 <li><a href="{{ path('archive_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.archive'|trans }}</a></li> 159 <li><a href="{{ path('archive_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.archive'|trans }}</a></li>
160 <li><a href="{{ path('all_rss', {'username': rss.username, 'token': rss.token}) }}">{{ 'config.form_rss.rss_link.all'|trans }}</a></li>
160 </ul> 161 </ul>
161 </div> 162 </div>
162 </div> 163 </div>
@@ -229,6 +230,9 @@
229 <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red"> 230 <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
230 {{ 'config.reset.tags'|trans }} 231 {{ 'config.reset.tags'|trans }}
231 </a> 232 </a>
233 <a href="{{ path('config_reset', { type: 'archived'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
234 {{ 'config.reset.archived'|trans }}
235 </a>
232 <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red"> 236 <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
233 {{ 'config.reset.entries'|trans }} 237 {{ 'config.reset.entries'|trans }}
234 </a> 238 </a>
@@ -251,11 +255,11 @@
251 {{ form_start(form.pwd) }} 255 {{ form_start(form.pwd) }}
252 {{ form_errors(form.pwd) }} 256 {{ form_errors(form.pwd) }}
253 257
254 <div class="row"> 258 <div class="row">
255 <div class="input-field col s12"> 259 <div class="input-field col s12">
256 {{ 'config.form_password.description'|trans }} 260 {{ 'config.form_password.description'|trans }}
261 </div>
257 </div> 262 </div>
258 </div>
259 263
260 <div class="row"> 264 <div class="row">
261 <div class="input-field col s12"> 265 <div class="input-field col s12">
@@ -410,8 +414,8 @@
410 <tr> 414 <tr>
411 <td>domainName</td> 415 <td>domainName</td>
412 <td>{{ 'config.form_rules.faq.variable_description.domainName'|trans }}</td> 416 <td>{{ 'config.form_rules.faq.variable_description.domainName'|trans }}</td>
413 <td>matches</td> 417 <td>matches<br />notmaches</td>
414 <td>{{ 'config.form_rules.faq.operator_description.matches'|trans|raw }}</td> 418 <td>{{ 'config.form_rules.faq.operator_description.matches'|trans|raw }}<br />{{ 'config.form_rules.faq.operator_description.notmatches'|trans|raw }}</td>
415 </tr> 419 </tr>
416 </tbody> 420 </tbody>
417 </table> 421 </table>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig
index d278da1b..468338ac 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig
@@ -9,7 +9,7 @@
9 <li> 9 <li>
10 <a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}redo{% endif %}</i></a> 10 <a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}redo{% endif %}</i></a>
11 <a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool grey-text" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i></a> 11 <a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool grey-text" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i></a>
12 <a title="{{ 'entry.list.delete'|trans }}" class="tool grey-text delete" href="{{ path('delete_entry', { 'id': entry.id }) }}"><i class="material-icons">delete</i></a> 12 <a title="{{ 'entry.list.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" class="tool grey-text delete" href="{{ path('delete_entry', { 'id': entry.id }) }}"><i class="material-icons">delete</i></a>
13 </li> 13 </li>
14 </ul> 14 </ul>
15</div> 15</div>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig
index 3ba6253a..b64e1436 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig
@@ -1,18 +1,28 @@
1<div class="card"> 1<div class="card-stacked">
2 <div class="card-stacked"> 2 <div class="preview">{% if entry.previewPicture is not null %}<img src="{{ entry.previewPicture }}" />{% endif %}</div>
3 <div class="card-content"> 3 <div class="card-content">
4 <span class="card-title dot-ellipsis dot-resize-update"> 4 <span class="card-title dot-ellipsis dot-resize-update">
5 <a href="{{ path('view', { 'id': entry.id }) }}" title="{{ entry.title | striptags | e('html_attr') }}"> 5 <a href="{{ path('view', { 'id': entry.id }) }}" title="{{ entry.title | striptags | e('html_attr') }}">
6 {{ entry.title| striptags | truncate(120, true, '…') | raw }} 6 {{ entry.title| striptags | truncate(120, true, '…') | raw }}
7 </a> 7 </a>
8 </span> 8 </span>
9 <ul class="tools-list right"> 9
10 <li> 10 <div class="metadata">
11 <a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}redo{% endif %}</i></a> 11 <a href="{{ entry.url|e }}" class="grey-text domain" target="_blank" title="{{ entry.domainName|removeWww }}">
12 <a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool grey-text" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i></a> 12 <span>{{ entry.domainName|removeWww }}</span>
13 <a title="{{ 'entry.list.delete'|trans }}" class="tool grey-text delete" href="{{ path('delete_entry', { 'id': entry.id }) }}"><i class="material-icons">delete</i></a> 13 </a>
14 </li> 14 {% for tag in entry.tags | slice(0, 3) %}
15 </ul> 15 <span class="chip hide-on-med-and-down">
16 <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a>
17 </span>
18 {% endfor %}
16 </div> 19 </div>
17 </div> 20 </div>
21 <ul class="tools-list hide-on-small-only">
22 <li>
23 <a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}redo{% endif %}</i></a>
24 <a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool grey-text" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i></a>
25 <a title="{{ 'entry.list.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" class="tool grey-text delete" href="{{ path('delete_entry', { 'id': entry.id }) }}"><i class="material-icons">delete</i></a>
26 </li>
27 </ul>
18</div> 28</div>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig
index 1a932a9f..6ba18768 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig
@@ -1,7 +1,7 @@
1{% set readingTime = entry.readingTime / app.user.config.readingSpeed %} 1{% set readingTime = entry.readingTime / app.user.config.readingSpeed %}
2<i class="material-icons">timer</i> 2<i class="material-icons">timer</i>
3{% if readingTime > 0 %} 3{% if readingTime > 0 %}
4 {{ 'entry.list.reading_time_minutes_short'|trans({'%readingTime%': readingTime|round}) }} 4 <span>{{ 'entry.list.reading_time_minutes_short'|trans({'%readingTime%': readingTime|round}) }}</span>
5{% else %} 5{% else %}
6 {{ 'entry.list.reading_time_less_one_minute_short'|trans|raw }} 6 <span>{{ 'entry.list.reading_time_less_one_minute_short'|trans|raw }}</span>
7{% endif %} 7{% endif %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/edit.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/edit.html.twig
index 1c5e2aab..b9537975 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/edit.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/edit.html.twig
@@ -27,11 +27,6 @@
27 {{ form_label(form.url) }} 27 {{ form_label(form.url) }}
28 {{ form_widget(form.url) }} 28 {{ form_widget(form.url) }}
29 </div> 29 </div>
30
31 <div class="input-field s12">
32 {{ form_widget(form.is_public) }}
33 {{ form_label(form.is_public) }}
34 </div>
35 <br> 30 <br>
36 31
37 {{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} 32 {{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig
index 5fca53ae..9d6fb3f5 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig
@@ -1,9 +1,16 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %} 1{% extends "WallabagCoreBundle::layout.html.twig" %}
2 2
3{% block head %}
4 {{ parent() }}
5 {% if tag is defined and app.user.config.rssToken %}
6 <link rel="alternate" type="application/rss+xml" href="{{ path('tag_rss', {'username': app.user.username, 'token': app.user.config.rssToken, 'slug': tag.slug}) }}" />
7 {% endif %}
8{% endblock %}
9
3{% block title %} 10{% block title %}
4 {% set filter = '' %} 11 {% set filter = '' %}
5 {% if tag is defined %} 12 {% if tag is defined %}
6 {% set filter = tag %} 13 {% set filter = tag.slug %}
7 {% endif %} 14 {% endif %}
8 {% if searchTerm is defined and searchTerm is not empty %} 15 {% if searchTerm is defined and searchTerm is not empty %}
9 {% set filter = searchTerm %} 16 {% set filter = searchTerm %}
@@ -13,10 +20,14 @@
13 20
14{% block content %} 21{% block content %}
15 {% set listMode = app.user.config.listMode %} 22 {% set listMode = app.user.config.listMode %}
23 {% set currentRoute = app.request.attributes.get('_route') %}
16 <div class="results clearfix"> 24 <div class="results clearfix">
17 <div class="nb-results left"> 25 <div class="nb-results left">
18 {{ 'entry.list.number_on_the_page'|transchoice(entries.count) }} 26 {{ 'entry.list.number_on_the_page'|transchoice(entries.count) }}
19 <a href="{{ path('switch_view_mode') }}"><i class="material-icons">{% if listMode == 0 %}view_list{% else %}view_module{% endif %}</i></a> 27 <a href="{{ path('switch_view_mode') }}"><i class="material-icons">{% if listMode == 0 %}view_list{% else %}view_module{% endif %}</i></a>
28 {% if app.user.config.rssToken %}
29 {% include "@WallabagCore/themes/common/Entry/_rss_link.html.twig" %}
30 {% endif %}
20 </div> 31 </div>
21 {% if entries.getNbPages > 1 %} 32 {% if entries.getNbPages > 1 %}
22 {{ pagerfanta(entries, 'twitter_bootstrap_translated', {'proximity': 1}) }} 33 {{ pagerfanta(entries, 'twitter_bootstrap_translated', {'proximity': 1}) }}
@@ -24,9 +35,9 @@
24 </div> 35 </div>
25 36
26 <br /> 37 <br />
27 <ul class="row data"> 38 <ul class="{% if listMode == 1 %}collection{% else %}row data{% endif %}">
28 {% for entry in entries %} 39 {% for entry in entries %}
29 <li id="entry-{{ entry.id|e }}" class="col {% if listMode == 0 %}l3 m6{% endif %} s12"> 40 <li id="entry-{{ entry.id|e }}" class="col {% if listMode == 0 %}l3 m6{% else %}collection-item{% endif %} s12">
30 {% if listMode == 1 %} 41 {% if listMode == 1 %}
31 {% include "@WallabagCore/themes/material/Entry/_card_list.html.twig" with {'entry': entry} only %} 42 {% include "@WallabagCore/themes/material/Entry/_card_list.html.twig" with {'entry': entry} only %}
32 {% elseif entry.previewPicture is null %} 43 {% elseif entry.previewPicture is null %}
@@ -45,11 +56,10 @@
45 {% endif %} 56 {% endif %}
46 57
47 <!-- Export --> 58 <!-- Export -->
48 <div id="export" class="side-nav fixed right-aligned"> 59 <div id="export" class="side-nav right-aligned">
49 {% set currentRoute = app.request.attributes.get('_route') %} 60 {% set currentTag = null %}
50 {% set currentTag = '' %}
51 {% if tag is defined %} 61 {% if tag is defined %}
52 {% set currentTag = tag %} 62 {% set currentTag = tag.slug %}
53 {% endif %} 63 {% endif %}
54 {% if currentRoute == 'homepage' %} 64 {% if currentRoute == 'homepage' %}
55 {% set currentRoute = 'unread' %} 65 {% set currentRoute = 'unread' %}
@@ -68,7 +78,7 @@
68 78
69 <!-- Filters --> 79 <!-- Filters -->
70 {% if form is not null %} 80 {% if form is not null %}
71 <div id="filters" class="side-nav fixed right-aligned"> 81 <div id="filters" class="side-nav right-aligned">
72 <form action="{{ path('all') }}"> 82 <form action="{{ path('all') }}">
73 83
74 <h4 class="center">{{ 'entry.filters.title'|trans }}</h4> 84 <h4 class="center">{{ 'entry.filters.title'|trans }}</h4>
@@ -103,6 +113,15 @@
103 </div> 113 </div>
104 114
105 <div class="col s12"> 115 <div class="col s12">
116 <label>{{ 'entry.filters.is_public_help'|trans }}</label>
117 </div>
118
119 <div class="input-field col s12 with-checkbox">
120 {{ form_widget(form.isPublic) }}
121 {{ form_label(form.isPublic) }}
122 </div>
123
124 <div class="col s12">
106 {{ form_label(form.language) }} 125 {{ form_label(form.language) }}
107 </div> 126 </div>
108 127
@@ -121,10 +140,12 @@
121 <div class="col s12"> 140 <div class="col s12">
122 {{ form_label(form.readingTime) }} 141 {{ form_label(form.readingTime) }}
123 </div> 142 </div>
143
124 <div class="input-field col s6"> 144 <div class="input-field col s6">
125 {{ form_widget(form.readingTime.left_number, {'type': 'number'}) }} 145 {{ form_widget(form.readingTime.left_number, {'type': 'number'}) }}
126 <label for="entry_filter_readingTime_left_number">{{ 'entry.filters.reading_time.from'|trans }}</label> 146 <label for="entry_filter_readingTime_left_number">{{ 'entry.filters.reading_time.from'|trans }}</label>
127 </div> 147 </div>
148
128 <div class="input-field col s6"> 149 <div class="input-field col s6">
129 {{ form_widget(form.readingTime.right_number, {'type': 'number'}) }} 150 {{ form_widget(form.readingTime.right_number, {'type': 'number'}) }}
130 <label for="entry_filter_readingTime_right_number">{{ 'entry.filters.reading_time.to'|trans }}</label> 151 <label for="entry_filter_readingTime_right_number">{{ 'entry.filters.reading_time.to'|trans }}</label>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig
index af53084f..4cff7bf2 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig
@@ -12,6 +12,11 @@
12 <div class="nav-wrapper cyan darken-1"> 12 <div class="nav-wrapper cyan darken-1">
13 <ul> 13 <ul>
14 <li> 14 <li>
15 <a href="#" data-activates="slide-out" class="button-collapse">
16 <i class="material-icons">menu</i>
17 </a>
18 </li>
19 <li>
15 <a class="waves-effect" href="{{ path('homepage') }}"> 20 <a class="waves-effect" href="{{ path('homepage') }}">
16 <i class="material-icons">exit_to_app</i> 21 <i class="material-icons">exit_to_app</i>
17 </a> 22 </a>
@@ -28,11 +33,6 @@
28 <i class="material-icons small">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i> 33 <i class="material-icons small">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i>
29 </a> 34 </a>
30 </li> 35 </li>
31 <li>
32 <a href="#" data-activates="slide-out" class="button-collapse right">
33 <i class="material-icons">menu</i>
34 </a>
35 </li>
36 </ul> 36 </ul>
37 </div> 37 </div>
38 </nav> 38 </nav>
@@ -82,7 +82,7 @@
82 <div class="collapsible-body"></div> 82 <div class="collapsible-body"></div>
83 </li> 83 </li>
84 <li class="bold border-bottom"> 84 <li class="bold border-bottom">
85 <a class="waves-effect collapsible-header delete" title="{{ 'entry.view.left_menu.delete'|trans }}" href="{{ path('delete_entry', { 'id': entry.id }) }}"> 85 <a class="waves-effect collapsible-header delete" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" title="{{ 'entry.view.left_menu.delete'|trans }}" href="{{ path('delete_entry', { 'id': entry.id }) }}">
86 <i class="material-icons small">delete</i> 86 <i class="material-icons small">delete</i>
87 <span>{{ 'entry.view.left_menu.delete'|trans }}</span> 87 <span>{{ 'entry.view.left_menu.delete'|trans }}</span>
88 </a> 88 </a>
@@ -125,39 +125,43 @@
125 {% endif %} 125 {% endif %}
126 {% if craue_setting('share_shaarli') %} 126 {% if craue_setting('share_shaarli') %}
127 <li> 127 <li>
128 <a href="{{ craue_setting('shaarli_url') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|striptags|url_encode }}&amp;tags={{ entry.tags|join(',')|striptags|url_encode }}" target="_blank"> 128 <a href="{{ craue_setting('shaarli_url') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|striptags|url_encode }}&amp;tags={{ entry.tags|join(',')|striptags|url_encode }}" target="_blank" title="shaarli" class="tool icon-image shaarli">
129 <i class="tool icon-image icon-image--shaarli" title="shaarli"></i>
130 <span>shaarli</span> 129 <span>shaarli</span>
131 </a> 130 </a>
132 </li> 131 </li>
133 {% endif %} 132 {% endif %}
133 {% if craue_setting('share_scuttle') %}
134 <li>
135 <a href="{{ craue_setting('scuttle_url') }}/bookmarks.php?action=add&amp;address={{ entry.url|url_encode }}&amp;title={{ entry.title|striptags|url_encode }}&amp;tags={{ entry.tags|join(',')|striptags|url_encode }}" target="_blank" title="scuttle" class="tool icon-image scuttle">
136 <span>scuttle</span>
137 </a>
138 </li>
139 {% endif %}
134 {% if craue_setting('share_diaspora') %} 140 {% if craue_setting('share_diaspora') %}
135 <li> 141 <li>
136 <a href="{{ craue_setting('diaspora_url') }}/bookmarklet?url={{ entry.url|url_encode }}&amp;title={{ entry.title|striptags|url_encode }}&amp;notes=&amp;v=1&amp;noui=1&amp;jump=doclose" target="_blank"> 142 <a href="{{ craue_setting('diaspora_url') }}/bookmarklet?url={{ entry.url|url_encode }}&amp;title={{ entry.title|striptags|url_encode }}&amp;notes=&amp;v=1&amp;noui=1&amp;jump=doclose" target="_blank" class="tool icon-image diaspora" title="diaspora">
137 <i class="tool icon-image icon-image--diaspora" title="diaspora"></i>
138 <span>diaspora*</span> 143 <span>diaspora*</span>
139 </a> 144 </a>
140 </li> 145 </li>
141 {% endif %} 146 {% endif %}
142 {% if craue_setting('share_unmark') %} 147 {% if craue_setting('share_unmark') %}
143 <li> 148 <li>
144 <a href="{{ craue_setting('unmark_url') }}/mark/add?url={{ entry.url|url_encode }}&amp;title={{entry.title|striptags|url_encode}}&amp;v=6" target="_blank"> 149 <a href="{{ craue_setting('unmark_url') }}/mark/add?url={{ entry.url|url_encode }}&amp;title={{entry.title|striptags|url_encode}}&amp;v=6" target="_blank" class="tool icon-image unmark" title="unmark">
145 <i class="tool icon-image icon-image--unmark" title="unmark"></i>
146 <span>unmark.it</span> 150 <span>unmark.it</span>
147 </a> 151 </a>
148 </li> 152 </li>
149 {% endif %} 153 {% endif %}
150 {% if craue_setting('carrot') %} 154 {% if craue_setting('carrot') %}
151 <li> 155 <li>
152 <a href="https://secure.carrot.org/GiveAndGetBack.do?url={{ entry.url|url_encode }}&amp;title={{ entry.title|striptags|url_encode }}" target="_blank" title="carrot"> 156 <a href="https://secure.carrot.org/GiveAndGetBack.do?url={{ entry.url|url_encode }}&amp;title={{ entry.title|striptags|url_encode }}" target="_blank" title="carrot" class="tool icon-image carrot">
153 <i class="tool icon-image icon-image--carrot"></i>
154 <span>Carrot</span> 157 <span>Carrot</span>
155 </a> 158 </a>
156 </li> 159 </li>
157 {% endif %} 160 {% endif %}
158 {% if craue_setting('share_mail') %} 161 {% if craue_setting('share_mail') %}
159 <li> 162 <li>
160 <a href="mailto:?subject={{ entry.title|striptags|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" title="{{ 'entry.view.left_menu.share_email_label'|trans }}" class="tool email icon icon-mail"> 163 <a href="mailto:?subject={{ entry.title|striptags|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" title="{{ 'entry.view.left_menu.share_email_label'|trans }}" class="tool icon">
164 <i class="material-icons vertical-align-middle">mail</i>
161 <span>{{ 'entry.view.left_menu.share_email_label'|trans }}</span> 165 <span>{{ 'entry.view.left_menu.share_email_label'|trans }}</span>
162 </a> 166 </a>
163 </li> 167 </li>
@@ -212,32 +216,51 @@
212 <h1>{{ entry.title|striptags|raw }} <a href="{{ path('edit', { 'id': entry.id }) }}" title="{{ 'entry.view.edit_title'|trans }}">✎</a></h1> 216 <h1>{{ entry.title|striptags|raw }} <a href="{{ path('edit', { 'id': entry.id }) }}" title="{{ 'entry.view.edit_title'|trans }}">✎</a></h1>
213 </header> 217 </header>
214 <aside> 218 <aside>
215 <ul class="tools"> 219 <div class="tools">
216 <li> 220 <ul class="stats">
217 {% include "@WallabagCore/themes/material/Entry/_reading_time.html.twig" with {'entry': entry} only %} 221 <li>
218 </li> 222 {% include "@WallabagCore/themes/material/Entry/_reading_time.html.twig" with {'entry': entry} only %}
219 <li> 223 </li>
220 <i class="material-icons" title="{{ 'entry.view.created_at'|trans }}">today</i> 224 <li>
221 {{ entry.createdAt|date('Y-m-d') }} 225 <i class="material-icons" title="{{ 'entry.view.created_at'|trans }}">today</i>
222 </li> 226 {{ entry.createdAt|date('Y-m-d H:i') }}
223 <li> 227 </li>
224 <i class="material-icons link">link</i> 228 {% if entry.publishedAt is not null %}
225 <a href="{{ entry.url|e }}" target="_blank" title="{{ 'entry.view.original_article'|trans }} : {{ entry.title|striptags }}" class="tool"> 229 <li>
226 {{ entry.domainName|removeWww }} 230 <i class="material-icons" title="{{ 'entry.view.published_at'|trans }}">create</i>
227 </a> 231 {{ entry.publishedAt|date('Y-m-d H:i') }}
228 </li> 232 </li>
229 <li> 233 {% endif %}
230 <i class="material-icons link">comment</i> 234 {% if entry.publishedBy is not empty %}
231 {{ 'entry.view.annotations_on_the_entry'|transchoice(entry.annotations | length) }} 235 <li>
232 </li> 236 <i class="material-icons" title="{{ 'entry.view.published_by'|trans }}">person</i>
233 <li id="list"> 237 {% for author in entry.publishedBy %}
238 {{ author }}{% if not loop.last %}, {% endif %}
239 {% endfor %}
240 </li>
241 {% endif %}
242 <li>
243 <i class="material-icons link">link</i>
244 <a href="{{ entry.url|e }}" target="_blank" title="{{ 'entry.view.original_article'|trans }} : {{ entry.title|striptags }}" class="tool">
245 {{ entry.domainName|removeWww }}
246 </a>
247 </li>
248 <li>
249 <i class="material-icons link">comment</i>
250 {{ 'entry.view.annotations_on_the_entry'|transchoice(entry.annotations | length) }}
251 </li>
252 </ul>
253 <ul class="tags">
234 {% for tag in entry.tags %} 254 {% for tag in entry.tags %}
235 <div class="chip"> 255 <li class="chip">
236 <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a> <a href="{{ path('remove_tag', { 'entry': entry.id, 'tag': tag.id }) }}"><i class="material-icons">delete</i></a> 256 <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a>
237 </div> 257 <a href="{{ path('remove_tag', { 'entry': entry.id, 'tag': tag.id }) }}" onclick="return confirm('{{ 'entry.confirm.delete_tag'|trans|escape('js') }}')">
258 <i class="material-icons vertical-align-middle">delete</i>
259 </a>
260 </li>
238 {% endfor %} 261 {% endfor %}
239 </li> 262 </ul>
240 </ul> 263 </div>
241 264
242 <div class="input-field nav-panel-add-tag" style="display: none"> 265 <div class="input-field nav-panel-add-tag" style="display: none">
243 {{ render(controller( "WallabagCoreBundle:Tag:addTagForm", { 'id': entry.id } )) }} 266 {{ render(controller( "WallabagCoreBundle:Tag:addTagForm", { 'id': entry.id } )) }}
@@ -259,7 +282,7 @@
259 <ul> 282 <ul>
260 <li><a class="btn-floating" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons">done</i></a></li> 283 <li><a class="btn-floating" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons">done</i></a></li>
261 <li><a class="btn-floating" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons">star_outline</i></a></li> 284 <li><a class="btn-floating" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons">star_outline</i></a></li>
262 <li><a class="btn-floating" href="{{ path('delete_entry', { 'id': entry.id }) }}"><i class="material-icons">delete</i></a></li> 285 <li><a class="btn-floating" href="{{ path('delete_entry', { 'id': entry.id }) }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')"><i class="material-icons">delete</i></a></li>
263 </ul> 286 </ul>
264 </div> 287 </div>
265 </div> 288 </div>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/edit.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/edit.html.twig
new file mode 100644
index 00000000..882be430
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/edit.html.twig
@@ -0,0 +1,60 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{{ 'site_credential.page_title'|trans }}{% endblock %}
4
5{% block content %}
6
7 <div class="row">
8 <div class="col s12">
9 <div class="card-panel">
10 <div class="row">
11 <div class="input-field col s12">
12 <h4>{{ 'site_credential.edit_site_credential'|trans }}</h4>
13
14 <div id="set6" class="col s12">
15 {{ form_start(edit_form) }}
16 {{ form_errors(edit_form) }}
17
18 <div class="row">
19 <div class="input-field col s12">
20 {{ form_label(edit_form.host) }}
21 {{ form_errors(edit_form.host) }}
22 {{ form_widget(edit_form.host) }}
23 </div>
24 </div>
25
26 <div class="row">
27 <div class="input-field col s12">
28 {{ form_label(edit_form.username) }}
29 {{ form_errors(edit_form.username) }}
30 {{ form_widget(edit_form.username) }}
31 </div>
32 </div>
33
34 <div class="row">
35 <div class="input-field col s12">
36 {{ form_label(edit_form.password) }}
37 {{ form_errors(edit_form.password) }}
38 {{ form_widget(edit_form.password) }}
39 </div>
40 </div>
41
42 <br/>
43
44 {{ form_widget(edit_form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
45 {{ form_widget(edit_form._token) }}
46 </form>
47 <p>
48 {{ form_start(delete_form) }}
49 <button onclick="return confirm('{{ 'site_credential.form.delete_confirm'|trans|escape('js') }}')" type="submit" class="btn waves-effect waves-light red">{{ 'site_credential.form.delete'|trans }}</button>
50 {{ form_end(delete_form) }}
51 </p>
52 <p><a class="waves-effect waves-light btn blue-grey" href="{{ path('site_credentials_index') }}">{{ 'site_credential.form.back_to_list'|trans }}</a></p>
53 </div>
54 </div>
55 </div>
56 </div>
57 </div>
58 </div>
59
60{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/index.html.twig
new file mode 100644
index 00000000..324854ad
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/index.html.twig
@@ -0,0 +1,42 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{{ 'site_credential.page_title'|trans }}{% endblock %}
4
5{% block content %}
6
7 <div class="row">
8 <div class="col s12">
9 <div class="card-panel">
10 <div class="row">
11 <div class="input-field col s12">
12 <p class="help">{{ 'site_credential.description'|trans|raw }}</p>
13
14 <table class="bordered">
15 <thead>
16 <tr>
17 <th>{{ 'site_credential.form.host_label'|trans }}</th>
18 <th>{{ 'site_credential.list.actions'|trans }}</th>
19 </tr>
20 </thead>
21 <tbody>
22 {% for credential in credentials %}
23 <tr>
24 <td>{{ credential.host }}</td>
25 <td>
26 <a href="{{ path('site_credentials_edit', { 'id': credential.id }) }}">{{ 'site_credential.list.edit_action'|trans }}</a>
27 </td>
28 </tr>
29 {% endfor %}
30 </tbody>
31 </table>
32 <br />
33 <p>
34 <a href="{{ path('site_credentials_new') }}" class="waves-effect waves-light btn">{{ 'site_credential.list.create_new_one'|trans }}</a>
35 </p>
36 </div>
37 </div>
38 </div>
39 </div>
40 </div>
41
42{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/new.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/new.html.twig
new file mode 100644
index 00000000..3c008cde
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/SiteCredential/new.html.twig
@@ -0,0 +1,53 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{{ 'site_credential.page_title'|trans }}{% endblock %}
4
5{% block content %}
6
7 <div class="row">
8 <div class="col s12">
9 <div class="card-panel">
10 <div class="row">
11 <div class="input-field col s12">
12 <h4>{{ 'site_credential.new_site_credential'|trans }}</h4>
13
14 <div id="set6" class="col s12">
15 {{ form_start(form) }}
16 {{ form_errors(form) }}
17
18 <div class="row">
19 <div class="input-field col s12">
20 {{ form_label(form.host) }}
21 {{ form_errors(form.host) }}
22 {{ form_widget(form.host) }}
23 </div>
24 </div>
25
26 <div class="row">
27 <div class="input-field col s12">
28 {{ form_label(form.username) }}
29 {{ form_errors(form.username) }}
30 {{ form_widget(form.username) }}
31 </div>
32 </div>
33
34 <div class="row">
35 <div class="input-field col s12">
36 {{ form_label(form.password) }}
37 {{ form_errors(form.password) }}
38 {{ form_widget(form.password) }}
39 </div>
40 </div>
41
42 {{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
43 {{ form_rest(form) }}
44 </form>
45 <p><a class="waves-effect waves-light btn blue-grey" href="{{ path('site_credentials_index') }}">{{ 'site_credential.form.back_to_list'|trans }}</a></p>
46 </div>
47 </div>
48 </div>
49 </div>
50 </div>
51 </div>
52
53{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Tag/tags.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Tag/tags.html.twig
index c83543ac..97ddedc9 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Tag/tags.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Tag/tags.html.twig
@@ -14,6 +14,9 @@
14 {% for tag in tags %} 14 {% for tag in tags %}
15 <li title="{{tag.label}} ({{ tag.nbEntries }})" id="tag-{{ tag.id }}" class="col l2 m2 s5"> 15 <li title="{{tag.label}} ({{ tag.nbEntries }})" id="tag-{{ tag.id }}" class="col l2 m2 s5">
16 <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{tag.label}} ({{ tag.nbEntries }})</a> 16 <a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{tag.label}} ({{ tag.nbEntries }})</a>
17 {% if app.user.config.rssToken %}
18 <a rel="alternate" type="application/rss+xml" href="{{ path('tag_rss', {'username': app.user.username, 'token': app.user.config.rssToken, 'slug': tag.slug}) }}" class="right"><i class="material-icons">rss_feed</i></a>
19 {% endif %}
17 </li> 20 </li>
18 {% endfor %} 21 {% endfor %}
19 </ul> 22 </ul>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
index 3c169c04..60907e11 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
@@ -2,12 +2,14 @@
2 2
3{% block css %} 3{% block css %}
4 {{ parent() }} 4 {{ parent() }}
5 <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/themes/material/css/style.min.css') }}" media="screen,projection,print"/> 5 {% if not app.debug %}
6 <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/material.css') }}">
7 {% endif %}
6{% endblock %} 8{% endblock %}
7 9
8{% block scripts %} 10{% block scripts %}
9 {{ parent() }} 11 {{ parent() }}
10 <script src="{{ asset('bundles/wallabagcore/themes/material/js/material.min.js') }}"></script> 12 <script src="{{ asset('bundles/wallabagcore/material' ~ (app.debug ? '.dev' : '') ~ '.js') }}"></script>
11{% endblock %} 13{% endblock %}
12 14
13{% block header %} 15{% block header %}
@@ -64,6 +66,11 @@
64 <li class="bold {% if currentRoute == 'config' %}active{% endif %}"> 66 <li class="bold {% if currentRoute == 'config' %}active{% endif %}">
65 <a class="waves-effect" href="{{ path('config') }}">{{ 'menu.left.config'|trans }}</a> 67 <a class="waves-effect" href="{{ path('config') }}">{{ 'menu.left.config'|trans }}</a>
66 </li> 68 </li>
69 {% if craue_setting('restricted_access') %}
70 <li class="bold {% if currentRoute starts with 'site_credentials_' %}active{% endif %}">
71 <a class="waves-effect" href="{{ path('site_credentials_index') }}">{{ 'menu.left.site_credentials'|trans }}</a>
72 </li>
73 {% endif %}
67 {% if is_granted('ROLE_SUPER_ADMIN') %} 74 {% if is_granted('ROLE_SUPER_ADMIN') %}
68 <li class="bold {% if currentRoute starts with 'user_' %}active{% endif %}"> 75 <li class="bold {% if currentRoute starts with 'user_' %}active{% endif %}">
69 <a class="waves-effect" href="{{ path('user_index') }}">{{ 'menu.left.users_management'|trans }}</a> 76 <a class="waves-effect" href="{{ path('user_index') }}">{{ 'menu.left.users_management'|trans }}</a>
@@ -116,12 +123,12 @@
116 </ul> 123 </ul>
117 <div class="input-field nav-panel-search" style="display: none"> 124 <div class="input-field nav-panel-search" style="display: none">
118 {{ render(controller("WallabagCoreBundle:Entry:searchForm", {'currentRoute': app.request.attributes.get('_route')})) }} 125 {{ render(controller("WallabagCoreBundle:Entry:searchForm", {'currentRoute': app.request.attributes.get('_route')})) }}
119 <label for="search" class="active"><i class="material-icons search">search</i></label> 126 <label for="search"><i class="material-icons search">search</i></label>
120 <i class="material-icons close">clear</i> 127 <i class="material-icons close">clear</i>
121 </div> 128 </div>
122 <div class="input-field nav-panel-add" style="display: none"> 129 <div class="input-field nav-panel-add" style="display: none">
123 {{ render(controller("WallabagCoreBundle:Entry:addEntryForm")) }} 130 {{ render(controller("WallabagCoreBundle:Entry:addEntryForm")) }}
124 <label for="add" class="active"><i class="material-icons add">add</i></label> 131 <label for="add"><i class="material-icons add">add</i></label>
125 <i class="material-icons close">clear</i> 132 <i class="material-icons close">clear</i>
126 </div> 133 </div>
127 </div> 134 </div>
diff --git a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php
index a305c53f..351172c4 100644
--- a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php
+++ b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php
@@ -3,9 +3,9 @@
3namespace Wallabag\CoreBundle\Twig; 3namespace Wallabag\CoreBundle\Twig;
4 4
5use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 5use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
6use Symfony\Component\Translation\TranslatorInterface;
6use Wallabag\CoreBundle\Repository\EntryRepository; 7use Wallabag\CoreBundle\Repository\EntryRepository;
7use Wallabag\CoreBundle\Repository\TagRepository; 8use Wallabag\CoreBundle\Repository\TagRepository;
8use Symfony\Component\Translation\TranslatorInterface;
9 9
10class WallabagExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface 10class WallabagExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface
11{ 11{
@@ -33,11 +33,11 @@ class WallabagExtension extends \Twig_Extension implements \Twig_Extension_Globa
33 33
34 public function getFunctions() 34 public function getFunctions()
35 { 35 {
36 return array( 36 return [
37 new \Twig_SimpleFunction('count_entries', [$this, 'countEntries']), 37 new \Twig_SimpleFunction('count_entries', [$this, 'countEntries']),
38 new \Twig_SimpleFunction('count_tags', [$this, 'countTags']), 38 new \Twig_SimpleFunction('count_tags', [$this, 'countTags']),
39 new \Twig_SimpleFunction('display_stats', [$this, 'displayStats']), 39 new \Twig_SimpleFunction('display_stats', [$this, 'displayStats']),
40 ); 40 ];
41 } 41 }
42 42
43 public function removeWww($url) 43 public function removeWww($url)
@@ -64,19 +64,15 @@ class WallabagExtension extends \Twig_Extension implements \Twig_Extension_Globa
64 case 'starred': 64 case 'starred':
65 $qb = $this->entryRepository->getBuilderForStarredByUser($user->getId()); 65 $qb = $this->entryRepository->getBuilderForStarredByUser($user->getId());
66 break; 66 break;
67
68 case 'archive': 67 case 'archive':
69 $qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId()); 68 $qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId());
70 break; 69 break;
71
72 case 'unread': 70 case 'unread':
73 $qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId()); 71 $qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId());
74 break; 72 break;
75
76 case 'all': 73 case 'all':
77 $qb = $this->entryRepository->getBuilderForAllByUser($user->getId()); 74 $qb = $this->entryRepository->getBuilderForAllByUser($user->getId());
78 break; 75 break;
79
80 default: 76 default:
81 throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type)); 77 throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
82 } 78 }
@@ -139,7 +135,7 @@ class WallabagExtension extends \Twig_Extension implements \Twig_Extension_Globa
139 $nbDays = (int) $interval->format('%a') ?: 1; 135 $nbDays = (int) $interval->format('%a') ?: 1;
140 136
141 // force setlocale for date translation 137 // force setlocale for date translation
142 setlocale(LC_TIME, strtolower($user->getConfig()->getLanguage()).'_'.strtoupper(strtolower($user->getConfig()->getLanguage()))); 138 setlocale(LC_TIME, strtolower($user->getConfig()->getLanguage()) . '_' . strtoupper(strtolower($user->getConfig()->getLanguage())));
143 139
144 return $this->translator->trans('footer.stats', [ 140 return $this->translator->trans('footer.stats', [
145 '%user_creation%' => strftime('%e %B %Y', $user->getCreatedAt()->getTimestamp()), 141 '%user_creation%' => strftime('%e %B %Y', $user->getCreatedAt()->getTimestamp()),
diff --git a/src/Wallabag/ImportBundle/Command/ImportCommand.php b/src/Wallabag/ImportBundle/Command/ImportCommand.php
index 28d01715..99056c2c 100644
--- a/src/Wallabag/ImportBundle/Command/ImportCommand.php
+++ b/src/Wallabag/ImportBundle/Command/ImportCommand.php
@@ -6,6 +6,7 @@ use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
6use Symfony\Component\Config\Definition\Exception\Exception; 6use Symfony\Component\Config\Definition\Exception\Exception;
7use Symfony\Component\Console\Input\InputArgument; 7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface; 8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
9use Symfony\Component\Console\Output\OutputInterface; 10use Symfony\Component\Console\Output\OutputInterface;
10 11
11class ImportCommand extends ContainerAwareCommand 12class ImportCommand extends ContainerAwareCommand
@@ -15,16 +16,18 @@ class ImportCommand extends ContainerAwareCommand
15 $this 16 $this
16 ->setName('wallabag:import') 17 ->setName('wallabag:import')
17 ->setDescription('Import entries from a JSON export') 18 ->setDescription('Import entries from a JSON export')
18 ->addArgument('userId', InputArgument::REQUIRED, 'User ID to populate') 19 ->addArgument('username', InputArgument::REQUIRED, 'User to populate')
19 ->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file') 20 ->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file')
20 ->addOption('importer', null, InputArgument::OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, readability, firefox or chrome', 'v1') 21 ->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, readability, firefox or chrome', 'v1')
21 ->addOption('markAsRead', null, InputArgument::OPTIONAL, 'Mark all entries as read', false) 22 ->addOption('markAsRead', null, InputOption::VALUE_OPTIONAL, 'Mark all entries as read', false)
23 ->addOption('useUserId', null, InputOption::VALUE_NONE, 'Use user id instead of username to find account')
24 ->addOption('disableContentUpdate', null, InputOption::VALUE_NONE, 'Disable fetching updated content from URL')
22 ; 25 ;
23 } 26 }
24 27
25 protected function execute(InputInterface $input, OutputInterface $output) 28 protected function execute(InputInterface $input, OutputInterface $output)
26 { 29 {
27 $output->writeln('Start : '.(new \DateTime())->format('d-m-Y G:i:s').' ---'); 30 $output->writeln('Start : ' . (new \DateTime())->format('d-m-Y G:i:s') . ' ---');
28 31
29 if (!file_exists($input->getArgument('filepath'))) { 32 if (!file_exists($input->getArgument('filepath'))) {
30 throw new Exception(sprintf('File "%s" not found', $input->getArgument('filepath'))); 33 throw new Exception(sprintf('File "%s" not found', $input->getArgument('filepath')));
@@ -34,10 +37,14 @@ class ImportCommand extends ContainerAwareCommand
34 // Turning off doctrine default logs queries for saving memory 37 // Turning off doctrine default logs queries for saving memory
35 $em->getConnection()->getConfiguration()->setSQLLogger(null); 38 $em->getConnection()->getConfiguration()->setSQLLogger(null);
36 39
37 $user = $em->getRepository('WallabagUserBundle:User')->findOneById($input->getArgument('userId')); 40 if ($input->getOption('useUserId')) {
41 $user = $em->getRepository('WallabagUserBundle:User')->findOneById($input->getArgument('username'));
42 } else {
43 $user = $em->getRepository('WallabagUserBundle:User')->findOneByUsername($input->getArgument('username'));
44 }
38 45
39 if (!is_object($user)) { 46 if (!is_object($user)) {
40 throw new Exception(sprintf('User with id "%s" not found', $input->getArgument('userId'))); 47 throw new Exception(sprintf('User "%s" not found', $input->getArgument('username')));
41 } 48 }
42 49
43 switch ($input->getOption('importer')) { 50 switch ($input->getOption('importer')) {
@@ -64,6 +71,7 @@ class ImportCommand extends ContainerAwareCommand
64 } 71 }
65 72
66 $import->setMarkAsRead($input->getOption('markAsRead')); 73 $import->setMarkAsRead($input->getOption('markAsRead'));
74 $import->setDisableContentUpdate($input->getOption('disableContentUpdate'));
67 $import->setUser($user); 75 $import->setUser($user);
68 76
69 $res = $import 77 $res = $import
@@ -72,12 +80,12 @@ class ImportCommand extends ContainerAwareCommand
72 80
73 if (true === $res) { 81 if (true === $res) {
74 $summary = $import->getSummary(); 82 $summary = $import->getSummary();
75 $output->writeln('<info>'.$summary['imported'].' imported</info>'); 83 $output->writeln('<info>' . $summary['imported'] . ' imported</info>');
76 $output->writeln('<comment>'.$summary['skipped'].' already saved</comment>'); 84 $output->writeln('<comment>' . $summary['skipped'] . ' already saved</comment>');
77 } 85 }
78 86
79 $em->clear(); 87 $em->clear();
80 88
81 $output->writeln('End : '.(new \DateTime())->format('d-m-Y G:i:s').' ---'); 89 $output->writeln('End : ' . (new \DateTime())->format('d-m-Y G:i:s') . ' ---');
82 } 90 }
83} 91}
diff --git a/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php b/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php
index 2d06af44..d94900ad 100644
--- a/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php
+++ b/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php
@@ -2,13 +2,13 @@
2 2
3namespace Wallabag\ImportBundle\Command; 3namespace Wallabag\ImportBundle\Command;
4 4
5use Simpleue\Worker\QueueWorker;
5use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; 6use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
6use Symfony\Component\Config\Definition\Exception\Exception; 7use Symfony\Component\Config\Definition\Exception\Exception;
7use Symfony\Component\Console\Input\InputArgument; 8use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputOption;
9use Symfony\Component\Console\Input\InputInterface; 9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface; 11use Symfony\Component\Console\Output\OutputInterface;
11use Simpleue\Worker\QueueWorker;
12 12
13class RedisWorkerCommand extends ContainerAwareCommand 13class RedisWorkerCommand extends ContainerAwareCommand
14{ 14{
@@ -24,18 +24,18 @@ class RedisWorkerCommand extends ContainerAwareCommand
24 24
25 protected function execute(InputInterface $input, OutputInterface $output) 25 protected function execute(InputInterface $input, OutputInterface $output)
26 { 26 {
27 $output->writeln('Worker started at: '.(new \DateTime())->format('d-m-Y G:i:s')); 27 $output->writeln('Worker started at: ' . (new \DateTime())->format('d-m-Y G:i:s'));
28 $output->writeln('Waiting for message ...'); 28 $output->writeln('Waiting for message ...');
29 29
30 $serviceName = $input->getArgument('serviceName'); 30 $serviceName = $input->getArgument('serviceName');
31 31
32 if (!$this->getContainer()->has('wallabag_import.queue.redis.'.$serviceName) || !$this->getContainer()->has('wallabag_import.consumer.redis.'.$serviceName)) { 32 if (!$this->getContainer()->has('wallabag_import.queue.redis.' . $serviceName) || !$this->getContainer()->has('wallabag_import.consumer.redis.' . $serviceName)) {
33 throw new Exception(sprintf('No queue or consumer found for service name: "%s"', $input->getArgument('serviceName'))); 33 throw new Exception(sprintf('No queue or consumer found for service name: "%s"', $input->getArgument('serviceName')));
34 } 34 }
35 35
36 $worker = new QueueWorker( 36 $worker = new QueueWorker(
37 $this->getContainer()->get('wallabag_import.queue.redis.'.$serviceName), 37 $this->getContainer()->get('wallabag_import.queue.redis.' . $serviceName),
38 $this->getContainer()->get('wallabag_import.consumer.redis.'.$serviceName), 38 $this->getContainer()->get('wallabag_import.consumer.redis.' . $serviceName),
39 (int) $input->getOption('maxIterations') 39 (int) $input->getOption('maxIterations')
40 ); 40 );
41 41
diff --git a/src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php b/src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php
index 992ce1ad..b035f5cc 100644
--- a/src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php
+++ b/src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php
@@ -3,14 +3,14 @@
3namespace Wallabag\ImportBundle\Consumer; 3namespace Wallabag\ImportBundle\Consumer;
4 4
5use Doctrine\ORM\EntityManager; 5use Doctrine\ORM\EntityManager;
6use Wallabag\ImportBundle\Import\AbstractImport;
7use Wallabag\UserBundle\Repository\UserRepository;
8use Wallabag\CoreBundle\Entity\Entry;
9use Wallabag\CoreBundle\Entity\Tag;
10use Psr\Log\LoggerInterface; 6use Psr\Log\LoggerInterface;
11use Psr\Log\NullLogger; 7use Psr\Log\NullLogger;
12use Symfony\Component\EventDispatcher\EventDispatcherInterface; 8use Symfony\Component\EventDispatcher\EventDispatcherInterface;
9use Wallabag\CoreBundle\Entity\Entry;
10use Wallabag\CoreBundle\Entity\Tag;
13use Wallabag\CoreBundle\Event\EntrySavedEvent; 11use Wallabag\CoreBundle\Event\EntrySavedEvent;
12use Wallabag\ImportBundle\Import\AbstractImport;
13use Wallabag\UserBundle\Repository\UserRepository;
14 14
15abstract class AbstractConsumer 15abstract class AbstractConsumer
16{ 16{
@@ -76,7 +76,7 @@ abstract class AbstractConsumer
76 return false; 76 return false;
77 } 77 }
78 78
79 $this->logger->info('Content with url imported! ('.$entry->getUrl().')'); 79 $this->logger->info('Content with url imported! (' . $entry->getUrl() . ')');
80 80
81 return true; 81 return true;
82 } 82 }
diff --git a/src/Wallabag/ImportBundle/Controller/BrowserController.php b/src/Wallabag/ImportBundle/Controller/BrowserController.php
index e119098f..77a7a904 100644
--- a/src/Wallabag/ImportBundle/Controller/BrowserController.php
+++ b/src/Wallabag/ImportBundle/Controller/BrowserController.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\ImportBundle\Controller; 3namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpFoundation\Response; 8use Symfony\Component\HttpFoundation\Response;
9use Wallabag\ImportBundle\Form\Type\UploadImportType; 9use Wallabag\ImportBundle\Form\Type\UploadImportType;
@@ -11,20 +11,6 @@ use Wallabag\ImportBundle\Form\Type\UploadImportType;
11abstract class BrowserController extends Controller 11abstract class BrowserController extends Controller
12{ 12{
13 /** 13 /**
14 * Return the service to handle the import.
15 *
16 * @return \Wallabag\ImportBundle\Import\ImportInterface
17 */
18 abstract protected function getImportService();
19
20 /**
21 * Return the template used for the form.
22 *
23 * @return string
24 */
25 abstract protected function getImportTemplate();
26
27 /**
28 * @Route("/browser", name="import_browser") 14 * @Route("/browser", name="import_browser")
29 * 15 *
30 * @param Request $request 16 * @param Request $request
@@ -42,11 +28,11 @@ abstract class BrowserController extends Controller
42 if ($form->isSubmitted() && $form->isValid()) { 28 if ($form->isSubmitted() && $form->isValid()) {
43 $file = $form->get('file')->getData(); 29 $file = $form->get('file')->getData();
44 $markAsRead = $form->get('mark_as_read')->getData(); 30 $markAsRead = $form->get('mark_as_read')->getData();
45 $name = $this->getUser()->getId().'.json'; 31 $name = $this->getUser()->getId() . '.json';
46 32
47 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) { 33 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
48 $res = $wallabag 34 $res = $wallabag
49 ->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name) 35 ->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
50 ->setMarkAsRead($markAsRead) 36 ->setMarkAsRead($markAsRead)
51 ->import(); 37 ->import();
52 38
@@ -65,7 +51,7 @@ abstract class BrowserController extends Controller
65 ]); 51 ]);
66 } 52 }
67 53
68 unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name); 54 unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
69 } 55 }
70 56
71 $this->get('session')->getFlashBag()->add( 57 $this->get('session')->getFlashBag()->add(
@@ -74,12 +60,11 @@ abstract class BrowserController extends Controller
74 ); 60 );
75 61
76 return $this->redirect($this->generateUrl('homepage')); 62 return $this->redirect($this->generateUrl('homepage'));
77 } else { 63 }
78 $this->get('session')->getFlashBag()->add( 64 $this->get('session')->getFlashBag()->add(
79 'notice', 65 'notice',
80 'flashes.import.notice.failed_on_file' 66 'flashes.import.notice.failed_on_file'
81 ); 67 );
82 }
83 } 68 }
84 69
85 return $this->render($this->getImportTemplate(), [ 70 return $this->render($this->getImportTemplate(), [
@@ -87,4 +72,18 @@ abstract class BrowserController extends Controller
87 'import' => $wallabag, 72 'import' => $wallabag,
88 ]); 73 ]);
89 } 74 }
75
76 /**
77 * Return the service to handle the import.
78 *
79 * @return \Wallabag\ImportBundle\Import\ImportInterface
80 */
81 abstract protected function getImportService();
82
83 /**
84 * Return the template used for the form.
85 *
86 * @return string
87 */
88 abstract protected function getImportTemplate();
90} 89}
diff --git a/src/Wallabag/ImportBundle/Controller/ChromeController.php b/src/Wallabag/ImportBundle/Controller/ChromeController.php
index 454f3347..0cb418a1 100644
--- a/src/Wallabag/ImportBundle/Controller/ChromeController.php
+++ b/src/Wallabag/ImportBundle/Controller/ChromeController.php
@@ -8,6 +8,14 @@ use Symfony\Component\HttpFoundation\Request;
8class ChromeController extends BrowserController 8class ChromeController extends BrowserController
9{ 9{
10 /** 10 /**
11 * @Route("/chrome", name="import_chrome")
12 */
13 public function indexAction(Request $request)
14 {
15 return parent::indexAction($request);
16 }
17
18 /**
11 * {@inheritdoc} 19 * {@inheritdoc}
12 */ 20 */
13 protected function getImportService() 21 protected function getImportService()
@@ -30,12 +38,4 @@ class ChromeController extends BrowserController
30 { 38 {
31 return 'WallabagImportBundle:Chrome:index.html.twig'; 39 return 'WallabagImportBundle:Chrome:index.html.twig';
32 } 40 }
33
34 /**
35 * @Route("/chrome", name="import_chrome")
36 */
37 public function indexAction(Request $request)
38 {
39 return parent::indexAction($request);
40 }
41} 41}
diff --git a/src/Wallabag/ImportBundle/Controller/FirefoxController.php b/src/Wallabag/ImportBundle/Controller/FirefoxController.php
index c329b9c4..88697f9d 100644
--- a/src/Wallabag/ImportBundle/Controller/FirefoxController.php
+++ b/src/Wallabag/ImportBundle/Controller/FirefoxController.php
@@ -8,6 +8,14 @@ use Symfony\Component\HttpFoundation\Request;
8class FirefoxController extends BrowserController 8class FirefoxController extends BrowserController
9{ 9{
10 /** 10 /**
11 * @Route("/firefox", name="import_firefox")
12 */
13 public function indexAction(Request $request)
14 {
15 return parent::indexAction($request);
16 }
17
18 /**
11 * {@inheritdoc} 19 * {@inheritdoc}
12 */ 20 */
13 protected function getImportService() 21 protected function getImportService()
@@ -30,12 +38,4 @@ class FirefoxController extends BrowserController
30 { 38 {
31 return 'WallabagImportBundle:Firefox:index.html.twig'; 39 return 'WallabagImportBundle:Firefox:index.html.twig';
32 } 40 }
33
34 /**
35 * @Route("/firefox", name="import_firefox")
36 */
37 public function indexAction(Request $request)
38 {
39 return parent::indexAction($request);
40 }
41} 41}
diff --git a/src/Wallabag/ImportBundle/Controller/ImportController.php b/src/Wallabag/ImportBundle/Controller/ImportController.php
index 237c748e..7e4fd174 100644
--- a/src/Wallabag/ImportBundle/Controller/ImportController.php
+++ b/src/Wallabag/ImportBundle/Controller/ImportController.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\ImportBundle\Controller; 3namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7 7
8class ImportController extends Controller 8class ImportController extends Controller
9{ 9{
@@ -86,9 +86,9 @@ class ImportController extends Controller
86 private function getTotalMessageInRabbitQueue($importService) 86 private function getTotalMessageInRabbitQueue($importService)
87 { 87 {
88 $message = $this 88 $message = $this
89 ->get('old_sound_rabbit_mq.import_'.$importService.'_consumer') 89 ->get('old_sound_rabbit_mq.import_' . $importService . '_consumer')
90 ->getChannel() 90 ->getChannel()
91 ->basic_get('wallabag.import.'.$importService); 91 ->basic_get('wallabag.import.' . $importService);
92 92
93 if (null === $message) { 93 if (null === $message) {
94 return 0; 94 return 0;
diff --git a/src/Wallabag/ImportBundle/Controller/InstapaperController.php b/src/Wallabag/ImportBundle/Controller/InstapaperController.php
index 0251acb9..550679c3 100644
--- a/src/Wallabag/ImportBundle/Controller/InstapaperController.php
+++ b/src/Wallabag/ImportBundle/Controller/InstapaperController.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\ImportBundle\Controller; 3namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Wallabag\ImportBundle\Form\Type\UploadImportType; 8use Wallabag\ImportBundle\Form\Type\UploadImportType;
9 9
@@ -29,11 +29,11 @@ class InstapaperController extends Controller
29 if ($form->isSubmitted() && $form->isValid()) { 29 if ($form->isSubmitted() && $form->isValid()) {
30 $file = $form->get('file')->getData(); 30 $file = $form->get('file')->getData();
31 $markAsRead = $form->get('mark_as_read')->getData(); 31 $markAsRead = $form->get('mark_as_read')->getData();
32 $name = 'instapaper_'.$this->getUser()->getId().'.csv'; 32 $name = 'instapaper_' . $this->getUser()->getId() . '.csv';
33 33
34 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) { 34 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
35 $res = $instapaper 35 $res = $instapaper
36 ->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name) 36 ->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
37 ->setMarkAsRead($markAsRead) 37 ->setMarkAsRead($markAsRead)
38 ->import(); 38 ->import();
39 39
@@ -52,7 +52,7 @@ class InstapaperController extends Controller
52 ]); 52 ]);
53 } 53 }
54 54
55 unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name); 55 unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
56 } 56 }
57 57
58 $this->get('session')->getFlashBag()->add( 58 $this->get('session')->getFlashBag()->add(
@@ -61,12 +61,12 @@ class InstapaperController extends Controller
61 ); 61 );
62 62
63 return $this->redirect($this->generateUrl('homepage')); 63 return $this->redirect($this->generateUrl('homepage'));
64 } else {
65 $this->get('session')->getFlashBag()->add(
66 'notice',
67 'flashes.import.notice.failed_on_file'
68 );
69 } 64 }
65
66 $this->get('session')->getFlashBag()->add(
67 'notice',
68 'flashes.import.notice.failed_on_file'
69 );
70 } 70 }
71 71
72 return $this->render('WallabagImportBundle:Instapaper:index.html.twig', [ 72 return $this->render('WallabagImportBundle:Instapaper:index.html.twig', [
diff --git a/src/Wallabag/ImportBundle/Controller/PinboardController.php b/src/Wallabag/ImportBundle/Controller/PinboardController.php
index d0ad8aa8..0e57fd41 100644
--- a/src/Wallabag/ImportBundle/Controller/PinboardController.php
+++ b/src/Wallabag/ImportBundle/Controller/PinboardController.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\ImportBundle\Controller; 3namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Wallabag\ImportBundle\Form\Type\UploadImportType; 8use Wallabag\ImportBundle\Form\Type\UploadImportType;
9 9
@@ -29,11 +29,11 @@ class PinboardController extends Controller
29 if ($form->isSubmitted() && $form->isValid()) { 29 if ($form->isSubmitted() && $form->isValid()) {
30 $file = $form->get('file')->getData(); 30 $file = $form->get('file')->getData();
31 $markAsRead = $form->get('mark_as_read')->getData(); 31 $markAsRead = $form->get('mark_as_read')->getData();
32 $name = 'pinboard_'.$this->getUser()->getId().'.json'; 32 $name = 'pinboard_' . $this->getUser()->getId() . '.json';
33 33
34 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) { 34 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
35 $res = $pinboard 35 $res = $pinboard
36 ->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name) 36 ->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
37 ->setMarkAsRead($markAsRead) 37 ->setMarkAsRead($markAsRead)
38 ->import(); 38 ->import();
39 39
@@ -52,7 +52,7 @@ class PinboardController extends Controller
52 ]); 52 ]);
53 } 53 }
54 54
55 unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name); 55 unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
56 } 56 }
57 57
58 $this->get('session')->getFlashBag()->add( 58 $this->get('session')->getFlashBag()->add(
@@ -61,12 +61,12 @@ class PinboardController extends Controller
61 ); 61 );
62 62
63 return $this->redirect($this->generateUrl('homepage')); 63 return $this->redirect($this->generateUrl('homepage'));
64 } else {
65 $this->get('session')->getFlashBag()->add(
66 'notice',
67 'flashes.import.notice.failed_on_file'
68 );
69 } 64 }
65
66 $this->get('session')->getFlashBag()->add(
67 'notice',
68 'flashes.import.notice.failed_on_file'
69 );
70 } 70 }
71 71
72 return $this->render('WallabagImportBundle:Pinboard:index.html.twig', [ 72 return $this->render('WallabagImportBundle:Pinboard:index.html.twig', [
diff --git a/src/Wallabag/ImportBundle/Controller/PocketController.php b/src/Wallabag/ImportBundle/Controller/PocketController.php
index 56be5cbf..9f28819a 100644
--- a/src/Wallabag/ImportBundle/Controller/PocketController.php
+++ b/src/Wallabag/ImportBundle/Controller/PocketController.php
@@ -2,34 +2,15 @@
2 2
3namespace Wallabag\ImportBundle\Controller; 3namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
7use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
8use Symfony\Component\HttpFoundation\Request; 6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9use Symfony\Component\Form\Extension\Core\Type\CheckboxType; 7use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
8use Symfony\Component\HttpFoundation\Request;
9use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
10 10
11class PocketController extends Controller 11class PocketController extends Controller
12{ 12{
13 /** 13 /**
14 * Return Pocket Import Service with or without RabbitMQ enabled.
15 *
16 * @return \Wallabag\ImportBundle\Import\PocketImport
17 */
18 private function getPocketImportService()
19 {
20 $pocket = $this->get('wallabag_import.pocket.import');
21 $pocket->setUser($this->getUser());
22
23 if ($this->get('craue_config')->get('import_with_rabbitmq')) {
24 $pocket->setProducer($this->get('old_sound_rabbit_mq.import_pocket_producer'));
25 } elseif ($this->get('craue_config')->get('import_with_redis')) {
26 $pocket->setProducer($this->get('wallabag_import.producer.redis.pocket'));
27 }
28
29 return $pocket;
30 }
31
32 /**
33 * @Route("/pocket", name="import_pocket") 14 * @Route("/pocket", name="import_pocket")
34 */ 15 */
35 public function indexAction() 16 public function indexAction()
@@ -70,7 +51,7 @@ class PocketController extends Controller
70 $this->get('session')->set('mark_as_read', $request->request->get('form')['mark_as_read']); 51 $this->get('session')->set('mark_as_read', $request->request->get('form')['mark_as_read']);
71 52
72 return $this->redirect( 53 return $this->redirect(
73 'https://getpocket.com/auth/authorize?request_token='.$requestToken.'&redirect_uri='.$this->generateUrl('import_pocket_callback', [], UrlGeneratorInterface::ABSOLUTE_URL), 54 'https://getpocket.com/auth/authorize?request_token=' . $requestToken . '&redirect_uri=' . $this->generateUrl('import_pocket_callback', [], UrlGeneratorInterface::ABSOLUTE_URL),
74 301 55 301
75 ); 56 );
76 } 57 }
@@ -117,4 +98,23 @@ class PocketController extends Controller
117 98
118 return $this->redirect($this->generateUrl('homepage')); 99 return $this->redirect($this->generateUrl('homepage'));
119 } 100 }
101
102 /**
103 * Return Pocket Import Service with or without RabbitMQ enabled.
104 *
105 * @return \Wallabag\ImportBundle\Import\PocketImport
106 */
107 private function getPocketImportService()
108 {
109 $pocket = $this->get('wallabag_import.pocket.import');
110 $pocket->setUser($this->getUser());
111
112 if ($this->get('craue_config')->get('import_with_rabbitmq')) {
113 $pocket->setProducer($this->get('old_sound_rabbit_mq.import_pocket_producer'));
114 } elseif ($this->get('craue_config')->get('import_with_redis')) {
115 $pocket->setProducer($this->get('wallabag_import.producer.redis.pocket'));
116 }
117
118 return $pocket;
119 }
120} 120}
diff --git a/src/Wallabag/ImportBundle/Controller/ReadabilityController.php b/src/Wallabag/ImportBundle/Controller/ReadabilityController.php
index aa732ddd..59de24cb 100644
--- a/src/Wallabag/ImportBundle/Controller/ReadabilityController.php
+++ b/src/Wallabag/ImportBundle/Controller/ReadabilityController.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\ImportBundle\Controller; 3namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Wallabag\ImportBundle\Form\Type\UploadImportType; 8use Wallabag\ImportBundle\Form\Type\UploadImportType;
9 9
@@ -29,11 +29,11 @@ class ReadabilityController extends Controller
29 if ($form->isSubmitted() && $form->isValid()) { 29 if ($form->isSubmitted() && $form->isValid()) {
30 $file = $form->get('file')->getData(); 30 $file = $form->get('file')->getData();
31 $markAsRead = $form->get('mark_as_read')->getData(); 31 $markAsRead = $form->get('mark_as_read')->getData();
32 $name = 'readability_'.$this->getUser()->getId().'.json'; 32 $name = 'readability_' . $this->getUser()->getId() . '.json';
33 33
34 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) { 34 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
35 $res = $readability 35 $res = $readability
36 ->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name) 36 ->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
37 ->setMarkAsRead($markAsRead) 37 ->setMarkAsRead($markAsRead)
38 ->import(); 38 ->import();
39 39
@@ -52,7 +52,7 @@ class ReadabilityController extends Controller
52 ]); 52 ]);
53 } 53 }
54 54
55 unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name); 55 unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
56 } 56 }
57 57
58 $this->get('session')->getFlashBag()->add( 58 $this->get('session')->getFlashBag()->add(
@@ -61,12 +61,12 @@ class ReadabilityController extends Controller
61 ); 61 );
62 62
63 return $this->redirect($this->generateUrl('homepage')); 63 return $this->redirect($this->generateUrl('homepage'));
64 } else {
65 $this->get('session')->getFlashBag()->add(
66 'notice',
67 'flashes.import.notice.failed_on_file'
68 );
69 } 64 }
65
66 $this->get('session')->getFlashBag()->add(
67 'notice',
68 'flashes.import.notice.failed_on_file'
69 );
70 } 70 }
71 71
72 return $this->render('WallabagImportBundle:Readability:index.html.twig', [ 72 return $this->render('WallabagImportBundle:Readability:index.html.twig', [
diff --git a/src/Wallabag/ImportBundle/Controller/WallabagController.php b/src/Wallabag/ImportBundle/Controller/WallabagController.php
index e81c1ca9..6e6524b4 100644
--- a/src/Wallabag/ImportBundle/Controller/WallabagController.php
+++ b/src/Wallabag/ImportBundle/Controller/WallabagController.php
@@ -3,7 +3,9 @@
3namespace Wallabag\ImportBundle\Controller; 3namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller; 5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Symfony\Component\HttpFoundation\RedirectResponse;
6use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpFoundation\Response;
7use Wallabag\ImportBundle\Form\Type\UploadImportType; 9use Wallabag\ImportBundle\Form\Type\UploadImportType;
8 10
9/** 11/**
@@ -12,20 +14,6 @@ use Wallabag\ImportBundle\Form\Type\UploadImportType;
12abstract class WallabagController extends Controller 14abstract class WallabagController extends Controller
13{ 15{
14 /** 16 /**
15 * Return the service to handle the import.
16 *
17 * @return \Wallabag\ImportBundle\Import\ImportInterface
18 */
19 abstract protected function getImportService();
20
21 /**
22 * Return the template used for the form.
23 *
24 * @return string
25 */
26 abstract protected function getImportTemplate();
27
28 /**
29 * Handle import request. 17 * Handle import request.
30 * 18 *
31 * @param Request $request 19 * @param Request $request
@@ -43,11 +31,11 @@ abstract class WallabagController extends Controller
43 if ($form->isSubmitted() && $form->isValid()) { 31 if ($form->isSubmitted() && $form->isValid()) {
44 $file = $form->get('file')->getData(); 32 $file = $form->get('file')->getData();
45 $markAsRead = $form->get('mark_as_read')->getData(); 33 $markAsRead = $form->get('mark_as_read')->getData();
46 $name = $this->getUser()->getId().'.json'; 34 $name = $this->getUser()->getId() . '.json';
47 35
48 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) { 36 if (null !== $file && in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
49 $res = $wallabag 37 $res = $wallabag
50 ->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name) 38 ->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
51 ->setMarkAsRead($markAsRead) 39 ->setMarkAsRead($markAsRead)
52 ->import(); 40 ->import();
53 41
@@ -66,7 +54,7 @@ abstract class WallabagController extends Controller
66 ]); 54 ]);
67 } 55 }
68 56
69 unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name); 57 unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
70 } 58 }
71 59
72 $this->get('session')->getFlashBag()->add( 60 $this->get('session')->getFlashBag()->add(
@@ -75,12 +63,12 @@ abstract class WallabagController extends Controller
75 ); 63 );
76 64
77 return $this->redirect($this->generateUrl('homepage')); 65 return $this->redirect($this->generateUrl('homepage'));
78 } else {
79 $this->get('session')->getFlashBag()->add(
80 'notice',
81 'flashes.import.notice.failed_on_file'
82 );
83 } 66 }
67
68 $this->get('session')->getFlashBag()->add(
69 'notice',
70 'flashes.import.notice.failed_on_file'
71 );
84 } 72 }
85 73
86 return $this->render($this->getImportTemplate(), [ 74 return $this->render($this->getImportTemplate(), [
@@ -88,4 +76,18 @@ abstract class WallabagController extends Controller
88 'import' => $wallabag, 76 'import' => $wallabag,
89 ]); 77 ]);
90 } 78 }
79
80 /**
81 * Return the service to handle the import.
82 *
83 * @return \Wallabag\ImportBundle\Import\ImportInterface
84 */
85 abstract protected function getImportService();
86
87 /**
88 * Return the template used for the form.
89 *
90 * @return string
91 */
92 abstract protected function getImportTemplate();
91} 93}
diff --git a/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php
index 312c7a35..d700d8a8 100644
--- a/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php
+++ b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php
@@ -8,6 +8,14 @@ use Symfony\Component\HttpFoundation\Request;
8class WallabagV1Controller extends WallabagController 8class WallabagV1Controller extends WallabagController
9{ 9{
10 /** 10 /**
11 * @Route("/wallabag-v1", name="import_wallabag_v1")
12 */
13 public function indexAction(Request $request)
14 {
15 return parent::indexAction($request);
16 }
17
18 /**
11 * {@inheritdoc} 19 * {@inheritdoc}
12 */ 20 */
13 protected function getImportService() 21 protected function getImportService()
@@ -30,12 +38,4 @@ class WallabagV1Controller extends WallabagController
30 { 38 {
31 return 'WallabagImportBundle:WallabagV1:index.html.twig'; 39 return 'WallabagImportBundle:WallabagV1:index.html.twig';
32 } 40 }
33
34 /**
35 * @Route("/wallabag-v1", name="import_wallabag_v1")
36 */
37 public function indexAction(Request $request)
38 {
39 return parent::indexAction($request);
40 }
41} 41}
diff --git a/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php b/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php
index 45211fe6..ab26400c 100644
--- a/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php
+++ b/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php
@@ -8,6 +8,14 @@ use Symfony\Component\HttpFoundation\Request;
8class WallabagV2Controller extends WallabagController 8class WallabagV2Controller extends WallabagController
9{ 9{
10 /** 10 /**
11 * @Route("/wallabag-v2", name="import_wallabag_v2")
12 */
13 public function indexAction(Request $request)
14 {
15 return parent::indexAction($request);
16 }
17
18 /**
11 * {@inheritdoc} 19 * {@inheritdoc}
12 */ 20 */
13 protected function getImportService() 21 protected function getImportService()
@@ -30,12 +38,4 @@ class WallabagV2Controller extends WallabagController
30 { 38 {
31 return 'WallabagImportBundle:WallabagV2:index.html.twig'; 39 return 'WallabagImportBundle:WallabagV2:index.html.twig';
32 } 40 }
33
34 /**
35 * @Route("/wallabag-v2", name="import_wallabag_v2")
36 */
37 public function indexAction(Request $request)
38 {
39 return parent::indexAction($request);
40 }
41} 41}
diff --git a/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php b/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php
index 3f23c36b..cab70297 100644
--- a/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php
+++ b/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php
@@ -2,10 +2,10 @@
2 2
3namespace Wallabag\ImportBundle\DependencyInjection; 3namespace Wallabag\ImportBundle\DependencyInjection;
4 4
5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Symfony\Component\Config\FileLocator; 5use Symfony\Component\Config\FileLocator;
7use Symfony\Component\HttpKernel\DependencyInjection\Extension; 6use Symfony\Component\DependencyInjection\ContainerBuilder;
8use Symfony\Component\DependencyInjection\Loader; 7use Symfony\Component\DependencyInjection\Loader;
8use Symfony\Component\HttpKernel\DependencyInjection\Extension;
9 9
10class WallabagImportExtension extends Extension 10class WallabagImportExtension extends Extension
11{ 11{
@@ -16,7 +16,7 @@ class WallabagImportExtension extends Extension
16 $container->setParameter('wallabag_import.allow_mimetypes', $config['allow_mimetypes']); 16 $container->setParameter('wallabag_import.allow_mimetypes', $config['allow_mimetypes']);
17 $container->setParameter('wallabag_import.resource_dir', $config['resource_dir']); 17 $container->setParameter('wallabag_import.resource_dir', $config['resource_dir']);
18 18
19 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 19 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
20 $loader->load('services.yml'); 20 $loader->load('services.yml');
21 } 21 }
22 22
diff --git a/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php b/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php
index f50424c1..c50ef8c9 100644
--- a/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php
+++ b/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php
@@ -3,10 +3,10 @@
3namespace Wallabag\ImportBundle\Form\Type; 3namespace Wallabag\ImportBundle\Form\Type;
4 4
5use Symfony\Component\Form\AbstractType; 5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\FormBuilderInterface;
7use Symfony\Component\Form\Extension\Core\Type\SubmitType;
8use Symfony\Component\Form\Extension\Core\Type\FileType;
9use Symfony\Component\Form\Extension\Core\Type\CheckboxType; 6use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
7use Symfony\Component\Form\Extension\Core\Type\FileType;
8use Symfony\Component\Form\Extension\Core\Type\SubmitType;
9use Symfony\Component\Form\FormBuilderInterface;
10 10
11class UploadImportType extends AbstractType 11class UploadImportType extends AbstractType
12{ 12{
diff --git a/src/Wallabag/ImportBundle/Import/AbstractImport.php b/src/Wallabag/ImportBundle/Import/AbstractImport.php
index 1d4a6e27..58a234f4 100644
--- a/src/Wallabag/ImportBundle/Import/AbstractImport.php
+++ b/src/Wallabag/ImportBundle/Import/AbstractImport.php
@@ -2,35 +2,39 @@
2 2
3namespace Wallabag\ImportBundle\Import; 3namespace Wallabag\ImportBundle\Import;
4 4
5use Doctrine\ORM\EntityManager;
6use OldSound\RabbitMqBundle\RabbitMq\ProducerInterface;
5use Psr\Log\LoggerInterface; 7use Psr\Log\LoggerInterface;
6use Psr\Log\NullLogger; 8use Psr\Log\NullLogger;
7use Doctrine\ORM\EntityManager; 9use Symfony\Component\EventDispatcher\EventDispatcherInterface;
8use Wallabag\CoreBundle\Helper\ContentProxy;
9use Wallabag\CoreBundle\Entity\Entry; 10use Wallabag\CoreBundle\Entity\Entry;
10use Wallabag\CoreBundle\Entity\Tag; 11use Wallabag\CoreBundle\Entity\Tag;
11use Wallabag\UserBundle\Entity\User;
12use OldSound\RabbitMqBundle\RabbitMq\ProducerInterface;
13use Symfony\Component\EventDispatcher\EventDispatcherInterface;
14use Wallabag\CoreBundle\Event\EntrySavedEvent; 12use Wallabag\CoreBundle\Event\EntrySavedEvent;
13use Wallabag\CoreBundle\Helper\ContentProxy;
14use Wallabag\CoreBundle\Helper\TagsAssigner;
15use Wallabag\UserBundle\Entity\User;
15 16
16abstract class AbstractImport implements ImportInterface 17abstract class AbstractImport implements ImportInterface
17{ 18{
18 protected $em; 19 protected $em;
19 protected $logger; 20 protected $logger;
20 protected $contentProxy; 21 protected $contentProxy;
22 protected $tagsAssigner;
21 protected $eventDispatcher; 23 protected $eventDispatcher;
22 protected $producer; 24 protected $producer;
23 protected $user; 25 protected $user;
24 protected $markAsRead; 26 protected $markAsRead;
27 protected $disableContentUpdate = false;
25 protected $skippedEntries = 0; 28 protected $skippedEntries = 0;
26 protected $importedEntries = 0; 29 protected $importedEntries = 0;
27 protected $queuedEntries = 0; 30 protected $queuedEntries = 0;
28 31
29 public function __construct(EntityManager $em, ContentProxy $contentProxy, EventDispatcherInterface $eventDispatcher) 32 public function __construct(EntityManager $em, ContentProxy $contentProxy, TagsAssigner $tagsAssigner, EventDispatcherInterface $eventDispatcher)
30 { 33 {
31 $this->em = $em; 34 $this->em = $em;
32 $this->logger = new NullLogger(); 35 $this->logger = new NullLogger();
33 $this->contentProxy = $contentProxy; 36 $this->contentProxy = $contentProxy;
37 $this->tagsAssigner = $tagsAssigner;
34 $this->eventDispatcher = $eventDispatcher; 38 $this->eventDispatcher = $eventDispatcher;
35 } 39 }
36 40
@@ -82,21 +86,55 @@ abstract class AbstractImport implements ImportInterface
82 } 86 }
83 87
84 /** 88 /**
89 * Set whether articles should be fetched for updated content.
90 *
91 * @param bool $disableContentUpdate
92 */
93 public function setDisableContentUpdate($disableContentUpdate)
94 {
95 $this->disableContentUpdate = $disableContentUpdate;
96
97 return $this;
98 }
99
100 /**
101 * {@inheritdoc}
102 */
103 public function getSummary()
104 {
105 return [
106 'skipped' => $this->skippedEntries,
107 'imported' => $this->importedEntries,
108 'queued' => $this->queuedEntries,
109 ];
110 }
111
112 /**
113 * Parse one entry.
114 *
115 * @param array $importedEntry
116 *
117 * @return Entry
118 */
119 abstract public function parseEntry(array $importedEntry);
120
121 /**
85 * Fetch content from the ContentProxy (using graby). 122 * Fetch content from the ContentProxy (using graby).
86 * If it fails return the given entry to be saved in all case (to avoid user to loose the content). 123 * If it fails return the given entry to be saved in all case (to avoid user to loose the content).
87 * 124 *
88 * @param Entry $entry Entry to update 125 * @param Entry $entry Entry to update
89 * @param string $url Url to grab content for 126 * @param string $url Url to grab content for
90 * @param array $content An array with AT LEAST keys title, html, url, language & content_type to skip the fetchContent from the url 127 * @param array $content An array with AT LEAST keys title, html, url, language & content_type to skip the fetchContent from the url
91 *
92 * @return Entry
93 */ 128 */
94 protected function fetchContent(Entry $entry, $url, array $content = []) 129 protected function fetchContent(Entry $entry, $url, array $content = [])
95 { 130 {
96 try { 131 try {
97 return $this->contentProxy->updateEntry($entry, $url, $content); 132 $this->contentProxy->updateEntry($entry, $url, $content, $this->disableContentUpdate);
98 } catch (\Exception $e) { 133 } catch (\Exception $e) {
99 return $entry; 134 $this->logger->error('Error trying to import an entry.', [
135 'entry_url' => $url,
136 'error_msg' => $e->getMessage(),
137 ]);
100 } 138 }
101 } 139 }
102 140
@@ -127,7 +165,7 @@ abstract class AbstractImport implements ImportInterface
127 $entryToBeFlushed[] = $entry; 165 $entryToBeFlushed[] = $entry;
128 166
129 // flush every 20 entries 167 // flush every 20 entries
130 if (($i % 20) === 0) { 168 if (0 === ($i % 20)) {
131 $this->em->flush(); 169 $this->em->flush();
132 170
133 foreach ($entryToBeFlushed as $entry) { 171 foreach ($entryToBeFlushed as $entry) {
@@ -179,27 +217,6 @@ abstract class AbstractImport implements ImportInterface
179 } 217 }
180 218
181 /** 219 /**
182 * {@inheritdoc}
183 */
184 public function getSummary()
185 {
186 return [
187 'skipped' => $this->skippedEntries,
188 'imported' => $this->importedEntries,
189 'queued' => $this->queuedEntries,
190 ];
191 }
192
193 /**
194 * Parse one entry.
195 *
196 * @param array $importedEntry
197 *
198 * @return Entry
199 */
200 abstract public function parseEntry(array $importedEntry);
201
202 /**
203 * Set current imported entry to archived / read. 220 * Set current imported entry to archived / read.
204 * Implementation is different accross all imports. 221 * Implementation is different accross all imports.
205 * 222 *
diff --git a/src/Wallabag/ImportBundle/Import/BrowserImport.php b/src/Wallabag/ImportBundle/Import/BrowserImport.php
index 8bf7d92e..b5593180 100644
--- a/src/Wallabag/ImportBundle/Import/BrowserImport.php
+++ b/src/Wallabag/ImportBundle/Import/BrowserImport.php
@@ -3,8 +3,6 @@
3namespace Wallabag\ImportBundle\Import; 3namespace Wallabag\ImportBundle\Import;
4 4
5use Wallabag\CoreBundle\Entity\Entry; 5use Wallabag\CoreBundle\Entity\Entry;
6use Wallabag\UserBundle\Entity\User;
7use Wallabag\CoreBundle\Helper\ContentProxy;
8use Wallabag\CoreBundle\Event\EntrySavedEvent; 6use Wallabag\CoreBundle\Event\EntrySavedEvent;
9 7
10abstract class BrowserImport extends AbstractImport 8abstract class BrowserImport extends AbstractImport
@@ -75,6 +73,80 @@ abstract class BrowserImport extends AbstractImport
75 } 73 }
76 74
77 /** 75 /**
76 * {@inheritdoc}
77 */
78 public function parseEntry(array $importedEntry)
79 {
80 if ((!array_key_exists('guid', $importedEntry) || (!array_key_exists('id', $importedEntry))) && is_array(reset($importedEntry))) {
81 if ($this->producer) {
82 $this->parseEntriesForProducer($importedEntry);
83
84 return;
85 }
86
87 $this->parseEntries($importedEntry);
88
89 return;
90 }
91
92 if (array_key_exists('children', $importedEntry)) {
93 if ($this->producer) {
94 $this->parseEntriesForProducer($importedEntry['children']);
95
96 return;
97 }
98
99 $this->parseEntries($importedEntry['children']);
100
101 return;
102 }
103
104 if (!array_key_exists('uri', $importedEntry) && !array_key_exists('url', $importedEntry)) {
105 return;
106 }
107
108 $url = array_key_exists('uri', $importedEntry) ? $importedEntry['uri'] : $importedEntry['url'];
109
110 $existingEntry = $this->em
111 ->getRepository('WallabagCoreBundle:Entry')
112 ->findByUrlAndUserId($url, $this->user->getId());
113
114 if (false !== $existingEntry) {
115 ++$this->skippedEntries;
116
117 return;
118 }
119
120 $data = $this->prepareEntry($importedEntry);
121
122 $entry = new Entry($this->user);
123 $entry->setUrl($data['url']);
124 $entry->setTitle($data['title']);
125
126 // update entry with content (in case fetching failed, the given entry will be return)
127 $this->fetchContent($entry, $data['url'], $data);
128
129 if (array_key_exists('tags', $data)) {
130 $this->tagsAssigner->assignTagsToEntry(
131 $entry,
132 $data['tags']
133 );
134 }
135
136 $entry->setArchived($data['is_archived']);
137
138 if (!empty($data['created_at'])) {
139 $dt = new \DateTime();
140 $entry->setCreatedAt($dt->setTimestamp($data['created_at']));
141 }
142
143 $this->em->persist($entry);
144 ++$this->importedEntries;
145
146 return $entry;
147 }
148
149 /**
78 * Parse and insert all given entries. 150 * Parse and insert all given entries.
79 * 151 *
80 * @param $entries 152 * @param $entries
@@ -99,7 +171,7 @@ abstract class BrowserImport extends AbstractImport
99 $entryToBeFlushed[] = $entry; 171 $entryToBeFlushed[] = $entry;
100 172
101 // flush every 20 entries 173 // flush every 20 entries
102 if (($i % 20) === 0) { 174 if (0 === ($i % 20)) {
103 $this->em->flush(); 175 $this->em->flush();
104 176
105 foreach ($entryToBeFlushed as $entry) { 177 foreach ($entryToBeFlushed as $entry) {
@@ -153,84 +225,12 @@ abstract class BrowserImport extends AbstractImport
153 /** 225 /**
154 * {@inheritdoc} 226 * {@inheritdoc}
155 */ 227 */
156 public function parseEntry(array $importedEntry)
157 {
158 if ((!array_key_exists('guid', $importedEntry) || (!array_key_exists('id', $importedEntry))) && is_array(reset($importedEntry))) {
159 if ($this->producer) {
160 $this->parseEntriesForProducer($importedEntry);
161
162 return;
163 }
164
165 $this->parseEntries($importedEntry);
166
167 return;
168 }
169
170 if (array_key_exists('children', $importedEntry)) {
171 if ($this->producer) {
172 $this->parseEntriesForProducer($importedEntry['children']);
173
174 return;
175 }
176
177 $this->parseEntries($importedEntry['children']);
178
179 return;
180 }
181
182 if (!array_key_exists('uri', $importedEntry) && !array_key_exists('url', $importedEntry)) {
183 return;
184 }
185
186 $url = array_key_exists('uri', $importedEntry) ? $importedEntry['uri'] : $importedEntry['url'];
187
188 $existingEntry = $this->em
189 ->getRepository('WallabagCoreBundle:Entry')
190 ->findByUrlAndUserId($url, $this->user->getId());
191
192 if (false !== $existingEntry) {
193 ++$this->skippedEntries;
194
195 return;
196 }
197
198 $data = $this->prepareEntry($importedEntry);
199
200 $entry = new Entry($this->user);
201 $entry->setUrl($data['url']);
202 $entry->setTitle($data['title']);
203
204 // update entry with content (in case fetching failed, the given entry will be return)
205 $entry = $this->fetchContent($entry, $data['url'], $data);
206
207 if (array_key_exists('tags', $data)) {
208 $this->contentProxy->assignTagsToEntry(
209 $entry,
210 $data['tags']
211 );
212 }
213
214 $entry->setArchived($data['is_archived']);
215
216 if (!empty($data['created_at'])) {
217 $dt = new \DateTime();
218 $entry->setCreatedAt($dt->setTimestamp($data['created_at']));
219 }
220
221 $this->em->persist($entry);
222 ++$this->importedEntries;
223
224 return $entry;
225 }
226
227 /**
228 * {@inheritdoc}
229 */
230 protected function setEntryAsRead(array $importedEntry) 228 protected function setEntryAsRead(array $importedEntry)
231 { 229 {
232 $importedEntry['is_archived'] = 1; 230 $importedEntry['is_archived'] = 1;
233 231
234 return $importedEntry; 232 return $importedEntry;
235 } 233 }
234
235 abstract protected function prepareEntry(array $entry = []);
236} 236}
diff --git a/src/Wallabag/ImportBundle/Import/ChromeImport.php b/src/Wallabag/ImportBundle/Import/ChromeImport.php
index 2667890f..09183abe 100644
--- a/src/Wallabag/ImportBundle/Import/ChromeImport.php
+++ b/src/Wallabag/ImportBundle/Import/ChromeImport.php
@@ -45,7 +45,7 @@ class ChromeImport extends BrowserImport
45 'created_at' => substr($entry['date_added'], 0, 10), 45 'created_at' => substr($entry['date_added'], 0, 10),
46 ]; 46 ];
47 47
48 if (array_key_exists('tags', $entry) && $entry['tags'] != '') { 48 if (array_key_exists('tags', $entry) && '' !== $entry['tags']) {
49 $data['tags'] = $entry['tags']; 49 $data['tags'] = $entry['tags'];
50 } 50 }
51 51
diff --git a/src/Wallabag/ImportBundle/Import/FirefoxImport.php b/src/Wallabag/ImportBundle/Import/FirefoxImport.php
index c50c69b3..73269fe1 100644
--- a/src/Wallabag/ImportBundle/Import/FirefoxImport.php
+++ b/src/Wallabag/ImportBundle/Import/FirefoxImport.php
@@ -45,7 +45,7 @@ class FirefoxImport extends BrowserImport
45 'created_at' => substr($entry['dateAdded'], 0, 10), 45 'created_at' => substr($entry['dateAdded'], 0, 10),
46 ]; 46 ];
47 47
48 if (array_key_exists('tags', $entry) && $entry['tags'] != '') { 48 if (array_key_exists('tags', $entry) && '' !== $entry['tags']) {
49 $data['tags'] = $entry['tags']; 49 $data['tags'] = $entry['tags'];
50 } 50 }
51 51
diff --git a/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php b/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php
index a363a566..d7df0a83 100644
--- a/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php
+++ b/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\ImportBundle\Import; 3namespace Wallabag\ImportBundle\Import;
4 4
5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 5use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
6use Symfony\Component\DependencyInjection\ContainerBuilder;
7use Symfony\Component\DependencyInjection\Reference; 7use Symfony\Component\DependencyInjection\Reference;
8 8
9class ImportCompilerPass implements CompilerPassInterface 9class ImportCompilerPass implements CompilerPassInterface
diff --git a/src/Wallabag/ImportBundle/Import/InstapaperImport.php b/src/Wallabag/ImportBundle/Import/InstapaperImport.php
index 70a53f1a..7ab69e7a 100644
--- a/src/Wallabag/ImportBundle/Import/InstapaperImport.php
+++ b/src/Wallabag/ImportBundle/Import/InstapaperImport.php
@@ -63,18 +63,27 @@ class InstapaperImport extends AbstractImport
63 63
64 $entries = []; 64 $entries = [];
65 $handle = fopen($this->filepath, 'r'); 65 $handle = fopen($this->filepath, 'r');
66 while (($data = fgetcsv($handle, 10240)) !== false) { 66 while (false !== ($data = fgetcsv($handle, 10240))) {
67 if ('URL' === $data[0]) { 67 if ('URL' === $data[0]) {
68 continue; 68 continue;
69 } 69 }
70 70
71 // last element in the csv is the folder where the content belong
72 // BUT it can also be the status (since status = folder in Instapaper)
73 // and we don't want archive, unread & starred to become a tag
74 $tags = null;
75 if (false === in_array($data[3], ['Archive', 'Unread', 'Starred'], true)) {
76 $tags = [$data[3]];
77 }
78
71 $entries[] = [ 79 $entries[] = [
72 'url' => $data[0], 80 'url' => $data[0],
73 'title' => $data[1], 81 'title' => $data[1],
74 'status' => $data[3], 82 'status' => $data[3],
75 'is_archived' => $data[3] === 'Archive' || $data[3] === 'Starred', 83 'is_archived' => 'Archive' === $data[3] || 'Starred' === $data[3],
76 'is_starred' => $data[3] === 'Starred', 84 'is_starred' => 'Starred' === $data[3],
77 'html' => false, 85 'html' => false,
86 'tags' => $tags,
78 ]; 87 ];
79 } 88 }
80 fclose($handle); 89 fclose($handle);
@@ -116,7 +125,15 @@ class InstapaperImport extends AbstractImport
116 $entry->setTitle($importedEntry['title']); 125 $entry->setTitle($importedEntry['title']);
117 126
118 // update entry with content (in case fetching failed, the given entry will be return) 127 // update entry with content (in case fetching failed, the given entry will be return)
119 $entry = $this->fetchContent($entry, $importedEntry['url'], $importedEntry); 128 $this->fetchContent($entry, $importedEntry['url'], $importedEntry);
129
130 if (!empty($importedEntry['tags'])) {
131 $this->tagsAssigner->assignTagsToEntry(
132 $entry,
133 $importedEntry['tags'],
134 $this->em->getUnitOfWork()->getScheduledEntityInsertions()
135 );
136 }
120 137
121 $entry->setArchived($importedEntry['is_archived']); 138 $entry->setArchived($importedEntry['is_archived']);
122 $entry->setStarred($importedEntry['is_starred']); 139 $entry->setStarred($importedEntry['is_starred']);
diff --git a/src/Wallabag/ImportBundle/Import/PinboardImport.php b/src/Wallabag/ImportBundle/Import/PinboardImport.php
index d9865534..110b0464 100644
--- a/src/Wallabag/ImportBundle/Import/PinboardImport.php
+++ b/src/Wallabag/ImportBundle/Import/PinboardImport.php
@@ -109,10 +109,10 @@ class PinboardImport extends AbstractImport
109 $entry->setTitle($data['title']); 109 $entry->setTitle($data['title']);
110 110
111 // update entry with content (in case fetching failed, the given entry will be return) 111 // update entry with content (in case fetching failed, the given entry will be return)
112 $entry = $this->fetchContent($entry, $data['url'], $data); 112 $this->fetchContent($entry, $data['url'], $data);
113 113
114 if (!empty($data['tags'])) { 114 if (!empty($data['tags'])) {
115 $this->contentProxy->assignTagsToEntry( 115 $this->tagsAssigner->assignTagsToEntry(
116 $entry, 116 $entry,
117 $data['tags'], 117 $data['tags'],
118 $this->em->getUnitOfWork()->getScheduledEntityInsertions() 118 $this->em->getUnitOfWork()->getScheduledEntityInsertions()
diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php
index 33093480..dddb87f4 100644
--- a/src/Wallabag/ImportBundle/Import/PocketImport.php
+++ b/src/Wallabag/ImportBundle/Import/PocketImport.php
@@ -5,15 +5,13 @@ namespace Wallabag\ImportBundle\Import;
5use GuzzleHttp\Client; 5use GuzzleHttp\Client;
6use GuzzleHttp\Exception\RequestException; 6use GuzzleHttp\Exception\RequestException;
7use Wallabag\CoreBundle\Entity\Entry; 7use Wallabag\CoreBundle\Entity\Entry;
8use Wallabag\CoreBundle\Helper\ContentProxy;
9 8
10class PocketImport extends AbstractImport 9class PocketImport extends AbstractImport
11{ 10{
11 const NB_ELEMENTS = 5000;
12 private $client; 12 private $client;
13 private $accessToken; 13 private $accessToken;
14 14
15 const NB_ELEMENTS = 5000;
16
17 /** 15 /**
18 * Only used for test purpose. 16 * Only used for test purpose.
19 * 17 *
@@ -151,7 +149,7 @@ class PocketImport extends AbstractImport
151 // - first call get 5k offset 0 149 // - first call get 5k offset 0
152 // - second call get 5k offset 5k 150 // - second call get 5k offset 5k
153 // - and so on 151 // - and so on
154 if (count($entries['list']) === self::NB_ELEMENTS) { 152 if (self::NB_ELEMENTS === count($entries['list'])) {
155 ++$run; 153 ++$run;
156 154
157 return $this->import(self::NB_ELEMENTS * $run); 155 return $this->import(self::NB_ELEMENTS * $run);
@@ -177,7 +175,7 @@ class PocketImport extends AbstractImport
177 */ 175 */
178 public function parseEntry(array $importedEntry) 176 public function parseEntry(array $importedEntry)
179 { 177 {
180 $url = isset($importedEntry['resolved_url']) && $importedEntry['resolved_url'] != '' ? $importedEntry['resolved_url'] : $importedEntry['given_url']; 178 $url = isset($importedEntry['resolved_url']) && '' !== $importedEntry['resolved_url'] ? $importedEntry['resolved_url'] : $importedEntry['given_url'];
181 179
182 $existingEntry = $this->em 180 $existingEntry = $this->em
183 ->getRepository('WallabagCoreBundle:Entry') 181 ->getRepository('WallabagCoreBundle:Entry')
@@ -193,18 +191,18 @@ class PocketImport extends AbstractImport
193 $entry->setUrl($url); 191 $entry->setUrl($url);
194 192
195 // update entry with content (in case fetching failed, the given entry will be return) 193 // update entry with content (in case fetching failed, the given entry will be return)
196 $entry = $this->fetchContent($entry, $url); 194 $this->fetchContent($entry, $url);
197 195
198 // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted 196 // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted
199 $entry->setArchived($importedEntry['status'] == 1 || $this->markAsRead); 197 $entry->setArchived(1 === $importedEntry['status'] || $this->markAsRead);
200 198
201 // 0 or 1 - 1 If the item is starred 199 // 0 or 1 - 1 If the item is starred
202 $entry->setStarred($importedEntry['favorite'] == 1); 200 $entry->setStarred(1 === $importedEntry['favorite']);
203 201
204 $title = 'Untitled'; 202 $title = 'Untitled';
205 if (isset($importedEntry['resolved_title']) && $importedEntry['resolved_title'] != '') { 203 if (isset($importedEntry['resolved_title']) && '' !== $importedEntry['resolved_title']) {
206 $title = $importedEntry['resolved_title']; 204 $title = $importedEntry['resolved_title'];
207 } elseif (isset($importedEntry['given_title']) && $importedEntry['given_title'] != '') { 205 } elseif (isset($importedEntry['given_title']) && '' !== $importedEntry['given_title']) {
208 $title = $importedEntry['given_title']; 206 $title = $importedEntry['given_title'];
209 } 207 }
210 208
@@ -216,7 +214,7 @@ class PocketImport extends AbstractImport
216 } 214 }
217 215
218 if (isset($importedEntry['tags']) && !empty($importedEntry['tags'])) { 216 if (isset($importedEntry['tags']) && !empty($importedEntry['tags'])) {
219 $this->contentProxy->assignTagsToEntry( 217 $this->tagsAssigner->assignTagsToEntry(
220 $entry, 218 $entry,
221 array_keys($importedEntry['tags']), 219 array_keys($importedEntry['tags']),
222 $this->em->getUnitOfWork()->getScheduledEntityInsertions() 220 $this->em->getUnitOfWork()->getScheduledEntityInsertions()
diff --git a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php
index de320d23..002b27f4 100644
--- a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php
+++ b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php
@@ -109,7 +109,7 @@ class ReadabilityImport extends AbstractImport
109 $entry->setTitle($data['title']); 109 $entry->setTitle($data['title']);
110 110
111 // update entry with content (in case fetching failed, the given entry will be return) 111 // update entry with content (in case fetching failed, the given entry will be return)
112 $entry = $this->fetchContent($entry, $data['url'], $data); 112 $this->fetchContent($entry, $data['url'], $data);
113 113
114 $entry->setArchived($data['is_archived']); 114 $entry->setArchived($data['is_archived']);
115 $entry->setStarred($data['is_starred']); 115 $entry->setStarred($data['is_starred']);
diff --git a/src/Wallabag/ImportBundle/Import/WallabagImport.php b/src/Wallabag/ImportBundle/Import/WallabagImport.php
index 702da057..c64ccd64 100644
--- a/src/Wallabag/ImportBundle/Import/WallabagImport.php
+++ b/src/Wallabag/ImportBundle/Import/WallabagImport.php
@@ -108,10 +108,10 @@ abstract class WallabagImport extends AbstractImport
108 $entry->setTitle($data['title']); 108 $entry->setTitle($data['title']);
109 109
110 // update entry with content (in case fetching failed, the given entry will be return) 110 // update entry with content (in case fetching failed, the given entry will be return)
111 $entry = $this->fetchContent($entry, $data['url'], $data); 111 $this->fetchContent($entry, $data['url'], $data);
112 112
113 if (array_key_exists('tags', $data)) { 113 if (array_key_exists('tags', $data)) {
114 $this->contentProxy->assignTagsToEntry( 114 $this->tagsAssigner->assignTagsToEntry(
115 $entry, 115 $entry,
116 $data['tags'], 116 $data['tags'],
117 $this->em->getUnitOfWork()->getScheduledEntityInsertions() 117 $this->em->getUnitOfWork()->getScheduledEntityInsertions()
diff --git a/src/Wallabag/ImportBundle/Import/WallabagV1Import.php b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php
index 59e3ce02..a35c411e 100644
--- a/src/Wallabag/ImportBundle/Import/WallabagV1Import.php
+++ b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php
@@ -4,6 +4,17 @@ namespace Wallabag\ImportBundle\Import;
4 4
5class WallabagV1Import extends WallabagImport 5class WallabagV1Import extends WallabagImport
6{ 6{
7 protected $fetchingErrorMessage;
8 protected $fetchingErrorMessageTitle;
9
10 public function __construct($em, $contentProxy, $tagsAssigner, $eventDispatcher, $fetchingErrorMessageTitle, $fetchingErrorMessage)
11 {
12 $this->fetchingErrorMessageTitle = $fetchingErrorMessageTitle;
13 $this->fetchingErrorMessage = $fetchingErrorMessage;
14
15 parent::__construct($em, $contentProxy, $tagsAssigner, $eventDispatcher);
16 }
17
7 /** 18 /**
8 * {@inheritdoc} 19 * {@inheritdoc}
9 */ 20 */
@@ -43,13 +54,14 @@ class WallabagV1Import extends WallabagImport
43 'created_at' => '', 54 'created_at' => '',
44 ]; 55 ];
45 56
46 // force content to be refreshed in case on bad fetch in the v1 installation 57 // In case of a bad fetch in v1, replace title and content with v2 error strings
47 if (in_array($entry['title'], $this->untitled)) { 58 // If fetching fails again, they will get this instead of the v1 strings
48 $data['title'] = ''; 59 if (in_array($entry['title'], $this->untitled, true)) {
49 $data['html'] = ''; 60 $data['title'] = $this->fetchingErrorMessageTitle;
61 $data['html'] = $this->fetchingErrorMessage;
50 } 62 }
51 63
52 if (array_key_exists('tags', $entry) && $entry['tags'] != '') { 64 if (array_key_exists('tags', $entry) && '' !== $entry['tags']) {
53 $data['tags'] = $entry['tags']; 65 $data['tags'] = $entry['tags'];
54 } 66 }
55 67
diff --git a/src/Wallabag/ImportBundle/Import/WallabagV2Import.php b/src/Wallabag/ImportBundle/Import/WallabagV2Import.php
index d2a89d79..3e085ecf 100644
--- a/src/Wallabag/ImportBundle/Import/WallabagV2Import.php
+++ b/src/Wallabag/ImportBundle/Import/WallabagV2Import.php
@@ -36,8 +36,8 @@ class WallabagV2Import extends WallabagImport
36 return [ 36 return [
37 'html' => $entry['content'], 37 'html' => $entry['content'],
38 'content_type' => $entry['mimetype'], 38 'content_type' => $entry['mimetype'],
39 'is_archived' => (int) ($entry['is_archived'] || $this->markAsRead), 39 'is_archived' => (bool) ($entry['is_archived'] || $this->markAsRead),
40 'is_starred' => false, 40 'is_starred' => (bool) $entry['is_starred'],
41 ] + $entry; 41 ] + $entry;
42 } 42 }
43 43
diff --git a/src/Wallabag/ImportBundle/Redis/Producer.php b/src/Wallabag/ImportBundle/Redis/Producer.php
index fedc3e57..c77b5174 100644
--- a/src/Wallabag/ImportBundle/Redis/Producer.php
+++ b/src/Wallabag/ImportBundle/Redis/Producer.php
@@ -29,7 +29,7 @@ class Producer implements ProducerInterface
29 * @param string $routingKey NOT USED 29 * @param string $routingKey NOT USED
30 * @param array $additionalProperties NOT USED 30 * @param array $additionalProperties NOT USED
31 */ 31 */
32 public function publish($msgBody, $routingKey = '', $additionalProperties = array()) 32 public function publish($msgBody, $routingKey = '', $additionalProperties = [])
33 { 33 {
34 $this->queue->sendJob($msgBody); 34 $this->queue->sendJob($msgBody);
35 } 35 }
diff --git a/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml
index c4fe3f92..b224a6a2 100644
--- a/src/Wallabag/ImportBundle/Resources/config/services.yml
+++ b/src/Wallabag/ImportBundle/Resources/config/services.yml
@@ -20,6 +20,7 @@ services:
20 arguments: 20 arguments:
21 - "@doctrine.orm.entity_manager" 21 - "@doctrine.orm.entity_manager"
22 - "@wallabag_core.content_proxy" 22 - "@wallabag_core.content_proxy"
23 - "@wallabag_core.tags_assigner"
23 - "@event_dispatcher" 24 - "@event_dispatcher"
24 calls: 25 calls:
25 - [ setClient, [ "@wallabag_import.pocket.client" ] ] 26 - [ setClient, [ "@wallabag_import.pocket.client" ] ]
@@ -32,7 +33,10 @@ services:
32 arguments: 33 arguments:
33 - "@doctrine.orm.entity_manager" 34 - "@doctrine.orm.entity_manager"
34 - "@wallabag_core.content_proxy" 35 - "@wallabag_core.content_proxy"
36 - "@wallabag_core.tags_assigner"
35 - "@event_dispatcher" 37 - "@event_dispatcher"
38 - "%wallabag_core.fetching_error_message_title%"
39 - "%wallabag_core.fetching_error_message%"
36 calls: 40 calls:
37 - [ setLogger, [ "@logger" ]] 41 - [ setLogger, [ "@logger" ]]
38 tags: 42 tags:
@@ -43,6 +47,7 @@ services:
43 arguments: 47 arguments:
44 - "@doctrine.orm.entity_manager" 48 - "@doctrine.orm.entity_manager"
45 - "@wallabag_core.content_proxy" 49 - "@wallabag_core.content_proxy"
50 - "@wallabag_core.tags_assigner"
46 - "@event_dispatcher" 51 - "@event_dispatcher"
47 calls: 52 calls:
48 - [ setLogger, [ "@logger" ]] 53 - [ setLogger, [ "@logger" ]]
@@ -54,6 +59,7 @@ services:
54 arguments: 59 arguments:
55 - "@doctrine.orm.entity_manager" 60 - "@doctrine.orm.entity_manager"
56 - "@wallabag_core.content_proxy" 61 - "@wallabag_core.content_proxy"
62 - "@wallabag_core.tags_assigner"
57 - "@event_dispatcher" 63 - "@event_dispatcher"
58 calls: 64 calls:
59 - [ setLogger, [ "@logger" ]] 65 - [ setLogger, [ "@logger" ]]
@@ -65,6 +71,7 @@ services:
65 arguments: 71 arguments:
66 - "@doctrine.orm.entity_manager" 72 - "@doctrine.orm.entity_manager"
67 - "@wallabag_core.content_proxy" 73 - "@wallabag_core.content_proxy"
74 - "@wallabag_core.tags_assigner"
68 - "@event_dispatcher" 75 - "@event_dispatcher"
69 calls: 76 calls:
70 - [ setLogger, [ "@logger" ]] 77 - [ setLogger, [ "@logger" ]]
@@ -76,6 +83,7 @@ services:
76 arguments: 83 arguments:
77 - "@doctrine.orm.entity_manager" 84 - "@doctrine.orm.entity_manager"
78 - "@wallabag_core.content_proxy" 85 - "@wallabag_core.content_proxy"
86 - "@wallabag_core.tags_assigner"
79 - "@event_dispatcher" 87 - "@event_dispatcher"
80 calls: 88 calls:
81 - [ setLogger, [ "@logger" ]] 89 - [ setLogger, [ "@logger" ]]
@@ -87,6 +95,7 @@ services:
87 arguments: 95 arguments:
88 - "@doctrine.orm.entity_manager" 96 - "@doctrine.orm.entity_manager"
89 - "@wallabag_core.content_proxy" 97 - "@wallabag_core.content_proxy"
98 - "@wallabag_core.tags_assigner"
90 - "@event_dispatcher" 99 - "@event_dispatcher"
91 calls: 100 calls:
92 - [ setLogger, [ "@logger" ]] 101 - [ setLogger, [ "@logger" ]]
@@ -97,6 +106,7 @@ services:
97 arguments: 106 arguments:
98 - "@doctrine.orm.entity_manager" 107 - "@doctrine.orm.entity_manager"
99 - "@wallabag_core.content_proxy" 108 - "@wallabag_core.content_proxy"
109 - "@wallabag_core.tags_assigner"
100 - "@event_dispatcher" 110 - "@event_dispatcher"
101 calls: 111 calls:
102 - [ setLogger, [ "@logger" ]] 112 - [ setLogger, [ "@logger" ]]
diff --git a/src/Wallabag/ImportBundle/WallabagImportBundle.php b/src/Wallabag/ImportBundle/WallabagImportBundle.php
index a5ddc1b4..98c2f97b 100644
--- a/src/Wallabag/ImportBundle/WallabagImportBundle.php
+++ b/src/Wallabag/ImportBundle/WallabagImportBundle.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\ImportBundle; 3namespace Wallabag\ImportBundle;
4 4
5use Symfony\Component\HttpKernel\Bundle\Bundle;
6use Symfony\Component\DependencyInjection\ContainerBuilder; 5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Symfony\Component\HttpKernel\Bundle\Bundle;
7use Wallabag\ImportBundle\Import\ImportCompilerPass; 7use Wallabag\ImportBundle\Import\ImportCompilerPass;
8 8
9class WallabagImportBundle extends Bundle 9class WallabagImportBundle extends Bundle
diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php
index 92ee2b41..f3de656f 100644
--- a/src/Wallabag/UserBundle/Controller/ManageController.php
+++ b/src/Wallabag/UserBundle/Controller/ManageController.php
@@ -4,12 +4,15 @@ namespace Wallabag\UserBundle\Controller;
4 4
5use FOS\UserBundle\Event\UserEvent; 5use FOS\UserBundle\Event\UserEvent;
6use FOS\UserBundle\FOSUserEvents; 6use FOS\UserBundle\FOSUserEvents;
7use Symfony\Component\HttpFoundation\Request; 7use Pagerfanta\Adapter\DoctrineORMAdapter;
8use Symfony\Bundle\FrameworkBundle\Controller\Controller; 8use Pagerfanta\Exception\OutOfRangeCurrentPageException;
9use Pagerfanta\Pagerfanta;
9use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 10use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
10use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 11use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
12use Symfony\Bundle\FrameworkBundle\Controller\Controller;
13use Symfony\Component\HttpFoundation\Request;
11use Wallabag\UserBundle\Entity\User; 14use Wallabag\UserBundle\Entity\User;
12use Wallabag\CoreBundle\Entity\Config; 15use Wallabag\UserBundle\Form\SearchUserType;
13 16
14/** 17/**
15 * User controller. 18 * User controller.
@@ -17,23 +20,6 @@ use Wallabag\CoreBundle\Entity\Config;
17class ManageController extends Controller 20class ManageController extends Controller
18{ 21{
19 /** 22 /**
20 * Lists all User entities.
21 *
22 * @Route("/", name="user_index")
23 * @Method("GET")
24 */
25 public function indexAction()
26 {
27 $em = $this->getDoctrine()->getManager();
28
29 $users = $em->getRepository('WallabagUserBundle:User')->findAll();
30
31 return $this->render('WallabagUserBundle:Manage:index.html.twig', array(
32 'users' => $users,
33 ));
34 }
35
36 /**
37 * Creates a new User entity. 23 * Creates a new User entity.
38 * 24 *
39 * @Route("/new", name="user_new") 25 * @Route("/new", name="user_new")
@@ -47,9 +33,7 @@ class ManageController extends Controller
47 // enable created user by default 33 // enable created user by default
48 $user->setEnabled(true); 34 $user->setEnabled(true);
49 35
50 $form = $this->createForm('Wallabag\UserBundle\Form\NewUserType', $user, [ 36 $form = $this->createForm('Wallabag\UserBundle\Form\NewUserType', $user);
51 'validation_groups' => ['Profile'],
52 ]);
53 $form->handleRequest($request); 37 $form->handleRequest($request);
54 38
55 if ($form->isSubmitted() && $form->isValid()) { 39 if ($form->isSubmitted() && $form->isValid()) {
@@ -64,13 +48,13 @@ class ManageController extends Controller
64 $this->get('translator')->trans('flashes.user.notice.added', ['%username%' => $user->getUsername()]) 48 $this->get('translator')->trans('flashes.user.notice.added', ['%username%' => $user->getUsername()])
65 ); 49 );
66 50
67 return $this->redirectToRoute('user_edit', array('id' => $user->getId())); 51 return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
68 } 52 }
69 53
70 return $this->render('WallabagUserBundle:Manage:new.html.twig', array( 54 return $this->render('WallabagUserBundle:Manage:new.html.twig', [
71 'user' => $user, 55 'user' => $user,
72 'form' => $form->createView(), 56 'form' => $form->createView(),
73 )); 57 ]);
74 } 58 }
75 59
76 /** 60 /**
@@ -95,15 +79,15 @@ class ManageController extends Controller
95 $this->get('translator')->trans('flashes.user.notice.updated', ['%username%' => $user->getUsername()]) 79 $this->get('translator')->trans('flashes.user.notice.updated', ['%username%' => $user->getUsername()])
96 ); 80 );
97 81
98 return $this->redirectToRoute('user_edit', array('id' => $user->getId())); 82 return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
99 } 83 }
100 84
101 return $this->render('WallabagUserBundle:Manage:edit.html.twig', array( 85 return $this->render('WallabagUserBundle:Manage:edit.html.twig', [
102 'user' => $user, 86 'user' => $user,
103 'edit_form' => $editForm->createView(), 87 'edit_form' => $editForm->createView(),
104 'delete_form' => $deleteForm->createView(), 88 'delete_form' => $deleteForm->createView(),
105 'twofactor_auth' => $this->getParameter('twofactor_auth'), 89 'twofactor_auth' => $this->getParameter('twofactor_auth'),
106 )); 90 ]);
107 } 91 }
108 92
109 /** 93 /**
@@ -132,6 +116,51 @@ class ManageController extends Controller
132 } 116 }
133 117
134 /** 118 /**
119 * @param Request $request
120 * @param int $page
121 *
122 * @Route("/list/{page}", name="user_index", defaults={"page" = 1})
123 *
124 * Default parameter for page is hardcoded (in duplication of the defaults from the Route)
125 * because this controller is also called inside the layout template without any page as argument
126 *
127 * @return \Symfony\Component\HttpFoundation\Response
128 */
129 public function searchFormAction(Request $request, $page = 1)
130 {
131 $em = $this->getDoctrine()->getManager();
132 $qb = $em->getRepository('WallabagUserBundle:User')->createQueryBuilder('u');
133
134 $form = $this->createForm(SearchUserType::class);
135 $form->handleRequest($request);
136
137 if ($form->isSubmitted() && $form->isValid()) {
138 $this->get('logger')->info('searching users');
139
140 $searchTerm = (isset($request->get('search_user')['term']) ? $request->get('search_user')['term'] : '');
141
142 $qb = $em->getRepository('WallabagUserBundle:User')->getQueryBuilderForSearch($searchTerm);
143 }
144
145 $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
146 $pagerFanta = new Pagerfanta($pagerAdapter);
147 $pagerFanta->setMaxPerPage(50);
148
149 try {
150 $pagerFanta->setCurrentPage($page);
151 } catch (OutOfRangeCurrentPageException $e) {
152 if ($page > 1) {
153 return $this->redirect($this->generateUrl('user_index', ['page' => $pagerFanta->getNbPages()]), 302);
154 }
155 }
156
157 return $this->render('WallabagUserBundle:Manage:index.html.twig', [
158 'searchForm' => $form->createView(),
159 'users' => $pagerFanta,
160 ]);
161 }
162
163 /**
135 * Creates a form to delete a User entity. 164 * Creates a form to delete a User entity.
136 * 165 *
137 * @param User $user The User entity 166 * @param User $user The User entity
@@ -141,7 +170,7 @@ class ManageController extends Controller
141 private function createDeleteForm(User $user) 170 private function createDeleteForm(User $user)
142 { 171 {
143 return $this->createFormBuilder() 172 return $this->createFormBuilder()
144 ->setAction($this->generateUrl('user_delete', array('id' => $user->getId()))) 173 ->setAction($this->generateUrl('user_delete', ['id' => $user->getId()]))
145 ->setMethod('DELETE') 174 ->setMethod('DELETE')
146 ->getForm() 175 ->getForm()
147 ; 176 ;
diff --git a/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php b/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php
index 99040f69..5ca3482e 100644
--- a/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php
+++ b/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php
@@ -2,10 +2,10 @@
2 2
3namespace Wallabag\UserBundle\DependencyInjection; 3namespace Wallabag\UserBundle\DependencyInjection;
4 4
5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Symfony\Component\Config\FileLocator; 5use Symfony\Component\Config\FileLocator;
7use Symfony\Component\HttpKernel\DependencyInjection\Extension; 6use Symfony\Component\DependencyInjection\ContainerBuilder;
8use Symfony\Component\DependencyInjection\Loader; 7use Symfony\Component\DependencyInjection\Loader;
8use Symfony\Component\HttpKernel\DependencyInjection\Extension;
9 9
10class WallabagUserExtension extends Extension 10class WallabagUserExtension extends Extension
11{ 11{
@@ -14,7 +14,7 @@ class WallabagUserExtension extends Extension
14 $configuration = new Configuration(); 14 $configuration = new Configuration();
15 $config = $this->processConfiguration($configuration, $configs); 15 $config = $this->processConfiguration($configuration, $configs);
16 16
17 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 17 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
18 $loader->load('services.yml'); 18 $loader->load('services.yml');
19 $container->setParameter('wallabag_user.registration_enabled', $config['registration_enabled']); 19 $container->setParameter('wallabag_user.registration_enabled', $config['registration_enabled']);
20 } 20 }
diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php
index 3a167de7..48446e3c 100644
--- a/src/Wallabag/UserBundle/Entity/User.php
+++ b/src/Wallabag/UserBundle/Entity/User.php
@@ -4,37 +4,43 @@ namespace Wallabag\UserBundle\Entity;
4 4
5use Doctrine\Common\Collections\ArrayCollection; 5use Doctrine\Common\Collections\ArrayCollection;
6use Doctrine\ORM\Mapping as ORM; 6use Doctrine\ORM\Mapping as ORM;
7use FOS\UserBundle\Model\User as BaseUser;
8use JMS\Serializer\Annotation\Accessor;
9use JMS\Serializer\Annotation\Groups;
10use JMS\Serializer\Annotation\XmlRoot;
7use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface; 11use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
8use Scheb\TwoFactorBundle\Model\TrustedComputerInterface; 12use Scheb\TwoFactorBundle\Model\TrustedComputerInterface;
9use FOS\UserBundle\Model\User as BaseUser;
10use JMS\Serializer\Annotation\ExclusionPolicy;
11use JMS\Serializer\Annotation\Expose;
12use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; 13use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
13use Symfony\Component\Security\Core\User\UserInterface; 14use Symfony\Component\Security\Core\User\UserInterface;
14use Wallabag\ApiBundle\Entity\Client; 15use Wallabag\ApiBundle\Entity\Client;
15use Wallabag\CoreBundle\Entity\Config; 16use Wallabag\CoreBundle\Entity\Config;
16use Wallabag\CoreBundle\Entity\Entry; 17use Wallabag\CoreBundle\Entity\Entry;
18use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
17 19
18/** 20/**
19 * User. 21 * User.
20 * 22 *
23 * @XmlRoot("user")
21 * @ORM\Entity(repositoryClass="Wallabag\UserBundle\Repository\UserRepository") 24 * @ORM\Entity(repositoryClass="Wallabag\UserBundle\Repository\UserRepository")
22 * @ORM\Table(name="`user`") 25 * @ORM\Table(name="`user`")
23 * @ORM\HasLifecycleCallbacks() 26 * @ORM\HasLifecycleCallbacks()
24 * @ExclusionPolicy("all")
25 * 27 *
26 * @UniqueEntity("email") 28 * @UniqueEntity("email")
27 * @UniqueEntity("username") 29 * @UniqueEntity("username")
28 */ 30 */
29class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface 31class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface
30{ 32{
33 use EntityTimestampsTrait;
34
35 /** @Serializer\XmlAttribute */
31 /** 36 /**
32 * @var int 37 * @var int
33 * 38 *
34 * @Expose
35 * @ORM\Column(name="id", type="integer") 39 * @ORM\Column(name="id", type="integer")
36 * @ORM\Id 40 * @ORM\Id
37 * @ORM\GeneratedValue(strategy="AUTO") 41 * @ORM\GeneratedValue(strategy="AUTO")
42 *
43 * @Groups({"user_api", "user_api_with_client"})
38 */ 44 */
39 protected $id; 45 protected $id;
40 46
@@ -42,20 +48,40 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
42 * @var string 48 * @var string
43 * 49 *
44 * @ORM\Column(name="name", type="text", nullable=true) 50 * @ORM\Column(name="name", type="text", nullable=true)
51 *
52 * @Groups({"user_api", "user_api_with_client"})
45 */ 53 */
46 protected $name; 54 protected $name;
47 55
48 /** 56 /**
49 * @var date 57 * @var string
58 *
59 * @Groups({"user_api", "user_api_with_client"})
60 */
61 protected $username;
62
63 /**
64 * @var string
65 *
66 * @Groups({"user_api", "user_api_with_client"})
67 */
68 protected $email;
69
70 /**
71 * @var \DateTime
50 * 72 *
51 * @ORM\Column(name="created_at", type="datetime") 73 * @ORM\Column(name="created_at", type="datetime")
74 *
75 * @Groups({"user_api", "user_api_with_client"})
52 */ 76 */
53 protected $createdAt; 77 protected $createdAt;
54 78
55 /** 79 /**
56 * @var date 80 * @var \DateTime
57 * 81 *
58 * @ORM\Column(name="updated_at", type="datetime") 82 * @ORM\Column(name="updated_at", type="datetime")
83 *
84 * @Groups({"user_api", "user_api_with_client"})
59 */ 85 */
60 protected $updatedAt; 86 protected $updatedAt;
61 87
@@ -70,12 +96,35 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
70 protected $config; 96 protected $config;
71 97
72 /** 98 /**
99 * @var ArrayCollection
100 *
101 * @ORM\OneToMany(targetEntity="Wallabag\CoreBundle\Entity\SiteCredential", mappedBy="user", cascade={"remove"})
102 */
103 protected $siteCredentials;
104
105 /**
106 * @var ArrayCollection
107 *
108 * @ORM\OneToMany(targetEntity="Wallabag\ApiBundle\Entity\Client", mappedBy="user", cascade={"remove"})
109 */
110 protected $clients;
111
112 /**
113 * @see getFirstClient() below
114 *
115 * @Groups({"user_api_with_client"})
116 * @Accessor(getter="getFirstClient")
117 */
118 protected $default_client;
119
120 /**
73 * @ORM\Column(type="integer", nullable=true) 121 * @ORM\Column(type="integer", nullable=true)
74 */ 122 */
75 private $authCode; 123 private $authCode;
76 124
77 /** 125 /**
78 * @var bool Enabled yes/no 126 * @var bool
127 *
79 * @ORM\Column(type="boolean") 128 * @ORM\Column(type="boolean")
80 */ 129 */
81 private $twoFactorAuthentication = false; 130 private $twoFactorAuthentication = false;
@@ -85,11 +134,6 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
85 */ 134 */
86 private $trusted; 135 private $trusted;
87 136
88 /**
89 * @ORM\OneToMany(targetEntity="Wallabag\ApiBundle\Entity\Client", mappedBy="user", cascade={"remove"})
90 */
91 protected $clients;
92
93 public function __construct() 137 public function __construct()
94 { 138 {
95 parent::__construct(); 139 parent::__construct();
@@ -98,19 +142,6 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
98 } 142 }
99 143
100 /** 144 /**
101 * @ORM\PrePersist
102 * @ORM\PreUpdate
103 */
104 public function timestamps()
105 {
106 if (is_null($this->createdAt)) {
107 $this->createdAt = new \DateTime();
108 }
109
110 $this->updatedAt = new \DateTime();
111 }
112
113 /**
114 * Set name. 145 * Set name.
115 * 146 *
116 * @param string $name 147 * @param string $name
@@ -135,7 +166,7 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
135 } 166 }
136 167
137 /** 168 /**
138 * @return string 169 * @return \DateTime
139 */ 170 */
140 public function getCreatedAt() 171 public function getCreatedAt()
141 { 172 {
@@ -143,7 +174,7 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
143 } 174 }
144 175
145 /** 176 /**
146 * @return string 177 * @return \DateTime
147 */ 178 */
148 public function getUpdatedAt() 179 public function getUpdatedAt()
149 { 180 {
@@ -266,4 +297,16 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
266 { 297 {
267 return $this->clients; 298 return $this->clients;
268 } 299 }
300
301 /**
302 * 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).
303 *
304 * @return Client
305 */
306 public function getFirstClient()
307 {
308 if (!empty($this->clients)) {
309 return $this->clients->first();
310 }
311 }
269} 312}
diff --git a/src/Wallabag/UserBundle/EventListener/AuthenticationFailureListener.php b/src/Wallabag/UserBundle/EventListener/AuthenticationFailureListener.php
new file mode 100644
index 00000000..18f14a3a
--- /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/EventListener/CreateConfigListener.php b/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php
index 0bdd1cae..e4d55c19 100644
--- a/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php
+++ b/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php
@@ -5,7 +5,6 @@ namespace Wallabag\UserBundle\EventListener;
5use Doctrine\ORM\EntityManager; 5use Doctrine\ORM\EntityManager;
6use FOS\UserBundle\Event\UserEvent; 6use FOS\UserBundle\Event\UserEvent;
7use FOS\UserBundle\FOSUserEvents; 7use FOS\UserBundle\FOSUserEvents;
8use Symfony\Component\EventDispatcher\EventDispatcherInterface;
9use Symfony\Component\EventDispatcher\EventSubscriberInterface; 8use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10use Wallabag\CoreBundle\Entity\Config; 9use Wallabag\CoreBundle\Entity\Config;
11 10
@@ -47,7 +46,7 @@ class CreateConfigListener implements EventSubscriberInterface
47 ]; 46 ];
48 } 47 }
49 48
50 public function createConfig(UserEvent $event, $eventName = null, EventDispatcherInterface $eventDispatcher = null) 49 public function createConfig(UserEvent $event)
51 { 50 {
52 $config = new Config($event->getUser()); 51 $config = new Config($event->getUser());
53 $config->setTheme($this->theme); 52 $config->setTheme($this->theme);
diff --git a/src/Wallabag/UserBundle/EventListener/PasswordResettingListener.php b/src/Wallabag/UserBundle/EventListener/PasswordResettingListener.php
index 3a7f2637..7df093f1 100644
--- a/src/Wallabag/UserBundle/EventListener/PasswordResettingListener.php
+++ b/src/Wallabag/UserBundle/EventListener/PasswordResettingListener.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\UserBundle\EventListener; 3namespace Wallabag\UserBundle\EventListener;
4 4
5use FOS\UserBundle\FOSUserEvents;
6use FOS\UserBundle\Event\FormEvent; 5use FOS\UserBundle\Event\FormEvent;
6use FOS\UserBundle\FOSUserEvents;
7use Symfony\Component\EventDispatcher\EventSubscriberInterface; 7use Symfony\Component\EventDispatcher\EventSubscriberInterface;
8use Symfony\Component\HttpFoundation\RedirectResponse; 8use Symfony\Component\HttpFoundation\RedirectResponse;
9use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 9use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
diff --git a/src/Wallabag/UserBundle/Form/SearchUserType.php b/src/Wallabag/UserBundle/Form/SearchUserType.php
new file mode 100644
index 00000000..9ce46ee1
--- /dev/null
+++ b/src/Wallabag/UserBundle/Form/SearchUserType.php
@@ -0,0 +1,29 @@
1<?php
2
3namespace Wallabag\UserBundle\Form;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\Extension\Core\Type\TextType;
7use Symfony\Component\Form\FormBuilderInterface;
8use Symfony\Component\OptionsResolver\OptionsResolver;
9
10class SearchUserType extends AbstractType
11{
12 public function buildForm(FormBuilderInterface $builder, array $options)
13 {
14 $builder
15 ->setMethod('GET')
16 ->add('term', TextType::class, [
17 'required' => true,
18 'label' => 'user.new.form_search.term_label',
19 ])
20 ;
21 }
22
23 public function configureOptions(OptionsResolver $resolver)
24 {
25 $resolver->setDefaults([
26 'csrf_protection' => false,
27 ]);
28 }
29}
diff --git a/src/Wallabag/UserBundle/Form/UserType.php b/src/Wallabag/UserBundle/Form/UserType.php
index d8cdbaf9..56fea640 100644
--- a/src/Wallabag/UserBundle/Form/UserType.php
+++ b/src/Wallabag/UserBundle/Form/UserType.php
@@ -3,12 +3,12 @@
3namespace Wallabag\UserBundle\Form; 3namespace Wallabag\UserBundle\Form;
4 4
5use Symfony\Component\Form\AbstractType; 5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\FormBuilderInterface;
7use Symfony\Component\OptionsResolver\OptionsResolver;
8use Symfony\Component\Form\Extension\Core\Type\CheckboxType; 6use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
7use Symfony\Component\Form\Extension\Core\Type\EmailType;
9use Symfony\Component\Form\Extension\Core\Type\SubmitType; 8use Symfony\Component\Form\Extension\Core\Type\SubmitType;
10use Symfony\Component\Form\Extension\Core\Type\TextType; 9use Symfony\Component\Form\Extension\Core\Type\TextType;
11use Symfony\Component\Form\Extension\Core\Type\EmailType; 10use Symfony\Component\Form\FormBuilderInterface;
11use Symfony\Component\OptionsResolver\OptionsResolver;
12 12
13class UserType extends AbstractType 13class UserType extends AbstractType
14{ 14{
@@ -50,8 +50,8 @@ class UserType extends AbstractType
50 */ 50 */
51 public function configureOptions(OptionsResolver $resolver) 51 public function configureOptions(OptionsResolver $resolver)
52 { 52 {
53 $resolver->setDefaults(array( 53 $resolver->setDefaults([
54 'data_class' => 'Wallabag\UserBundle\Entity\User', 54 'data_class' => 'Wallabag\UserBundle\Entity\User',
55 )); 55 ]);
56 } 56 }
57} 57}
diff --git a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php
index 961208f2..aed805c9 100644
--- a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php
+++ b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php
@@ -2,8 +2,8 @@
2 2
3namespace Wallabag\UserBundle\Mailer; 3namespace Wallabag\UserBundle\Mailer;
4 4
5use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
6use Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface; 5use Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface;
6use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
7 7
8/** 8/**
9 * Custom mailer for TwoFactorBundle email. 9 * Custom mailer for TwoFactorBundle email.
diff --git a/src/Wallabag/UserBundle/Repository/UserRepository.php b/src/Wallabag/UserBundle/Repository/UserRepository.php
index f913f52d..be693d3b 100644
--- a/src/Wallabag/UserBundle/Repository/UserRepository.php
+++ b/src/Wallabag/UserBundle/Repository/UserRepository.php
@@ -3,6 +3,8 @@
3namespace Wallabag\UserBundle\Repository; 3namespace Wallabag\UserBundle\Repository;
4 4
5use Doctrine\ORM\EntityRepository; 5use Doctrine\ORM\EntityRepository;
6use Doctrine\ORM\QueryBuilder;
7use Wallabag\UserBundle\Entity\User;
6 8
7class UserRepository extends EntityRepository 9class UserRepository extends EntityRepository
8{ 10{
@@ -52,4 +54,30 @@ class UserRepository extends EntityRepository
52 ->getQuery() 54 ->getQuery()
53 ->getSingleScalarResult(); 55 ->getSingleScalarResult();
54 } 56 }
57
58 /**
59 * Count how many users are existing.
60 *
61 * @return int
62 */
63 public function getSumUsers()
64 {
65 return $this->createQueryBuilder('u')
66 ->select('count(u)')
67 ->getQuery()
68 ->getSingleScalarResult();
69 }
70
71 /**
72 * Retrieves users filtered with a search term.
73 *
74 * @param string $term
75 *
76 * @return QueryBuilder
77 */
78 public function getQueryBuilderForSearch($term)
79 {
80 return $this->createQueryBuilder('u')
81 ->andWhere('lower(u.username) LIKE lower(:term) OR lower(u.email) LIKE lower(:term) OR lower(u.name) LIKE lower(:term)')->setParameter('term', '%' . $term . '%');
82 }
55} 83}
diff --git a/src/Wallabag/UserBundle/Resources/config/services.yml b/src/Wallabag/UserBundle/Resources/config/services.yml
index 72f6f12c..d3925de3 100644
--- a/src/Wallabag/UserBundle/Resources/config/services.yml
+++ b/src/Wallabag/UserBundle/Resources/config/services.yml
@@ -7,7 +7,7 @@ services:
7 - "%scheb_two_factor.email.sender_email%" 7 - "%scheb_two_factor.email.sender_email%"
8 - "%scheb_two_factor.email.sender_name%" 8 - "%scheb_two_factor.email.sender_name%"
9 - '@=service(''craue_config'').get(''wallabag_support_url'')' 9 - '@=service(''craue_config'').get(''wallabag_support_url'')'
10 - '@=service(''craue_config'').get(''wallabag_url'')' 10 - '%domain_name%'
11 11
12 wallabag_user.password_resetting: 12 wallabag_user.password_resetting:
13 class: Wallabag\UserBundle\EventListener\PasswordResettingListener 13 class: Wallabag\UserBundle\EventListener\PasswordResettingListener
@@ -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 }
diff --git a/src/Wallabag/UserBundle/Resources/translations/wallabag_user.oc.yml b/src/Wallabag/UserBundle/Resources/translations/wallabag_user.oc.yml
index 53a1afd1..e62ea2bc 100644
--- a/src/Wallabag/UserBundle/Resources/translations/wallabag_user.oc.yml
+++ b/src/Wallabag/UserBundle/Resources/translations/wallabag_user.oc.yml
@@ -5,7 +5,7 @@ auth_code:
5 subject: "Còdi d'autentificacion wallabag" 5 subject: "Còdi d'autentificacion wallabag"
6 body: 6 body:
7 hello: "Bonjorn %user%," 7 hello: "Bonjorn %user%,"
8 first_para: "Estant qu'avètz activat la dobla autentificacion sus vòtre compte wallabag e que venètz de vos conectar dempuèi un novèl aparelh (ordinador, mobil, etc.) vos mandem un còdi per validar la connexion." 8 first_para: "Estant qu'avètz activat l'autentificacion en dos temps sus vòstre compte wallabag e que venètz de vos connectar dempuèi un novèl periferic (ordinador, mobil, etc.) vos mandem un còdi per validar la connexion."
9 second_para: "Vaquí lo còdi a dintrar :" 9 second_para: "Vaquí lo còdi per dintrar:"
10 support: "S'avètz un problèma de connexion, dobtetz pas a contacter l'assisténcia : " 10 support: "S'avètz un problèma de connexion, dobtetz pas a contactar l'assisténcia:"
11 signature: "La còla de wallabag" 11 signature: "La còla de wallabag"
diff --git a/src/Wallabag/UserBundle/Resources/translations/wallabag_user.ru.yml b/src/Wallabag/UserBundle/Resources/translations/wallabag_user.ru.yml
new file mode 100644
index 00000000..2a418224
--- /dev/null
+++ b/src/Wallabag/UserBundle/Resources/translations/wallabag_user.ru.yml
@@ -0,0 +1,11 @@
1# Two factor mail
2auth_code:
3 on: 'Вкл'
4 mailer:
5 subject: 'код аутентификации wallabag'
6 body:
7 hello: "Привет %user%,"
8 first_para: "Поскольку вы активируете двухфакторную аутентификацию на своей учетной записи wallabag и только что вошли в систему с нового устройства (компьютер, телефон и т. д.), мы отправляем вам код для подтверждения вашего соединения."
9 second_para: "Вот код:"
10 support: "Пожалуйста, не стесняйтесь обращаться к нам, если у вас есть какие-либо проблемы:"
11 signature: "Команда wallabag"
diff --git a/src/Wallabag/UserBundle/Resources/views/Manage/index.html.twig b/src/Wallabag/UserBundle/Resources/views/Manage/index.html.twig
index daba29e4..15002632 100644
--- a/src/Wallabag/UserBundle/Resources/views/Manage/index.html.twig
+++ b/src/Wallabag/UserBundle/Resources/views/Manage/index.html.twig
@@ -7,37 +7,60 @@
7 <div class="row"> 7 <div class="row">
8 <div class="col s12"> 8 <div class="col s12">
9 <div class="card-panel"> 9 <div class="card-panel">
10 {% if users.getNbPages > 1 %}
11 {{ pagerfanta(users, 'twitter_bootstrap_translated', {'proximity': 1}) }}
12 {% endif %}
10 <div class="row"> 13 <div class="row">
11 <div class="input-field col s12"> 14 <div class="col s6">
12 <p class="help">{{ 'user.description'|trans|raw }}</p> 15 <p class="help">{{ 'user.description'|trans|raw }}</p>
16 </div>
17 <div class="col s6">
18 <div class="input-field">
19 <form name="search_users" method="GET" action="{{ path('user_index')}}">
20 {% if form_errors(searchForm) %}
21 <span class="black-text">{{ form_errors(searchForm) }}</span>
22 {% endif %}
23
24 {% if form_errors(searchForm.term) %}
25 <span class="black-text">{{ form_errors(searchForm.term) }}</span>
26 {% endif %}
13 27
14 <table class="bordered"> 28 {{ form_widget(searchForm.term, { 'attr': {'autocomplete': 'off', 'placeholder': 'user.search.placeholder'} }) }}
15 <thead> 29
16 <tr> 30 {{ form_rest(searchForm) }}
17 <th>{{ 'user.form.username_label'|trans }}</th> 31 </form>
18 <th>{{ 'user.form.email_label'|trans }}</th> 32 </div>
19 <th>{{ 'user.form.last_login_label'|trans }}</th>
20 <th>{{ 'user.list.actions'|trans }}</th>
21 </tr>
22 </thead>
23 <tbody>
24 {% for user in users %}
25 <tr>
26 <td>{{ user.username }}</td>
27 <td>{{ user.email }}</td>
28 <td>{% if user.lastLogin %}{{ user.lastLogin|date('Y-m-d H:i:s') }}{% endif %}</td>
29 <td>
30 <a href="{{ path('user_edit', { 'id': user.id }) }}">{{ 'user.list.edit_action'|trans }}</a>
31 </td>
32 </tr>
33 {% endfor %}
34 </tbody>
35 </table>
36 <br />
37 <p>
38 <a href="{{ path('user_new') }}" class="waves-effect waves-light btn">{{ 'user.list.create_new_one'|trans }}</a>
39 </p>
40 </div> 33 </div>
34
35 <table class="bordered">
36 <thead>
37 <tr>
38 <th>{{ 'user.form.username_label'|trans }}</th>
39 <th>{{ 'user.form.email_label'|trans }}</th>
40 <th>{{ 'user.form.last_login_label'|trans }}</th>
41 <th>{{ 'user.list.actions'|trans }}</th>
42 </tr>
43 </thead>
44 <tbody>
45 {% for user in users %}
46 <tr>
47 <td>{{ user.username }}</td>
48 <td>{{ user.email }}</td>
49 <td>{% if user.lastLogin %}{{ user.lastLogin|date('Y-m-d H:i:s') }}{% endif %}</td>
50 <td>
51 <a href="{{ path('user_edit', { 'id': user.id }) }}">{{ 'user.list.edit_action'|trans }}</a>
52 </td>
53 </tr>
54 {% endfor %}
55 </tbody>
56 </table>
57 <br />
58 <p>
59 <a href="{{ path('user_new') }}" class="waves-effect waves-light btn">{{ 'user.list.create_new_one'|trans }}</a>
60 </p>
61 {% if users.getNbPages > 1 %}
62 {{ pagerfanta(users, 'twitter_bootstrap_translated', {'proximity': 1}) }}
63 {% endif %}
41 </div> 64 </div>
42 </div> 65 </div>
43 </div> 66 </div>