aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/Wallabag
diff options
context:
space:
mode:
Diffstat (limited to 'src/Wallabag')
-rw-r--r--src/Wallabag/ApiBundle/Controller/EntryRestController.php251
-rw-r--r--src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php119
-rw-r--r--src/Wallabag/CoreBundle/Command/InstallCommand.php23
-rw-r--r--src/Wallabag/CoreBundle/DependencyInjection/Configuration.php3
-rw-r--r--src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php1
-rw-r--r--src/Wallabag/CoreBundle/Repository/EntryRepository.php30
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.da.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.de.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.en.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.es.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml140
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.it.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml2
-rw-r--r--src/Wallabag/ImportBundle/Command/ImportCommand.php11
-rw-r--r--src/Wallabag/UserBundle/Controller/ManageController.php67
-rw-r--r--src/Wallabag/UserBundle/Form/SearchUserType.php29
-rw-r--r--src/Wallabag/UserBundle/Repository/UserRepository.php13
-rw-r--r--src/Wallabag/UserBundle/Resources/views/Manage/index.html.twig77
23 files changed, 586 insertions, 200 deletions
diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php
index 7590efbb..dbff6065 100644
--- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php
+++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php
@@ -5,6 +5,7 @@ namespace Wallabag\ApiBundle\Controller;
5use Hateoas\Configuration\Route; 5use Hateoas\Configuration\Route;
6use Hateoas\Representation\Factory\PagerfantaFactory; 6use Hateoas\Representation\Factory\PagerfantaFactory;
7use Nelmio\ApiDocBundle\Annotation\ApiDoc; 7use Nelmio\ApiDocBundle\Annotation\ApiDoc;
8use Symfony\Component\HttpKernel\Exception\HttpException;
8use Symfony\Component\HttpFoundation\Request; 9use Symfony\Component\HttpFoundation\Request;
9use Symfony\Component\HttpFoundation\JsonResponse; 10use Symfony\Component\HttpFoundation\JsonResponse;
10use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 11use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -44,9 +45,7 @@ class EntryRestController extends WallabagRestController
44 $results[$url] = $res instanceof Entry ? $res->getId() : false; 45 $results[$url] = $res instanceof Entry ? $res->getId() : false;
45 } 46 }
46 47
47 $json = $this->get('serializer')->serialize($results, 'json'); 48 return $this->sendResponse($results);
48
49 return (new JsonResponse())->setJson($json);
50 } 49 }
51 50
52 // let's see if it is a simple url? 51 // let's see if it is a simple url?
@@ -62,9 +61,7 @@ class EntryRestController extends WallabagRestController
62 61
63 $exists = $res instanceof Entry ? $res->getId() : false; 62 $exists = $res instanceof Entry ? $res->getId() : false;
64 63
65 $json = $this->get('serializer')->serialize(['exists' => $exists], 'json'); 64 return $this->sendResponse(['exists' => $exists]);
66
67 return (new JsonResponse())->setJson($json);
68 } 65 }
69 66
70 /** 67 /**
@@ -124,9 +121,7 @@ class EntryRestController extends WallabagRestController
124 ) 121 )
125 ); 122 );
126 123
127 $json = $this->get('serializer')->serialize($paginatedCollection, 'json'); 124 return $this->sendResponse($paginatedCollection);
128
129 return (new JsonResponse())->setJson($json);
130 } 125 }
131 126
132 /** 127 /**
@@ -145,9 +140,7 @@ class EntryRestController extends WallabagRestController
145 $this->validateAuthentication(); 140 $this->validateAuthentication();
146 $this->validateUserAccess($entry->getUser()->getId()); 141 $this->validateUserAccess($entry->getUser()->getId());
147 142
148 $json = $this->get('serializer')->serialize($entry, 'json'); 143 return $this->sendResponse($entry);
149
150 return (new JsonResponse())->setJson($json);
151 } 144 }
152 145
153 /** 146 /**
@@ -173,6 +166,110 @@ class EntryRestController extends WallabagRestController
173 } 166 }
174 167
175 /** 168 /**
169 * Handles an entries list and delete URL.
170 *
171 * @ApiDoc(
172 * parameters={
173 * {"name"="urls", "dataType"="string", "required"=true, "format"="A JSON array of urls [{'url': 'http://...'}, {'url': 'http://...'}]", "description"="Urls (as an array) to delete."}
174 * }
175 * )
176 *
177 * @return JsonResponse
178 */
179 public function deleteEntriesListAction(Request $request)
180 {
181 $this->validateAuthentication();
182
183 $urls = json_decode($request->query->get('urls', []));
184
185 if (empty($urls)) {
186 return $this->sendResponse([]);
187 }
188
189 $results = [];
190
191 // handle multiple urls
192 foreach ($urls as $key => $url) {
193 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
194 $url,
195 $this->getUser()->getId()
196 );
197
198 $results[$key]['url'] = $url;
199
200 if (false !== $entry) {
201 $em = $this->getDoctrine()->getManager();
202 $em->remove($entry);
203 $em->flush();
204
205 // entry deleted, dispatch event about it!
206 $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry));
207 }
208
209 $results[$key]['entry'] = $entry instanceof Entry ? true : false;
210 }
211
212 return $this->sendResponse($results);
213 }
214
215 /**
216 * Handles an entries list and create URL.
217 *
218 * @ApiDoc(
219 * parameters={
220 * {"name"="urls", "dataType"="string", "required"=true, "format"="A JSON array of urls [{'url': 'http://...'}, {'url': 'http://...'}]", "description"="Urls (as an array) to create."}
221 * }
222 * )
223 *
224 * @return JsonResponse
225 *
226 * @throws HttpException When limit is reached
227 */
228 public function postEntriesListAction(Request $request)
229 {
230 $this->validateAuthentication();
231
232 $urls = json_decode($request->query->get('urls', []));
233 $results = [];
234
235 $limit = $this->container->getParameter('wallabag_core.api_limit_mass_actions');
236
237 if (count($urls) > $limit) {
238 throw new HttpException(400, 'API limit reached');
239 }
240
241 // handle multiple urls
242 if (!empty($urls)) {
243 foreach ($urls as $key => $url) {
244 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
245 $url,
246 $this->getUser()->getId()
247 );
248
249 $results[$key]['url'] = $url;
250
251 if (false === $entry) {
252 $entry = $this->get('wallabag_core.content_proxy')->updateEntry(
253 new Entry($this->getUser()),
254 $url
255 );
256 }
257
258 $em = $this->getDoctrine()->getManager();
259 $em->persist($entry);
260 $em->flush();
261
262 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false;
263
264 // entry saved, dispatch event about it!
265 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
266 }
267 }
268
269 return $this->sendResponse($results);
270 }
271
272 /**
176 * Create an entry. 273 * Create an entry.
177 * 274 *
178 * @ApiDoc( 275 * @ApiDoc(
@@ -229,9 +326,7 @@ class EntryRestController extends WallabagRestController
229 // entry saved, dispatch event about it! 326 // entry saved, dispatch event about it!
230 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); 327 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
231 328
232 $json = $this->get('serializer')->serialize($entry, 'json'); 329 return $this->sendResponse($entry);
233
234 return (new JsonResponse())->setJson($json);
235 } 330 }
236 331
237 /** 332 /**
@@ -280,9 +375,7 @@ class EntryRestController extends WallabagRestController
280 $em = $this->getDoctrine()->getManager(); 375 $em = $this->getDoctrine()->getManager();
281 $em->flush(); 376 $em->flush();
282 377
283 $json = $this->get('serializer')->serialize($entry, 'json'); 378 return $this->sendResponse($entry);
284
285 return (new JsonResponse())->setJson($json);
286 } 379 }
287 380
288 /** 381 /**
@@ -325,9 +418,7 @@ class EntryRestController extends WallabagRestController
325 // entry saved, dispatch event about it! 418 // entry saved, dispatch event about it!
326 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); 419 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
327 420
328 $json = $this->get('serializer')->serialize($entry, 'json'); 421 return $this->sendResponse($entry);
329
330 return (new JsonResponse())->setJson($json);
331 } 422 }
332 423
333 /** 424 /**
@@ -353,9 +444,7 @@ class EntryRestController extends WallabagRestController
353 // entry deleted, dispatch event about it! 444 // entry deleted, dispatch event about it!
354 $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry)); 445 $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry));
355 446
356 $json = $this->get('serializer')->serialize($entry, 'json'); 447 return $this->sendResponse($entry);
357
358 return (new JsonResponse())->setJson($json);
359 } 448 }
360 449
361 /** 450 /**
@@ -374,9 +463,7 @@ class EntryRestController extends WallabagRestController
374 $this->validateAuthentication(); 463 $this->validateAuthentication();
375 $this->validateUserAccess($entry->getUser()->getId()); 464 $this->validateUserAccess($entry->getUser()->getId());
376 465
377 $json = $this->get('serializer')->serialize($entry->getTags(), 'json'); 466 return $this->sendResponse($entry->getTags());
378
379 return (new JsonResponse())->setJson($json);
380 } 467 }
381 468
382 /** 469 /**
@@ -407,9 +494,7 @@ class EntryRestController extends WallabagRestController
407 $em->persist($entry); 494 $em->persist($entry);
408 $em->flush(); 495 $em->flush();
409 496
410 $json = $this->get('serializer')->serialize($entry, 'json'); 497 return $this->sendResponse($entry);
411
412 return (new JsonResponse())->setJson($json);
413 } 498 }
414 499
415 /** 500 /**
@@ -434,9 +519,7 @@ class EntryRestController extends WallabagRestController
434 $em->persist($entry); 519 $em->persist($entry);
435 $em->flush(); 520 $em->flush();
436 521
437 $json = $this->get('serializer')->serialize($entry, 'json'); 522 return $this->sendResponse($entry);
438
439 return (new JsonResponse())->setJson($json);
440 } 523 }
441 524
442 /** 525 /**
@@ -455,45 +538,46 @@ class EntryRestController extends WallabagRestController
455 $this->validateAuthentication(); 538 $this->validateAuthentication();
456 539
457 $list = json_decode($request->query->get('list', [])); 540 $list = json_decode($request->query->get('list', []));
458 $results = []; 541
542 if (empty($list)) {
543 return $this->sendResponse([]);
544 }
459 545
460 // handle multiple urls 546 // handle multiple urls
461 if (!empty($list)) { 547 $results = [];
462 foreach ($list as $key => $element) {
463 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
464 $element->url,
465 $this->getUser()->getId()
466 );
467 548
468 $results[$key]['url'] = $element->url; 549 foreach ($list as $key => $element) {
469 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false; 550 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
551 $element->url,
552 $this->getUser()->getId()
553 );
470 554
471 $tags = $element->tags; 555 $results[$key]['url'] = $element->url;
556 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false;
472 557
473 if (false !== $entry && !(empty($tags))) { 558 $tags = $element->tags;
474 $tags = explode(',', $tags);
475 foreach ($tags as $label) {
476 $label = trim($label);
477 559
478 $tag = $this->getDoctrine() 560 if (false !== $entry && !(empty($tags))) {
479 ->getRepository('WallabagCoreBundle:Tag') 561 $tags = explode(',', $tags);
480 ->findOneByLabel($label); 562 foreach ($tags as $label) {
563 $label = trim($label);
481 564
482 if (false !== $tag) { 565 $tag = $this->getDoctrine()
483 $entry->removeTag($tag); 566 ->getRepository('WallabagCoreBundle:Tag')
484 } 567 ->findOneByLabel($label);
485 }
486 568
487 $em = $this->getDoctrine()->getManager(); 569 if (false !== $tag) {
488 $em->persist($entry); 570 $entry->removeTag($tag);
489 $em->flush(); 571 }
490 } 572 }
573
574 $em = $this->getDoctrine()->getManager();
575 $em->persist($entry);
576 $em->flush();
491 } 577 }
492 } 578 }
493 579
494 $json = $this->get('serializer')->serialize($results, 'json'); 580 return $this->sendResponse($results);
495
496 return (new JsonResponse())->setJson($json);
497 } 581 }
498 582
499 /** 583 /**
@@ -512,32 +596,47 @@ class EntryRestController extends WallabagRestController
512 $this->validateAuthentication(); 596 $this->validateAuthentication();
513 597
514 $list = json_decode($request->query->get('list', [])); 598 $list = json_decode($request->query->get('list', []));
599
600 if (empty($list)) {
601 return $this->sendResponse([]);
602 }
603
515 $results = []; 604 $results = [];
516 605
517 // handle multiple urls 606 // handle multiple urls
518 if (!empty($list)) { 607 foreach ($list as $key => $element) {
519 foreach ($list as $key => $element) { 608 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId(
520 $entry = $this->get('wallabag_core.entry_repository')->findByUrlAndUserId( 609 $element->url,
521 $element->url, 610 $this->getUser()->getId()
522 $this->getUser()->getId() 611 );
523 );
524 612
525 $results[$key]['url'] = $element->url; 613 $results[$key]['url'] = $element->url;
526 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false; 614 $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false;
527 615
528 $tags = $element->tags; 616 $tags = $element->tags;
529 617
530 if (false !== $entry && !(empty($tags))) { 618 if (false !== $entry && !(empty($tags))) {
531 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags); 619 $this->get('wallabag_core.content_proxy')->assignTagsToEntry($entry, $tags);
532 620
533 $em = $this->getDoctrine()->getManager(); 621 $em = $this->getDoctrine()->getManager();
534 $em->persist($entry); 622 $em->persist($entry);
535 $em->flush(); 623 $em->flush();
536 }
537 } 624 }
538 } 625 }
539 626
540 $json = $this->get('serializer')->serialize($results, 'json'); 627 return $this->sendResponse($results);
628 }
629
630 /**
631 * Shortcut to send data serialized in json.
632 *
633 * @param mixed $data
634 *
635 * @return JsonResponse
636 */
637 private function sendResponse($data)
638 {
639 $json = $this->get('serializer')->serialize($data, 'json');
541 640
542 return (new JsonResponse())->setJson($json); 641 return (new JsonResponse())->setJson($json);
543 } 642 }
diff --git a/src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php b/src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php
new file mode 100644
index 00000000..65f35d8e
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Command/CleanDuplicatesCommand.php
@@ -0,0 +1,119 @@
1<?php
2
3namespace Wallabag\CoreBundle\Command;
4
5use Doctrine\ORM\NoResultException;
6use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface;
10use Wallabag\CoreBundle\Entity\Entry;
11use Wallabag\UserBundle\Entity\User;
12
13class CleanDuplicatesCommand extends ContainerAwareCommand
14{
15 /** @var OutputInterface */
16 protected $output;
17
18 protected $duplicates = 0;
19
20 protected function configure()
21 {
22 $this
23 ->setName('wallabag:clean-duplicates')
24 ->setDescription('Cleans the database for duplicates')
25 ->setHelp('This command helps you to clean your articles list in case of duplicates')
26 ->addArgument(
27 'username',
28 InputArgument::OPTIONAL,
29 'User to clean'
30 );
31 }
32
33 protected function execute(InputInterface $input, OutputInterface $output)
34 {
35 $this->output = $output;
36
37 $username = $input->getArgument('username');
38
39 if ($username) {
40 try {
41 $user = $this->getUser($username);
42 $this->cleanDuplicates($user);
43 } catch (NoResultException $e) {
44 $output->writeln(sprintf('<error>User "%s" not found.</error>', $username));
45
46 return 1;
47 }
48 } else {
49 $users = $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findAll();
50
51 $output->writeln(sprintf('Cleaning through %d user accounts', count($users)));
52
53 foreach ($users as $user) {
54 $output->writeln(sprintf('Processing user %s', $user->getUsername()));
55 $this->cleanDuplicates($user);
56 }
57 $output->writeln(sprintf('Finished cleaning. %d duplicates found in total', $this->duplicates));
58 }
59
60 return 0;
61 }
62
63 /**
64 * @param User $user
65 */
66 private function cleanDuplicates(User $user)
67 {
68 $em = $this->getContainer()->get('doctrine.orm.entity_manager');
69 $repo = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry');
70
71 $entries = $repo->getAllEntriesIdAndUrl($user->getId());
72
73 $duplicatesCount = 0;
74 $urls = [];
75 foreach ($entries as $entry) {
76 $url = $this->similarUrl($entry['url']);
77
78 /* @var $entry Entry */
79 if (in_array($url, $urls)) {
80 ++$duplicatesCount;
81
82 $em->remove($repo->find($entry['id']));
83 $em->flush(); // Flushing at the end of the loop would require the instance not being online
84 } else {
85 $urls[] = $entry['url'];
86 }
87 }
88
89 $this->duplicates += $duplicatesCount;
90
91 $this->output->writeln(sprintf('Cleaned %d duplicates for user %s', $duplicatesCount, $user->getUserName()));
92 }
93
94 private function similarUrl($url)
95 {
96 if (in_array(substr($url, -1), ['/', '#'])) { // get rid of "/" and "#" and the end of urls
97 return substr($url, 0, strlen($url));
98 }
99
100 return $url;
101 }
102
103 /**
104 * Fetches a user from its username.
105 *
106 * @param string $username
107 *
108 * @return \Wallabag\UserBundle\Entity\User
109 */
110 private function getUser($username)
111 {
112 return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username);
113 }
114
115 private function getDoctrine()
116 {
117 return $this->getContainer()->get('doctrine');
118 }
119}
diff --git a/src/Wallabag/CoreBundle/Command/InstallCommand.php b/src/Wallabag/CoreBundle/Command/InstallCommand.php
index 3c4d3f25..0d9364f6 100644
--- a/src/Wallabag/CoreBundle/Command/InstallCommand.php
+++ b/src/Wallabag/CoreBundle/Command/InstallCommand.php
@@ -63,6 +63,7 @@ class InstallCommand extends ContainerAwareCommand
63 ->setupDatabase() 63 ->setupDatabase()
64 ->setupAdmin() 64 ->setupAdmin()
65 ->setupConfig() 65 ->setupConfig()
66 ->runMigrations()
66 ; 67 ;
67 68
68 $output->writeln('<info>wallabag has been successfully installed.</info>'); 69 $output->writeln('<info>wallabag has been successfully installed.</info>');
@@ -71,7 +72,7 @@ class InstallCommand extends ContainerAwareCommand
71 72
72 protected function checkRequirements() 73 protected function checkRequirements()
73 { 74 {
74 $this->defaultOutput->writeln('<info><comment>Step 1 of 4.</comment> Checking system requirements.</info>'); 75 $this->defaultOutput->writeln('<info><comment>Step 1 of 5.</comment> Checking system requirements.</info>');
75 $doctrineManager = $this->getContainer()->get('doctrine')->getManager(); 76 $doctrineManager = $this->getContainer()->get('doctrine')->getManager();
76 77
77 $rows = []; 78 $rows = [];
@@ -175,11 +176,11 @@ class InstallCommand extends ContainerAwareCommand
175 176
176 protected function setupDatabase() 177 protected function setupDatabase()
177 { 178 {
178 $this->defaultOutput->writeln('<info><comment>Step 2 of 4.</comment> Setting up database.</info>'); 179 $this->defaultOutput->writeln('<info><comment>Step 2 of 5.</comment> Setting up database.</info>');
179 180
180 // user want to reset everything? Don't care about what is already here 181 // user want to reset everything? Don't care about what is already here
181 if (true === $this->defaultInput->getOption('reset')) { 182 if (true === $this->defaultInput->getOption('reset')) {
182 $this->defaultOutput->writeln('Droping database, creating database and schema, clearing the cache'); 183 $this->defaultOutput->writeln('Dropping database, creating database and schema, clearing the cache');
183 184
184 $this 185 $this
185 ->runCommand('doctrine:database:drop', ['--force' => true]) 186 ->runCommand('doctrine:database:drop', ['--force' => true])
@@ -211,7 +212,7 @@ class InstallCommand extends ContainerAwareCommand
211 $question = new ConfirmationQuestion('It appears that your database already exists. Would you like to reset it? (y/N)', false); 212 $question = new ConfirmationQuestion('It appears that your database already exists. Would you like to reset it? (y/N)', false);
212 213
213 if ($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)) { 214 if ($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)) {
214 $this->defaultOutput->writeln('Droping database, creating database and schema'); 215 $this->defaultOutput->writeln('Dropping database, creating database and schema');
215 216
216 $this 217 $this
217 ->runCommand('doctrine:database:drop', ['--force' => true]) 218 ->runCommand('doctrine:database:drop', ['--force' => true])
@@ -221,7 +222,7 @@ class InstallCommand extends ContainerAwareCommand
221 } elseif ($this->isSchemaPresent()) { 222 } elseif ($this->isSchemaPresent()) {
222 $question = new ConfirmationQuestion('Seems like your database contains schema. Do you want to reset it? (y/N)', false); 223 $question = new ConfirmationQuestion('Seems like your database contains schema. Do you want to reset it? (y/N)', false);
223 if ($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)) { 224 if ($questionHelper->ask($this->defaultInput, $this->defaultOutput, $question)) {
224 $this->defaultOutput->writeln('Droping schema and creating schema'); 225 $this->defaultOutput->writeln('Dropping schema and creating schema');
225 226
226 $this 227 $this
227 ->runCommand('doctrine:schema:drop', ['--force' => true]) 228 ->runCommand('doctrine:schema:drop', ['--force' => true])
@@ -246,7 +247,7 @@ class InstallCommand extends ContainerAwareCommand
246 247
247 protected function setupAdmin() 248 protected function setupAdmin()
248 { 249 {
249 $this->defaultOutput->writeln('<info><comment>Step 3 of 4.</comment> Administration setup.</info>'); 250 $this->defaultOutput->writeln('<info><comment>Step 3 of 5.</comment> Administration setup.</info>');
250 251
251 $questionHelper = $this->getHelperSet()->get('question'); 252 $questionHelper = $this->getHelperSet()->get('question');
252 $question = new ConfirmationQuestion('Would you like to create a new admin user (recommended) ? (Y/n)', true); 253 $question = new ConfirmationQuestion('Would you like to create a new admin user (recommended) ? (Y/n)', true);
@@ -285,7 +286,7 @@ class InstallCommand extends ContainerAwareCommand
285 286
286 protected function setupConfig() 287 protected function setupConfig()
287 { 288 {
288 $this->defaultOutput->writeln('<info><comment>Step 4 of 4.</comment> Config setup.</info>'); 289 $this->defaultOutput->writeln('<info><comment>Step 4 of 5.</comment> Config setup.</info>');
289 $em = $this->getContainer()->get('doctrine.orm.entity_manager'); 290 $em = $this->getContainer()->get('doctrine.orm.entity_manager');
290 291
291 // cleanup before insert new stuff 292 // cleanup before insert new stuff
@@ -464,6 +465,14 @@ class InstallCommand extends ContainerAwareCommand
464 return $this; 465 return $this;
465 } 466 }
466 467
468 protected function runMigrations()
469 {
470 $this->defaultOutput->writeln('<info><comment>Step 5 of 5.</comment> Run migrations.</info>');
471
472 $this
473 ->runCommand('doctrine:migrations:migrate', ['--no-interaction' => true]);
474 }
475
467 /** 476 /**
468 * Run a command. 477 * Run a command.
469 * 478 *
diff --git a/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php b/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php
index 006a18c3..75b37729 100644
--- a/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php
+++ b/src/Wallabag/CoreBundle/DependencyInjection/Configuration.php
@@ -47,6 +47,9 @@ class Configuration implements ConfigurationInterface
47 ->scalarNode('list_mode') 47 ->scalarNode('list_mode')
48 ->defaultValue(1) 48 ->defaultValue(1)
49 ->end() 49 ->end()
50 ->scalarNode('api_limit_mass_actions')
51 ->defaultValue(10)
52 ->end()
50 ->end() 53 ->end()
51 ; 54 ;
52 55
diff --git a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php
index aa9ee339..c075c19f 100644
--- a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php
+++ b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php
@@ -26,6 +26,7 @@ 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.api_limit_mass_actions', $config['api_limit_mass_actions']);
29 30
30 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 31 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
31 $loader->load('services.yml'); 32 $loader->load('services.yml');
diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
index 1f22e901..6972e974 100644
--- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php
+++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
@@ -379,4 +379,34 @@ class EntryRepository extends EntityRepository
379 ->setParameter('userId', $userId) 379 ->setParameter('userId', $userId)
380 ->execute(); 380 ->execute();
381 } 381 }
382
383 /**
384 * Get id and url from all entries
385 * Used for the clean-duplicates command.
386 */
387 public function getAllEntriesIdAndUrl($userId)
388 {
389 $qb = $this->createQueryBuilder('e')
390 ->select('e.id, e.url')
391 ->where('e.user = :userid')->setParameter(':userid', $userId);
392
393 return $qb->getQuery()->getArrayResult();
394 }
395
396 /**
397 * Find all entries by url and owner.
398 *
399 * @param $url
400 * @param $userId
401 *
402 * @return array
403 */
404 public function findAllByUrlAndUserId($url, $userId)
405 {
406 return $this->createQueryBuilder('e')
407 ->where('e.url = :url')->setParameter('url', urldecode($url))
408 ->andWhere('e.user = :user_id')->setParameter('user_id', $userId)
409 ->getQuery()
410 ->getResult();
411 }
382} 412}
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
index e5211b57..57319af7 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
@@ -512,6 +512,8 @@ user:
512 # delete: Delete 512 # delete: Delete
513 # delete_confirm: Are you sure? 513 # delete_confirm: Are you sure?
514 # back_to_list: Back to list 514 # back_to_list: Back to list
515 search:
516 # placeholder: Filter by username or email
515 517
516error: 518error:
517 # page_title: An error occurred 519 # page_title: An error occurred
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
index 893a4564..a7bcecc6 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
@@ -513,6 +513,8 @@ user:
513 delete: Löschen 513 delete: Löschen
514 delete_confirm: Bist du sicher? 514 delete_confirm: Bist du sicher?
515 back_to_list: Zurück zur Liste 515 back_to_list: Zurück zur Liste
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 page_title: Ein Fehler ist aufgetreten 520 page_title: Ein Fehler ist aufgetreten
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
index 4b745683..1ef2874d 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
@@ -513,6 +513,8 @@ user:
513 delete: Delete 513 delete: Delete
514 delete_confirm: Are you sure? 514 delete_confirm: Are you sure?
515 back_to_list: Back to list 515 back_to_list: Back to list
516 search:
517 placeholder: Filter by username or email
516 518
517error: 519error:
518 page_title: An error occurred 520 page_title: An error occurred
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
index 99d25859..6cd079b0 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
@@ -513,6 +513,8 @@ user:
513 delete: Eliminar 513 delete: Eliminar
514 delete_confirm: ¿Estás seguro? 514 delete_confirm: ¿Estás seguro?
515 back_to_list: Volver a la lista 515 back_to_list: Volver a la lista
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 page_title: Ha ocurrido un error 520 page_title: Ha ocurrido un error
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
index ccd9d555..fb6e315e 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
@@ -513,6 +513,8 @@ user:
513 # delete: Delete 513 # delete: Delete
514 # delete_confirm: Are you sure? 514 # delete_confirm: Are you sure?
515 # back_to_list: Back to list 515 # back_to_list: Back to list
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 # page_title: An error occurred 520 # page_title: An error occurred
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
index a0f100f7..ad886363 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
@@ -46,7 +46,7 @@ footer:
46 social: "Social" 46 social: "Social"
47 powered_by: "propulsé par" 47 powered_by: "propulsé par"
48 about: "À propos" 48 about: "À propos"
49 stats: Depuis le %user_creation%, vous avez lu %nb_archives% articles. Ce qui fait %per_day% par jour ! 49 stats: "Depuis le %user_creation%, vous avez lu %nb_archives% articles. Ce qui fait %per_day% par jour !"
50 50
51config: 51config:
52 page_title: "Configuration" 52 page_title: "Configuration"
@@ -71,16 +71,16 @@ config:
71 300_word: "Je lis environ 300 mots par minute" 71 300_word: "Je lis environ 300 mots par minute"
72 400_word: "Je lis environ 400 mots par minute" 72 400_word: "Je lis environ 400 mots par minute"
73 action_mark_as_read: 73 action_mark_as_read:
74 label: 'Où souhaitez-vous être redirigé après avoir marqué un article comme lu ?' 74 label: "Où souhaitez-vous être redirigé après avoir marqué un article comme lu ?"
75 redirect_homepage: "À la page d'accueil" 75 redirect_homepage: "À la page d’accueil"
76 redirect_current_page: 'À la page courante' 76 redirect_current_page: "À la page courante"
77 pocket_consumer_key_label: Clé d’authentification Pocket pour importer les données 77 pocket_consumer_key_label: "Clé d’authentification Pocket pour importer les données"
78 android_configuration: Configurez votre application Android 78 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." 79 help_theme: "L’affichage de wallabag est personnalisable. C’est ici que vous choisissez le thème que vous préférez."
80 help_items_per_page: "Vous pouvez définir le nombre d'articles affichés sur chaque page." 80 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." 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."
82 help_language: "Vous pouvez définir la langue de l'interface de wallabag." 82 help_language: "Vous pouvez définir la langue de l’interface de wallabag."
83 help_pocket_consumer_key: "Nécessaire pour l'import depuis Pocket. Vous pouvez le créer depuis votre compte Pocket." 83 help_pocket_consumer_key: "Nécessaire pour l’import depuis Pocket. Vous pouvez le créer depuis votre compte Pocket."
84 form_rss: 84 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." 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."
86 token_label: "Jeton RSS" 86 token_label: "Jeton RSS"
@@ -100,18 +100,18 @@ config:
100 twoFactorAuthentication_label: "Double authentification" 100 twoFactorAuthentication_label: "Double authentification"
101 help_twoFactorAuthentication: "Si vous activez 2FA, à chaque tentative de connexion à wallabag, vous recevrez un code par email." 101 help_twoFactorAuthentication: "Si vous activez 2FA, à chaque tentative de connexion à wallabag, vous recevrez un code par email."
102 delete: 102 delete:
103 title: Supprimer mon compte (attention danger !) 103 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é. 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é."
105 confirm: Vous êtes vraiment sûr ? (C'EST IRRÉVERSIBLE) 105 confirm: "Vous êtes vraiment sûr ? (C’EST IRRÉVERSIBLE)"
106 button: 'Supprimer mon compte' 106 button: "Supprimer mon compte"
107 reset: 107 reset:
108 title: Réinitialisation (attention danger !) 108 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 ! 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 !"
110 annotations: Supprimer TOUTES les annotations 110 annotations: "Supprimer TOUTES les annotations"
111 tags: Supprimer TOUS les tags 111 tags: "Supprimer TOUS les tags"
112 entries: Supprimer TOUS les articles 112 entries: "Supprimer TOUS les articles"
113 archived: Supprimer TOUS les articles archivés 113 archived: "Supprimer TOUS les articles archivés"
114 confirm: Êtes-vous vraiment vraiment sûr ? (C'EST IRRÉVERSIBLE) 114 confirm: "Êtes-vous vraiment vraiment sûr ? (C’EST IRRÉVERSIBLE)"
115 form_password: 115 form_password:
116 description: "Vous pouvez changer ici votre mot de passe. Le mot de passe doit contenir au moins 8 caractères." 116 description: "Vous pouvez changer ici votre mot de passe. Le mot de passe doit contenir au moins 8 caractères."
117 old_password_label: "Mot de passe actuel" 117 old_password_label: "Mot de passe actuel"
@@ -164,7 +164,7 @@ entry:
164 archived: "Articles lus" 164 archived: "Articles lus"
165 filtered: "Articles filtrés" 165 filtered: "Articles filtrés"
166 filtered_tags: "Articles filtrés par tags :" 166 filtered_tags: "Articles filtrés par tags :"
167 filtered_search: 'Articles filtrés par recherche :' 167 filtered_search: "Articles filtrés par recherche :"
168 untagged: "Article sans tag" 168 untagged: "Article sans tag"
169 list: 169 list:
170 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." 170 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."
@@ -188,7 +188,7 @@ entry:
188 preview_picture_label: "A une photo" 188 preview_picture_label: "A une photo"
189 preview_picture_help: "Photo" 189 preview_picture_help: "Photo"
190 language_label: "Langue" 190 language_label: "Langue"
191 http_status_label: 'Statut HTTP' 191 http_status_label: "Statut HTTP"
192 reading_time: 192 reading_time:
193 label: "Durée de lecture en minutes" 193 label: "Durée de lecture en minutes"
194 from: "de" 194 from: "de"
@@ -298,32 +298,32 @@ howto:
298 bookmarklet: 298 bookmarklet:
299 description: "Glissez et déposez ce lien dans votre barre de favoris :" 299 description: "Glissez et déposez ce lien dans votre barre de favoris :"
300 shortcuts: 300 shortcuts:
301 page_description: Voici les raccourcis disponibles dans wallabag. 301 page_description: "Voici les raccourcis disponibles dans wallabag."
302 shortcut: Raccourci 302 shortcut: "Raccourci"
303 action: Action 303 action: "Action"
304 all_pages_title: Raccourcis disponibles dans toutes les pages 304 all_pages_title: "Raccourcis disponibles dans toutes les pages"
305 go_unread: Afficher les articles non lus 305 go_unread: "Afficher les articles non lus"
306 go_starred: Afficher les articles favoris 306 go_starred: "Afficher les articles favoris"
307 go_archive: Afficher les articles lus 307 go_archive: "Afficher les articles lus"
308 go_all: Afficher tous les articles 308 go_all: "Afficher tous les articles"
309 go_tags: Afficher les tags 309 go_tags: "Afficher les tags"
310 go_config: Aller à la configuration 310 go_config: "Aller à la configuration"
311 go_import: Aller aux imports 311 go_import: "Aller aux imports"
312 go_developers: Aller à la section Développeurs 312 go_developers: "Aller à la section Développeurs"
313 go_howto: Afficher l'aide (cette page !) 313 go_howto: "Afficher l’aide (cette page !)"
314 go_logout: Se déconnecter 314 go_logout: "Se déconnecter"
315 list_title: Raccourcis disponibles dans les pages de liste 315 list_title: "Raccourcis disponibles dans les pages de liste"
316 search: Afficher le formulaire de recherche 316 search: "Afficher le formulaire de recherche"
317 article_title: Raccourcis disponibles quand on affiche un article 317 article_title: "Raccourcis disponibles quand on affiche un article"
318 open_original: Ouvrir l'URL originale de l'article 318 open_original: "Ouvrir l’URL originale de l’article"
319 toggle_favorite: Changer le statut Favori de l'article 319 toggle_favorite: "Changer le statut Favori de l’article"
320 toggle_archive: Changer le status Lu de l'article 320 toggle_archive: "Changer le status Lu de l’article"
321 delete: Supprimer l'article 321 delete: "Supprimer l’article"
322 material_title: Raccourcis disponibles avec le thème Material uniquement 322 material_title: "Raccourcis disponibles avec le thème Material uniquement"
323 add_link: Ajouter un nouvel article 323 add_link: "Ajouter un nouvel article"
324 hide_form: Masquer le formulaire courant (recherche ou nouvel article) 324 hide_form: "Masquer le formulaire courant (recherche ou nouvel article)"
325 arrows_navigation: Naviguer à travers les articles 325 arrows_navigation: "Naviguer à travers les articles"
326 open_article: Afficher l'article sélectionné 326 open_article: "Afficher l’article sélectionné"
327 327
328quickstart: 328quickstart:
329 page_title: "Pour bien débuter" 329 page_title: "Pour bien débuter"
@@ -385,8 +385,8 @@ tag:
385 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." 385 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."
386 see_untagged_entries: "Voir les articles sans tag" 386 see_untagged_entries: "Voir les articles sans tag"
387 new: 387 new:
388 add: 'Ajouter' 388 add: "Ajouter"
389 placeholder: 'Vous pouvez ajouter plusieurs tags, séparés par une virgule.' 389 placeholder: "Vous pouvez ajouter plusieurs tags, séparés par une virgule."
390 390
391import: 391import:
392 page_title: "Importer" 392 page_title: "Importer"
@@ -420,7 +420,7 @@ import:
420 how_to: "Choisissez le fichier de votre export Readability et cliquez sur le bouton ci-dessous pour l’importer." 420 how_to: "Choisissez le fichier de votre export Readability et cliquez sur le bouton ci-dessous pour l’importer."
421 worker: 421 worker:
422 enabled: "Les imports sont asynchrones. Une fois l’import commencé un worker externe traitera les messages un par un. Le service activé est :" 422 enabled: "Les imports sont asynchrones. Une fois l’import commencé un worker externe traitera les messages un par un. Le service activé est :"
423 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." 423 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."
424 firefox: 424 firefox:
425 page_title: "Import > Firefox" 425 page_title: "Import > Firefox"
426 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>" 426 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>"
@@ -489,16 +489,16 @@ developer:
489 back: "Retour" 489 back: "Retour"
490 490
491user: 491user:
492 page_title: Gestion des utilisateurs 492 page_title: "Gestion des utilisateurs"
493 new_user: Créer un nouvel utilisateur 493 new_user: "Créer un nouvel utilisateur"
494 edit_user: Éditer un utilisateur existant 494 edit_user: "Éditer un utilisateur existant"
495 description: Ici vous pouvez gérer vos utilisateurs (création, mise à jour et suppression) 495 description: "Ici vous pouvez gérer vos utilisateurs (création, mise à jour et suppression)"
496 list: 496 list:
497 actions: Actions 497 actions: "Actions"
498 edit_action: Éditer 498 edit_action: "Éditer"
499 yes: Oui 499 yes: "Oui"
500 no: Non 500 no: "Non"
501 create_new_one: Créer un nouvel utilisateur 501 create_new_one: "Créer un nouvel utilisateur"
502 form: 502 form:
503 username_label: "Nom d’utilisateur" 503 username_label: "Nom d’utilisateur"
504 name_label: "Nom" 504 name_label: "Nom"
@@ -513,9 +513,11 @@ user:
513 delete: "Supprimer" 513 delete: "Supprimer"
514 delete_confirm: "Voulez-vous vraiment ?" 514 delete_confirm: "Voulez-vous vraiment ?"
515 back_to_list: "Revenir à la liste" 515 back_to_list: "Revenir à la liste"
516 search:
517 placeholder: "Filtrer par nom d’utilisateur ou email"
516 518
517error: 519error:
518 page_title: Une erreur est survenue 520 page_title: "Une erreur est survenue"
519 521
520flashes: 522flashes:
521 config: 523 config:
@@ -528,10 +530,10 @@ flashes:
528 tagging_rules_updated: "Règles mises à jour" 530 tagging_rules_updated: "Règles mises à jour"
529 tagging_rules_deleted: "Règle supprimée" 531 tagging_rules_deleted: "Règle supprimée"
530 rss_token_updated: "Jeton RSS mis à jour" 532 rss_token_updated: "Jeton RSS mis à jour"
531 annotations_reset: Annotations supprimées 533 annotations_reset: "Annotations supprimées"
532 tags_reset: Tags supprimés 534 tags_reset: "Tags supprimés"
533 entries_reset: Articles supprimés 535 entries_reset: "Articles supprimés"
534 archived_reset: Articles archivés supprimés 536 archived_reset: "Articles archivés supprimés"
535 entry: 537 entry:
536 notice: 538 notice:
537 entry_already_saved: "Article déjà sauvegardé le %date%" 539 entry_already_saved: "Article déjà sauvegardé le %date%"
@@ -563,6 +565,6 @@ flashes:
563 client_deleted: "Client %name% supprimé" 565 client_deleted: "Client %name% supprimé"
564 user: 566 user:
565 notice: 567 notice:
566 added: 'Utilisateur "%username%" ajouté' 568 added: "Utilisateur \"%username%\" ajouté"
567 updated: 'Utilisateur "%username%" mis à jour' 569 updated: "Utilisateur \"%username%\" mis à jour"
568 deleted: 'Utilisateur "%username%" supprimé' 570 deleted: "Utilisateur \"%username%\" supprimé"
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
index 374071ce..5a9605ff 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
@@ -513,6 +513,8 @@ user:
513 # delete: Delete 513 # delete: Delete
514 # delete_confirm: Are you sure? 514 # delete_confirm: Are you sure?
515 # back_to_list: Back to list 515 # back_to_list: Back to list
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 # page_title: An error occurred 520 # page_title: An error occurred
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
index b01c611b..942bc257 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
@@ -513,6 +513,8 @@ user:
513 delete: 'Suprimir' 513 delete: 'Suprimir'
514 delete_confirm: 'Sètz segur ?' 514 delete_confirm: 'Sètz segur ?'
515 back_to_list: 'Tornar a la lista' 515 back_to_list: 'Tornar a la lista'
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 page_title: Una error s'es produsida 520 page_title: Una error s'es produsida
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
index d76ac328..fea90440 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
@@ -513,6 +513,8 @@ user:
513 delete: Usuń 513 delete: Usuń
514 delete_confirm: JesteÅ› pewien? 514 delete_confirm: JesteÅ› pewien?
515 back_to_list: Powrót do listy 515 back_to_list: Powrót do listy
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 page_title: Wystąpił błąd 520 page_title: Wystąpił błąd
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
index 98dfcd25..c59991f8 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
@@ -513,6 +513,8 @@ user:
513 delete: 'Apagar' 513 delete: 'Apagar'
514 delete_confirm: 'Tem certeza?' 514 delete_confirm: 'Tem certeza?'
515 back_to_list: 'Voltar para a lista' 515 back_to_list: 'Voltar para a lista'
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 # page_title: An error occurred 520 # page_title: An error occurred
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
index 8c07c13f..5846b7cc 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
@@ -513,6 +513,8 @@ user:
513 # delete: Delete 513 # delete: Delete
514 # delete_confirm: Are you sure? 514 # delete_confirm: Are you sure?
515 # back_to_list: Back to list 515 # back_to_list: Back to list
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 # page_title: An error occurred 520 # page_title: An error occurred
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
index bd21cb67..430fb96b 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
@@ -513,6 +513,8 @@ user:
513 # delete: Delete 513 # delete: Delete
514 # delete_confirm: Are you sure? 514 # delete_confirm: Are you sure?
515 # back_to_list: Back to list 515 # back_to_list: Back to list
516 search:
517 # placeholder: Filter by username or email
516 518
517error: 519error:
518 # page_title: An error occurred 520 # page_title: An error occurred
diff --git a/src/Wallabag/ImportBundle/Command/ImportCommand.php b/src/Wallabag/ImportBundle/Command/ImportCommand.php
index 28d01715..ce72837a 100644
--- a/src/Wallabag/ImportBundle/Command/ImportCommand.php
+++ b/src/Wallabag/ImportBundle/Command/ImportCommand.php
@@ -15,10 +15,11 @@ class ImportCommand extends ContainerAwareCommand
15 $this 15 $this
16 ->setName('wallabag:import') 16 ->setName('wallabag:import')
17 ->setDescription('Import entries from a JSON export') 17 ->setDescription('Import entries from a JSON export')
18 ->addArgument('userId', InputArgument::REQUIRED, 'User ID to populate') 18 ->addArgument('username', InputArgument::REQUIRED, 'User to populate')
19 ->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file') 19 ->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') 20 ->addOption('importer', null, InputArgument::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) 21 ->addOption('markAsRead', null, InputArgument::OPTIONAL, 'Mark all entries as read', false)
22 ->addOption('useUserId', null, InputArgument::OPTIONAL, 'Use user id instead of username to find account', false)
22 ; 23 ;
23 } 24 }
24 25
@@ -34,10 +35,14 @@ class ImportCommand extends ContainerAwareCommand
34 // Turning off doctrine default logs queries for saving memory 35 // Turning off doctrine default logs queries for saving memory
35 $em->getConnection()->getConfiguration()->setSQLLogger(null); 36 $em->getConnection()->getConfiguration()->setSQLLogger(null);
36 37
37 $user = $em->getRepository('WallabagUserBundle:User')->findOneById($input->getArgument('userId')); 38 if ($input->getOption('useUserId')) {
39 $user = $em->getRepository('WallabagUserBundle:User')->findOneById($input->getArgument('username'));
40 } else {
41 $user = $em->getRepository('WallabagUserBundle:User')->findOneByUsername($input->getArgument('username'));
42 }
38 43
39 if (!is_object($user)) { 44 if (!is_object($user)) {
40 throw new Exception(sprintf('User with id "%s" not found', $input->getArgument('userId'))); 45 throw new Exception(sprintf('User "%s" not found', $input->getArgument('username')));
41 } 46 }
42 47
43 switch ($input->getOption('importer')) { 48 switch ($input->getOption('importer')) {
diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php
index 92ee2b41..1c5c86d4 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 Pagerfanta\Adapter\DoctrineORMAdapter;
8use Pagerfanta\Exception\OutOfRangeCurrentPageException;
9use Pagerfanta\Pagerfanta;
7use Symfony\Component\HttpFoundation\Request; 10use Symfony\Component\HttpFoundation\Request;
8use Symfony\Bundle\FrameworkBundle\Controller\Controller; 11use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 12use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
10use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 13use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
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")
@@ -146,4 +132,49 @@ class ManageController extends Controller
146 ->getForm() 132 ->getForm()
147 ; 133 ;
148 } 134 }
135
136 /**
137 * @param Request $request
138 * @param int $page
139 *
140 * @Route("/list/{page}", name="user_index", defaults={"page" = 1})
141 *
142 * Default parameter for page is hardcoded (in duplication of the defaults from the Route)
143 * because this controller is also called inside the layout template without any page as argument
144 *
145 * @return \Symfony\Component\HttpFoundation\Response
146 */
147 public function searchFormAction(Request $request, $page = 1)
148 {
149 $em = $this->getDoctrine()->getManager();
150 $qb = $em->getRepository('WallabagUserBundle:User')->createQueryBuilder('u');
151
152 $form = $this->createForm(SearchUserType::class);
153 $form->handleRequest($request);
154
155 if ($form->isSubmitted() && $form->isValid()) {
156 $this->get('logger')->info('searching users');
157
158 $searchTerm = (isset($request->get('search_user')['term']) ? $request->get('search_user')['term'] : '');
159
160 $qb = $em->getRepository('WallabagUserBundle:User')->getQueryBuilderForSearch($searchTerm);
161 }
162
163 $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
164 $pagerFanta = new Pagerfanta($pagerAdapter);
165 $pagerFanta->setMaxPerPage(50);
166
167 try {
168 $pagerFanta->setCurrentPage($page);
169 } catch (OutOfRangeCurrentPageException $e) {
170 if ($page > 1) {
171 return $this->redirect($this->generateUrl('user_index', ['page' => $pagerFanta->getNbPages()]), 302);
172 }
173 }
174
175 return $this->render('WallabagUserBundle:Manage:index.html.twig', [
176 'searchForm' => $form->createView(),
177 'users' => $pagerFanta,
178 ]);
179 }
149} 180}
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/Repository/UserRepository.php b/src/Wallabag/UserBundle/Repository/UserRepository.php
index f913f52d..6adbe329 100644
--- a/src/Wallabag/UserBundle/Repository/UserRepository.php
+++ b/src/Wallabag/UserBundle/Repository/UserRepository.php
@@ -52,4 +52,17 @@ class UserRepository extends EntityRepository
52 ->getQuery() 52 ->getQuery()
53 ->getSingleScalarResult(); 53 ->getSingleScalarResult();
54 } 54 }
55
56 /**
57 * Retrieves users filtered with a search term.
58 *
59 * @param string $term
60 *
61 * @return QueryBuilder
62 */
63 public function getQueryBuilderForSearch($term)
64 {
65 return $this->createQueryBuilder('u')
66 ->andWhere('lower(u.username) LIKE lower(:term) OR lower(u.email) LIKE lower(:term) OR lower(u.name) LIKE lower(:term)')->setParameter('term', '%'.$term.'%');
67 }
55} 68}
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>